Certificate problems are confusing for a specific reason: the certificate is almost never the thing that is wrong, and openssl will happily dump a page of fields that are all technically correct while the actual failure sits in a section you did not know to look at. This is the workflow for getting from “the browser says this site is not private” to a specific answer, using only a command line.

The loop is always the same: use s_client to fetch what the server actually sends, then read the leaf certificate with x509 -text. Two fields cause most real incidents — notAfter, which is the expiry, and subjectAltName, which is where hostnames have lived since the common name was deprecated and where most people are still not looking. For the two checks worth automating, -checkend and -checkhost both return a useful exit status rather than only printing.

Key takeaways:

  • -servername is not optional for HTTPS. Without it you test the default virtual host, which is often not the one you meant.
  • Modern clients ignore the certificate’s common name and check subjectAltName. A certificate whose CN matches but whose SANs do not is broken for browsers.
  • A server sends its leaf and usually the intermediates, but not the root. A chain that fails to build locally is often missing the root you have on disk, not a server fault.
  • openssl x509 does not check revocation. Use s_client -status to see stapled OCSP, and expect nothing if the server does not staple.
  • -checkend and -checkhost are designed for monitoring because they signal through the exit status.

The one-liner for a quick answer

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

Two things to understand about that pipeline. < /dev/null closes the input so s_client terminates instead of waiting for you; without it the command hangs. And 2>/dev/null drops s_client‘s own diagnostics, which are useful interactively and noise in a script.

For an even shorter summary, -brief on the client side:

openssl s_client -connect example.com:443 -servername example.com -brief < /dev/null

Fetching the chain

# Everything the server sends, as PEM, including intermediates
openssl s_client -connect example.com:443 -servername example.com 
  -showcerts < /dev/null

# Save the leaf certificate on its own
openssl s_client -connect example.com:443 -servername example.com 
  -showcerts < /dev/null 2>/dev/null 
  | awk '/BEGIN CERT/,/END CERT/' > chain.pem

# First certificate only, in its own file
openssl s_client -connect example.com:443 -servername example.com 
  < /dev/null 2>/dev/null | openssl x509 -outform PEM > leaf.pem

The second command captures the whole chain, which is what you want for offline verification. The third is cleaner for a single certificate, because x509 consumes one certificate and emits it in the format you asked for.

Reading the certificate properly

# Everything, human readable, no PEM wrapper
openssl x509 -in leaf.pem -text -noout

# The fields you usually want, one per line
openssl x509 -in leaf.pem -noout 
  -subject -issuer -dates -serial -fingerprint -sha256

# Hostnames, which live in the SAN extension
openssl x509 -in leaf.pem -noout -ext subjectAltName

# Human-readable subject and issuer
openssl x509 -in leaf.pem -noout -subject -nameopt RFC2253
openssl x509 -in leaf.pem -noout -issuer  -nameopt RFC2253

-noout suppresses the base64 body, which is essential when you are reading output and irrelevant when you are piping. -nameopt RFC2253 makes the subject readable rather than the default reversed ordering, which trips up anyone who has typed a DN into openssl by hand.

If you already have a PEM and just need it interpreted rather than fetched, the SSL certificate decoder parses subject, issuer, expiry and chain in the browser without anything touching a server. Use it when the certificate is already in hand and you are not testing a live endpoint; use the commands above when you need to test what a server actually serves.

The two fields that cause most incidents

notAfter, the expiry

openssl x509 -in leaf.pem -noout -dates
# notBefore=Oct  1 00:00:00 2026 GMT
# notAfter=Dec 30 23:59:59 2026 GMT

# Will it still be valid in 30 days? Exit status is the answer.
openssl x509 -in leaf.pem -checkend 2592000 -noout
echo $?     # 0 = fine, non-zero = expiring or expired

-checkend is built for monitoring because it returns a non-zero exit status rather than only printing. That makes it usable directly as a health check. Watch the unit carefully: the argument is a count of seconds, not days, and confusing the two produces a check that always passes.

subjectAltName, the hostnames

openssl x509 -in leaf.pem -noout -ext subjectAltName
# X509v3 Subject Alternative Name:
#     DNS:example.com, DNS:www.example.com, IP Address:192.0.2.10

This is the single most valuable habit to build. A certificate’s common name can say example.com while its SANs list only old.example.com. Older clients accepted that. Current browsers do not read the common name for hostname verification at all. They use the SAN extension, and a certificate with no SANs is rejected outright.

So when someone reports a name-mismatch error, check the SANs before the CN. In a certificate with both, the CN is decoration.

You can also test a specific hostname against a certificate directly, which is more precise than reading the list and comparing by eye:

