Sample output
$ openssl s_client -connect bash-hacks.com:443 -servername bash-hacks.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
subject=CN = bash-hacks.com
issuer=C = US, O = Amazon, CN = Amazon RSA 2048 M04
notBefore=Sep 2 00:00:00 2026 GMT
notAfter=Mar 18 23:59:59 2027 GMT
notAfter is the line you came for: this certificate stops validating on 18 March 2027. Both timestamps are GMT and always will be, because X.509 encodes times in UTC and openssl prints them back unconverted, so there is no TZ you can set to make them local. The issuer line is worth a glance too, since a certificate that renewed overnight under a different issuer than you expected usually means something upstream changed hands.
When you would reach for it
A site that works in your browser and fails from a server, a health check that started returning a certificate error at 3am, or the quarterly question of which of your forty hostnames renews next. It is also the fastest way to see what a load balancer is actually serving, as opposed to what the control panel claims it configured, because this asks the socket rather than the dashboard. When a colleague says the certificate is fine, this is the command that settles it in one line.
Gotchas
2>/dev/nullhides the verdict, not the noise. The chain walk and theverify errorline both go to standard error, so suppressing it means an expired certificate prints its dates without complaint and the pipeline still exits 0. Point the command atexpired.badssl.comand you get a tidynotAfter=Apr 12 23:59:59 2015 GMTwith no hint that anything is wrong. If an exit code matters, add-verify_return_errorand drop the redirect, which makess_clientexit 1 on a failed chain. For a human reading the terminal,-briefprints the protocol, the cipher, and aVerification: OKorVerification error:line and nothing else.- Point
-connectat an IP address and SNI silently disappears. There is no hostname to derive it from, and an IP is not a legal value for the extension, so unless you supply-servernameyourself nothing is sent. A shared address answers that withsslv3 alert handshake failureandSSL alert number 40,s_clientreportsno peer certificate available, andx509then fails on empty input withCould not read certificate from <stdin>. The error you read comes from the last command in the pipeline, three steps from the flag that caused it. This matters precisely when you are testing one node behind a load balancer, which is the case where you most wanted an answer. - Without
</dev/nullthe command appears to hang. It has not hung. The handshake finished ands_clientis waiting for you to type something at the server, which is the entire point of the tool in interactive use.Qon its own line closes the session cleanly, and Ctrl-C works too. - The certificate is not the whole story, the chain is.
s_clientprints only the leaf to standard output, so a server that forgot to send its intermediate looks perfect here and fails in a browser.-showcertsprints every certificate the server sent, and counting them is usually how you find the missing one. - macOS ships LibreSSL as
/usr/bin/openssl, not OpenSSL. The flags above are old enough to exist in both, but newer subcommand options are not guaranteed:x509 -ext subjectAltName, the neat way to list which hostnames a certificate covers, is an OpenSSL addition. Runopenssl versionto see which you have, and fall back toopenssl x509 -noout -textand read the Subject Alternative Name block, which works everywhere.
Variants
$ openssl s_client -connect bash-hacks.com:443 -servername bash-hacks.com </dev/null 2>/dev/null | openssl x509 -noout -checkend 604800
Answers in an exit code. -checkend takes a number of seconds and returns 0 if the certificate survives that long, 1 if it does not, so 604800 asks whether you have a week left and drops straight into an if. It also prints Certificate will not expire, which you can discard once the status is what you are reading
$ for h in bash-hacks.com example.com www.debian.org; do
printf '%-20s ' "$h"
openssl s_client -connect "$h:443" -servername "$h" </dev/null 2>/dev/null |
openssl x509 -noout -enddate
done
A whole fleet in one pass. printf with %-20s pads the hostname into a column so the dates line up, and quoting "$h:443" keeps the loop safe on hostnames you did not write yourself
$ openssl s_client -starttls smtp -connect smtp.gmail.com:587 -servername smtp.gmail.com </dev/null 2>/dev/null | openssl x509 -noout -dates
For protocols that begin in plaintext and upgrade mid-conversation. -starttls speaks the protocol’s own upgrade command first, and it accepts smtp, pop3, imap, ftp, xmpp, telnet, irc, mysql, postgres, lmtp, nntp, sieve, and ldap