Reading an SSL certificate with openssl — a tactical reference
By Nick Phillips, Founder
openssl is the swiss army knife of cert debugging, and its UI is, charitably, from a different era. The flags are inconsistent across subcommands, the help text is missing in places, and the same task can usually be accomplished four different ways. This post is the cheatsheet I wish I'd had — the commands you actually need, with the flags that actually matter, and a sentence on what each one is doing.
If you only want the expiration date, the five-ways post covers it in three flavors. This post is the depth version: everything else you can read off a live cert, and how.
The base command
Almost every recipe in this post starts from one of these two openssl invocations. Memorize the shape; the rest is just piping into x509 with different -no* flags.
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null
That's "do a TLS handshake against example.com:443, send an empty line so it closes cleanly, throw away the chatty stderr." What comes back on stdout is the handshake details followed by the cert(s) in PEM format.
To parse the cert with x509, pipe to it:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout <FLAGS>
-noout means "don't re-print the PEM block, just give me the parsed info."
The -servername flag is the one most people forget. Without it, you get whatever cert the server hands out for an empty SNI — frequently a default cert on shared infrastructure, which isn't what you wanted to inspect.
Recipes
Expiration date
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate
notAfter=Apr 1 23:59:59 2026 GMT
To get both:
... | openssl x509 -noout -dates
notBefore=Jan 1 00:00:00 2026 GMT
notAfter=Apr 1 23:59:59 2026 GMT
To check whether the cert is expired right now:
... | openssl x509 -noout -checkend 0
Exits 0 if the cert hasn't expired, 1 if it has. -checkend N checks "expires within N seconds," useful for cron-friendly "warn me 30 days ahead":
... | openssl x509 -noout -checkend $((30 * 86400)) || echo "expires in 30 days or less"
Issuer and subject
... | openssl x509 -noout -issuer -subject
issuer=C=US, O=Let's Encrypt, CN=R3
subject=CN=example.com
If you want the same info but more parseable:
... | openssl x509 -noout -issuer -subject -nameopt RFC2253
-nameopt RFC2253 gives you the comma-separated DN format that's easier to grep.
Subject Alternative Names
This is the field that lists every hostname the cert covers — including the primary hostname (the CN is more or less ceremonial these days; SAN is what browsers actually check).
... | openssl x509 -noout -ext subjectAltName
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com, DNS:api.example.com
If your openssl is old enough that -ext doesn't exist (pre-1.1.1), fall back to the bulky version:
... | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
The full handshake details (issuer chain, cipher, protocol)
For when "what cert is being served" isn't enough and you want everything openssl saw:
echo | openssl s_client -servername example.com -connect example.com:443
(Note: no 2>/dev/null this time — the interesting bits are on stderr.) You'll see:
Server certificate— the leafCertificate chain— every cert the server sent, with issuer/subject for eachNew, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384— protocol version and cipherVerification: OK— whether the chain validated against the system trust store
If Verification shows an error code, the next thing to look at is Verify return code: near the bottom — it'll tell you why (expired, untrusted root, hostname mismatch, etc.).
Fingerprints
For pinning, comparison, or just identifying a specific cert:
... | openssl x509 -noout -fingerprint -sha256
sha256 Fingerprint=AA:BB:CC:DD:...
SHA-1 fingerprints are still around in older docs (-fingerprint -sha1) but are not security-meaningful anymore. Use SHA-256.
Public key (for SPKI pinning)
If you're pinning the public key rather than the whole cert:
... | openssl x509 -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| base64
The output is the base64-encoded SPKI hash, which is what HTTP Public Key Pinning expected (and what HTTPS Pin Reporting still uses if you've configured it).
Save the cert to a file
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -outform PEM > example.com.pem
You can then re-run any of the recipes above against example.com.pem instead of fetching it live:
openssl x509 -in example.com.pem -noout -dates
Useful for keeping a known-good copy around for diffing against the live cert.
The entire chain, not just the leaf
openssl s_client will print the chain in its stderr output, but if you want each cert as a separate PEM block:
echo | openssl s_client -showcerts -servername example.com -connect example.com:443 2>/dev/null
The -showcerts flag tells it to dump every cert in PEM form. From there you can split with awk or csplit and inspect each individually.
Validate against a specific CA bundle
If your platform's default trust store isn't the one you care about (e.g. you're debugging what an old Android device sees), point openssl at a different bundle:
echo | openssl s_client -servername example.com -connect example.com:443 \
-CAfile /path/to/old-android-bundle.pem
The Verification: line at the end of the output tells you whether the cert was trusted by that bundle. Handy for figuring out why a specific client population is hitting "untrusted root" errors after a chain change.
Check a non-443 port
For SMTP, IMAP, custom ports — anything that isn't standard HTTPS:
openssl s_client -starttls smtp -connect mail.example.com:587
openssl s_client -starttls imap -connect mail.example.com:143
openssl s_client -connect mqtt.example.com:8883 # custom port
-starttls is for protocols that begin in cleartext and upgrade. Without it, openssl will try to start the TLS handshake immediately, which won't work on STARTTLS-only ports.
When openssl isn't the right tool
A few cases where reaching for openssl is the wrong reflex:
- You want to inspect a cert chain that includes an unusual extension (CT log entries, ACME identifiers). Use
openssl x509 -textfor a verbose dump, butgnutls-cliand especiallystep certificate inspect(from smallstep) are nicer to read. - You're writing a script that needs to be portable. Don't shell out to openssl from a real programming language — use the language's TLS library. The output formatting of openssl changes between versions, and your script will break.
- You need a quick visual check from a phone or borrowed laptop. The free checker on /check takes a domain and shows you the same fields, no terminal required.
For ongoing monitoring (which is the whole point of being able to read notAfter), openssl in a cron job is fine for one or two hosts but doesn't scale and shares fate with the host it's running on. The monitoring tools comparison covers when to graduate from "openssl + cron" to "something that runs from outside your box."
And if you'd like daily checks for the domains you actually care about — without writing a single shell script — signing up takes about a minute.
For everything else: this page is bookmark-able. There are about a dozen flags worth knowing, and the rest you can google when you need them.
Catch the next cert expiry before your users do.
Otterwatch checks your SSL certificates daily and emails you 30 days before they expire. Five sites free.
Start watching →