openssl x509 -in leaf.pem -noout -checkhost example.com
echo $?     # 0 = this certificate covers that hostname

Verifying a chain

openssl verify checks a certificate against a set of trusted roots and intermediates. Getting this right requires knowing what you have:

# Trust the system roots, and see what the server actually sends
openssl s_client -connect example.com:443 -servername example.com 
  -showcerts -verify_return_error < /dev/null

# Verify offline with an explicit chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt leaf.pem

# Verify with a specific intermediate bundle
openssl verify -untrusted intermediates.pem -CAfile root.pem leaf.pem

-verify_return_error matters: without it, s_client reports a verification failure and then carries on, exiting successfully. In a script that means a broken chain looks like a healthy connection.

A chain failure is usually one of three things. The server is not sending its intermediate, which is a server misconfiguration and the most common cause. Your local root store is missing or outdated. Or the chain is genuinely mis-issued. Reading the verification output in order tells you which, because openssl names the point at which the chain could not be completed.

Revocation, protocol features, and what openssl does not check

# Ask for and print a stapled OCSP response
openssl s_client -connect example.com:443 -servername example.com 
  -status < /dev/null

# Request and print Certificate Transparency SCTs
openssl s_client -connect example.com:443 -servername example.com 
  -ct < /dev/null

# See ALPN protocols the server will negotiate
openssl s_client -connect example.com:443 -servername example.com 
  -alpn h2 -brief < /dev/null

# TLS version in use
openssl s_client -connect example.com:443 -servername example.com -brief < /dev/null 2>&1 
  | grep -i 'protocol|cipher'

# Inspect a specific TLS extension in detail
openssl s_client -connect example.com:443 -servername example.com 
  -tlsextdebug < /dev/null 2>&1 | less

Two limits worth stating plainly. First, openssl x509 does not check revocation; it validates structure, dates and names only. Revocation needs OCSP or CRL, and OCSP is only visible if the server staples it, which many do not. Second, s_client does not enforce modern browser policy by default. It will happily negotiate a protocol or cipher that no current browser would accept, so it tells you what the server supports, not what a visitor will experience. For that, test with an actual browser or a scanner that models current clients.

For the stricter question, “would a browser accept this certificate for this purpose?” — -purpose applies the validation rules for a given use:

openssl x509 -in leaf.pem -noout -purpose
openssl x509 -in leaf.pem -noout -purpose sslserver

Other protocols, and interactive mode

# STARTTLS rather than implicit TLS
openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com
openssl s_client -connect example.com:443 -starttls http -servername example.com

Without < /dev/null, s_client stays interactive and gives you a session. Three commands are worth remembering: Q to quit, R to renegotiate, and C to send a client certificate, which is how you test mutual TLS from the client side.

Monitoring, which is the point

#!/usr/bin/env bash
set -uo pipefail

host="${1:?usage: check-cert host}"
days="${2:-30}"
seconds=$(( days * 86400 ))

cert=$(openssl s_client -connect "$host:443" -servername "$host" 
  < /dev/null 2>/dev/null) || { echo "$host unreachable"; exit 2; }

if ! printf '%s' "$cert" | openssl x509 -checkend "$seconds" -noout; then
  echo "FAIL $host expires within $days days"
  exit 1
fi

if ! printf '%s' "$cert" | openssl x509 -checkhost "$host" -noout; then
  echo "FAIL $host certificate does not cover the hostname"
  exit 1
fi

echo "OK $host valid for more than $days days"

Two checks, both driven by exit status rather than by parsing output, because parsing x509 -text breaks the next time the wording changes. Add a third for the chain, using -verify_return_error and checking that the connection itself succeeded.

Set the warning threshold well before expiry. Certificate renewal goes wrong for operational reasons, a DNS record, an automated renewal that stopped, a deploy that shipped the old certificate — and those take hours to fix, not minutes.

A quick triage order

  • Is the name in the SANs? Check before looking at anything else.
  • Is notAfter in the future? And is it in the future for the client, which may have a wrong clock?
  • Does the chain build, with -verify_return_error so a failure actually fails?
  • Is the server sending its intermediate? Check with -showcerts.
  • Is the algorithm, key size or validity period something the client will accept? x509 -text shows signature algorithm, public key size and dates in one output.
  • Only then look at protocol and cipher, which cause fewer problems than the five above and are easier to test in a browser.

Certificate expiry is the most common internet-facing outage, and it is entirely preventable. what a major outage teaches us about reliability covers the organisational side of running infrastructure that other people depend on, and how to check whether a service is down covers the diagnostic workflow this fits into.

References

Related Tools

Try these companion tools on DevStackIO Tools:

  • JWT decoder — decode and inspect JWT tokens alongside your TLS certificate analysis.