Check SSL certificate expiration date with curl & openssl
In this example I will use my own domain https://michalwojcik.com.pl
Certificate can be obtained through the browser, but one has to be awared that browsers can cache certificates. So if we change them, we will have to wait some time to invalidate cache and let the browser retrieve cert one more time. Partial solution is doing this check in incognito window.
$ curl https://michalwojcik.com.pl ... lots of output ... $ curl -s https://michalwojcik.com.pl | wc -l 773
Hence we want to see only details of certificate, we have to use some flags.
$ curl -I https://michalwojcik.com.pl HTTP/2 200 date: Tue, 15 Feb 2022 21:29:28 GMT content-type: text/html; charset=UTF-8 link: <https://michalwojcik.com.pl/wp-json/>; rel="https://api.w.org/" cf-cache-status: DYNAMIC expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=bhSYKngOkqycDBNX%2B96aUqJURULp3AvWZjIKl6%2FYM%2F1AbHJIEkA8%2B0L0gQ%2BphLDfhUYid68pK3IjTOAomPg1%2FDsv%2BwcmCbCQj2nzFM9x4Dhr91cwmwlXp0iHstWcT2znPSCcQ0yW"}],"group":"cf-nel","max_age":604800} nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800} strict-transport-security: max-age=7776000 server: cloudflare cf-ray: 6de1a1c2ca587803-VIE alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
-I
stands for HEAD
request – there is no body and we can see headers only. By adding verbose -v
flag, we will see even more details.
$ curl -Iv https://michalwojcik.com.pl * Trying 188.114.96.20:443... * Connected to michalwojcik.com.pl (188.114.96.20) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/cert.pem * CApath: none * TLSv1.2 (OUT), TLS handshake, Client hello (1): * TLSv1.2 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305 * ALPN, server accepted to use h2 * Server certificate: * subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com * start date: Jul 11 00:00:00 2021 GMT * expire date: Jul 10 23:59:59 2022 GMT * subjectAltName: host "michalwojcik.com.pl" matched cert's "michalwojcik.com.pl" * issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3 * SSL certificate verify ok. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x7fb54a80da00) > HEAD / HTTP/2 > Host: michalwojcik.com.pl > user-agent: curl/7.77.0 > accept: */* > * Connection state changed (MAX_CONCURRENT_STREAMS == 256)! < HTTP/2 200 HTTP/2 200 < date: Tue, 15 Feb 2022 21:30:55 GMT date: Tue, 15 Feb 2022 21:30:55 GMT < content-type: text/html; charset=UTF-8 content-type: text/html; charset=UTF-8 < link: <https://michalwojcik.com.pl/wp-json/>; rel="https://api.w.org/" link: <https://michalwojcik.com.pl/wp-json/>; rel="https://api.w.org/" < cf-cache-status: DYNAMIC cf-cache-status: DYNAMIC < expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" < report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=NF1A%2FxHyeNwzVfnCUDQDNd2NYoauGzJCahrMTwDw06v99QDskzY5u%2Bpq1bc94leMfkrD6W%2FvHXjmtqZpZIwst1ciB%2FQGvvtjLcjafTCxtjkNZU8FBIoD3TMQIPg5cQwefqE5bw2N"}],"group":"cf-nel","max_age":604800} report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=NF1A%2FxHyeNwzVfnCUDQDNd2NYoauGzJCahrMTwDw06v99QDskzY5u%2Bpq1bc94leMfkrD6W%2FvHXjmtqZpZIwst1ciB%2FQGvvtjLcjafTCxtjkNZU8FBIoD3TMQIPg5cQwefqE5bw2N"}],"group":"cf-nel","max_age":604800} < nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800} nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800} < strict-transport-security: max-age=7776000 strict-transport-security: max-age=7776000 < server: cloudflare server: cloudflare < cf-ray: 6de1a3e07fc07803-VIE cf-ray: 6de1a3e07fc07803-VIE < alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400 alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400 < * Connection #0 to host michalwojcik.com.pl left intact
Above we can see details of TLS handshake and information aboot TLS cert. Hence curl
writes response to stderr
we are unable to grep
over it. Instead of it, use following flags:
$ curl -Iv --stderr - https://michalwojcik.com.pl | grep "expire date" * expire date: Jul 10 23:59:59 2022 GMT $ curl -Iv 2>&1 https://michalwojcik.com.pl | grep "expire date" * expire date: Jul 10 23:59:59 2022 GMT
--stderr -
will direct output from curl to stdout, which is equivalent to 2>&1
openssl
The same we can achieve using openssl
$ openssl s_client -connect michalwojcik.com.pl:443 | openssl x509 -noout -dates depth=2 C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root verify return:1 depth=1 C = US, O = "Cloudflare, Inc.", CN = Cloudflare Inc ECC CA-3 verify return:1 depth=0 C = US, ST = California, L = San Francisco, O = "Cloudflare, Inc.", CN = sni.cloudflaressl.com verify return:1 notBefore=Jun 10 00:00:00 2022 GMT notAfter=Jun 10 23:59:59 2023 GMT