Sooner or later every certificate question comes down to the same handful of openssl invocations, and I never seem to remember the exact flag for "just show me the SANs" when I'm three terminals deep into an incident. So here is the cheat sheet I wish I had pinned somewhere. Two halves: inspecting a cert file on disk, and inspecting a live endpoint over the wire.

Inspecting a certificate file

The workhorse subcommand is x509. By itself it'll dump the entire certificate in human-readable form:

openssl x509 -in cert.pem -noout -text

-noout suppresses the re-encoded PEM blob at the end (you almost always want this), and -text is the full decode. That's great when you want everything, but most of the time you want one specific field.

Subject Alternative Names

openssl x509 -in cert.pem -noout -ext subjectAltName

The -ext flag lets you target a single extension by name. This is the one I reach for constantly, because the SAN list is what actually matters for hostname matching, not the legacy CN.

Expiration and validity dates

# Both notBefore and notAfter
openssl x509 -in cert.pem -noout -dates

# Just the expiry
openssl x509 -in cert.pem -noout -enddate

# Just the start
openssl x509 -in cert.pem -noout -startdate

Subject and issuer

openssl x509 -in cert.pem -noout -subject -issuer

Handy for confirming who signed a cert, especially when you're chasing an intermediate problem.

Fingerprints and serial

openssl x509 -in cert.pem -noout -fingerprint -sha256
openssl x509 -in cert.pem -noout -serial

The SHA-256 fingerprint is the quickest way to confirm two cert files are byte-for-byte the same thing without diffing them.

The expiry check worth memorizing

# Exit 0 if still valid right now, nonzero if already expired
openssl x509 -in cert.pem -noout -checkend 0

# Exit nonzero if it expires within 30 days (30 * 24 * 3600)
openssl x509 -in cert.pem -noout -checkend 2592000

-checkend takes a number of seconds and sets the exit code based on whether the cert expires within that window. It prints nothing useful, the value is the exit code, which makes it perfect for cron jobs and monitoring scripts. A tiny expiry alarm is just:

openssl x509 -in cert.pem -noout -checkend 2592000 \
  || echo "cert expires within 30 days"

Inspecting a live endpoint

When the cert isn't a file you have, it's whatever a server is actually presenting, you pull it down with s_client and pipe it straight into x509:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName -dates

A few things going on in that line:

  • echo | feeds an immediate EOF to stdin so s_client doesn't sit there waiting for you to type an HTTP request.
  • 2>/dev/null drops the connection chatter so only the cert decode comes through.
  • -servername sets the SNI hostname, and this matters more than anything else here.

Always set -servername on SNI hosts

On any multi-tenant listener, and that describes most modern hosting, the server decides which certificate to send based on the SNI hostname in the TLS handshake. If you omit -servername, you get whatever default cert the listener falls back to, which is frequently not the one you're trying to debug. The symptom is maddening: the file on disk looks right, the live check looks wrong, and you burn twenty minutes before realizing you never sent SNI. Set it every time.

Seeing the full chain

echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null

-showcerts prints every certificate the server sends, leaf plus intermediates. This is how you catch the classic "works in my browser but fails in curl" problem, where the server is presenting an incomplete chain and your browser is quietly filling in a cached intermediate that other clients don't have.

Checking a specific protocol or port

s_client isn't limited to 443. For services that use STARTTLS you tell it the protocol:

# SMTP on 25
openssl s_client -connect mail.example.com:25 -starttls smtp

# IMAP, POP3, etc. also supported
openssl s_client -connect mail.example.com:143 -starttls imap

A couple of bonus one-liners

Decode a CSR instead of a cert:

openssl req -in request.csr -noout -text

Confirm a private key and a certificate actually match (the two moduli should hash to the same value):

openssl x509 -in cert.pem -noout -modulus | openssl md5
openssl rsa  -in key.pem  -noout -modulus | openssl md5

If those two hashes differ, you've got a mismatched key and cert, which is one of the more common reasons a freshly deployed cert refuses to load.

The short version

If you only keep two lines in your head, keep these:

# File on disk
openssl x509 -in cert.pem -noout -ext subjectAltName -dates

# Live endpoint
echo | openssl s_client -connect HOST:443 -servername HOST 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName -dates

Everything else is a flag away once you're staring at -text.