← back to latest

See when a TLS certificate expires without opening a browser

openssl s_client -connect bash-hacks.com:443 -servername bash-hacks.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

anatomy

openssl s_client
A generic TLS client that speaks the handshake and then hands you the raw socket. It exists for debugging, not for fetching, which is why it prints the whole negotiation instead of a web page. Everything you learn here about the connection is what a browser learns before it decides whether to show you a padlock.
-connect host:443
Where to open the TCP socket. This half is pure networking: a hostname resolved to an address, and a port. It says nothing about which site you want. On a host serving one certificate the difference never surfaces; on a CDN edge serving thousands, the address and the name are two separate questions and only one of them is answered here.
-servername
Sends Server Name Indication in the handshake, the field that tells a shared host which certificate to present. Since OpenSSL 1.1.1 the client fills this in from the `-connect` hostname on its own, so here it is redundant and deliberate. It stops being redundant the moment the two differ, which is the whole reason to keep it in the line you memorize: swap `-connect` for a raw IP and the `-servername` stays, and you are now asking one specific backend what it serves for that name.
</dev/null
Closes standard input. After the handshake completes, `s_client` turns into a pipe between your keyboard and the server and sits there, because that is what you want when you are typing HTTP or SMTP by hand. In a script it reads as a hang. Feeding it EOF makes it finish the handshake, print, and exit.
2>/dev/null
Discards the certificate chain walk and the verification verdict, which go to standard error. It buys a clean pipeline and costs you the one line that says whether the chain is trusted. See the gotchas before you accept that trade in anything automated.
| openssl x509
A second, separate invocation. `s_client` writes the leaf certificate to standard output as PEM text, and `x509` is the subcommand that parses PEM. The pipe is the join between a network tool and a parsing tool, and it is why the certificate never has to touch disk.
-noout
Suppresses the re-encoded PEM block that `x509` prints by default. Without it you get the certificate back out again, base64 and all, after the fields you asked for. It is the difference between four lines and forty.
-subject -issuer -dates
The three questions worth asking: who the certificate is for, who signed it, and when it stops being valid. `-dates` is shorthand for `-startdate -enddate` together, printed as `notBefore` and `notAfter`.

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/null hides the verdict, not the noise. The chain walk and the verify error line 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 at expired.badssl.com and you get a tidy notAfter=Apr 12 23:59:59 2015 GMT with no hint that anything is wrong. If an exit code matters, add -verify_return_error and drop the redirect, which makes s_client exit 1 on a failed chain. For a human reading the terminal, -brief prints the protocol, the cipher, and a Verification: OK or Verification error: line and nothing else.
  • Point -connect at 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 -servername yourself nothing is sent. A shared address answers that with sslv3 alert handshake failure and SSL alert number 40, s_client reports no peer certificate available, and x509 then fails on empty input with Could 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/null the command appears to hang. It has not hung. The handshake finished and s_client is waiting for you to type something at the server, which is the entire point of the tool in interactive use. Q on its own line closes the session cleanly, and Ctrl-C works too.
  • The certificate is not the whole story, the chain is. s_client prints only the leaf to standard output, so a server that forgot to send its intermediate looks perfect here and fails in a browser. -showcerts prints 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. Run openssl version to see which you have, and fall back to openssl x509 -noout -text and 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

lineage

The `openssl` command is a fossil of a single-handed project. Eric A. Young began writing SSLeay in Australia in 1995, with Tim Hudson joining soon after, at a time when the SSL protocol was a Netscape document and no free implementation existed. The initials in the name are Young's own, and traces of them survive in the library to this day: the `eay` in header names and the `SSLeay_version` function that OpenSSL kept as an alias for years. When Young and Hudson both went to work for RSA Security in 1998, development of SSLeay stopped, and a group of outside contributors forked the last release into OpenSSL. The swiss-army dispatcher survived the fork intact, which is why one binary still answers to `s_client`, `x509`, `req`, `genrsa`, and several dozen other subcommands that share little beyond a parser. The project ran for sixteen years on donations and a handful of part-time maintainers until Heartbleed in 2014 made its funding situation a news story, which produced the Core Infrastructure Initiative on one side and OpenBSD's LibreSSL fork on the other. Apple shipped LibreSSL in macOS shortly after, so the `openssl` on a Mac and the `openssl` on a Linux server have been cousins rather than siblings ever since.