Every certificate check with OpenSSL is two commands joined by a pipe. openssl s_client speaks TLS to the host and dumps the PEM of the certificates it received; openssl x509 parses whatever PEM arrives on stdin. Once you understand that split, every one-liner on this page is a variation on the second half.
What is the minimum command to read an expiry date?
Three details make the difference between a command that works and one that hangs. echo | closes stdin so s_client does not sit waiting for you to type SMTP-style commands. 2>/dev/null discards the handshake chatter that would otherwise confuse the x509 parser. And -servername sends SNI — without it, a shared host will hand you the default vhost's certificate and you will debug the wrong one.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates
# notBefore=Jul 29 22:10:08 2026 GMT
# notAfter=Oct 27 22:17:21 2026 GMTIf you only want the expiry, swap -dates for -enddate. To read a certificate already on disk, drop the s_client half entirely: openssl x509 -in /etc/ssl/certs/example.com.crt -noout -enddate.
How do I test expiry in a script without parsing dates?
-checkend takes a number of seconds and sets the exit status: 0 if the certificate is still valid that far ahead, 1 if it will have expired. This is the right primitive for monitoring because it involves no date arithmetic, no locale assumptions, and no timezone bugs.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -checkend 2592000
# "Certificate will not expire" → exit 0
# "Certificate will expire" → exit 1
# Useful windows:
# 604800 = 7 days
# 1209600 = 14 days
# 2592000 = 30 daysHow do I see the whole chain, not just the leaf?
By default x509 reads only the first certificate on stdin — your leaf. Intermediates expire too, and an expired intermediate breaks the site just as thoroughly. -showcerts prints the full presented chain, and the crl2pkcs7 trick below turns it into a readable subject/issuer list.
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null \
| openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
| openssl pkcs7 -print_certs -noout
# subject=CN=example.com
# issuer=C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
#
# subject=C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
# issuer=C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2While you are in there, the verification verdict is worth reading. Verify return code: 0 (ok) means the chain built successfully; code 21 means an intermediate is missing, which is covered in Fix an Incomplete SSL Certificate Chain.
How do I check which hostnames a certificate actually covers?
Modern clients ignore the Common Name entirely and validate against the Subject Alternative Name extension. A certificate that is perfectly in-date will still fail if the hostname is not in its SAN list — which is why a name mismatch and an expiry problem are often confused.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
# X509v3 Subject Alternative Name:
# DNS:example.com, DNS:*.example.com
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -serialDNS:*.example.com covers www.example.com but not a.b.example.com and not the bare example.com. Both names must be listed separately, as they are above.How do I check several hosts at once?
A shell loop over a hostname list gets you a fleet-wide view in seconds. Keep the list in a file and this becomes a cron job.
for h in example.com github.com cloudflare.com; do
end=$(echo | openssl s_client -connect "$h:443" -servername "$h" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
printf '%-24s %s\n' "$h" "${end:-UNREACHABLE}"
done
# example.com Oct 27 22:17:21 2026 GMT
# github.com Sep 30 23:59:59 2026 GMT
# cloudflare.com Oct 6 22:47:27 2026 GMTHow do I build an expiry alert?
Use -checkend for the decision and only compute a day count for the human-readable message. The script below exits non-zero if any host is inside the warning window, which is exactly what Nagios-style checks, cron MAILTO, and CI jobs need.
#!/usr/bin/env bash
set -u
WINDOW_DAYS=21
WINDOW=$(( WINDOW_DAYS * 86400 ))
STATUS=0
while read -r host; do
[ -z "$host" ] && continue
pem=$(echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null)
if [ -z "$pem" ]; then
echo "CRIT $host connection failed"
STATUS=2
continue
fi
end=$(printf '%s' "$pem" | openssl x509 -noout -enddate | cut -d= -f2)
if printf '%s' "$pem" | openssl x509 -noout -checkend "$WINDOW" >/dev/null; then
echo "OK $host expires $end"
else
echo "WARN $host expires $end (within $WINDOW_DAYS days)"
STATUS=1
fi
done < hosts.txt
exit "$STATUS"If you want a day count in the message, note that date parsing is not portable. GNU coreutils accepts the OpenSSL format directly with date -d "$end" +%s; BSD/macOS needs an explicit format string: date -j -f "%b %e %H:%M:%S %Y %Z" "$end" +%s. Subtract $(date +%s) and divide by 86400.
What about non-HTTPS services?
s_client speaks more than HTTPS. -starttls upgrades a plaintext protocol before the handshake, which is how you check a mail submission certificate. Implicit-TLS services such as SMTPS on 465 or IMAPS on 993 need no flag at all.
# SMTP submission with STARTTLS (587)
openssl s_client -starttls smtp -crlf -connect mail.example.com:587 </dev/null 2>/dev/null \
| openssl x509 -noout -dates -subject
# Inbound MTA on port 25
openssl s_client -starttls smtp -crlf -connect mx.example.com:25 </dev/null 2>/dev/null \
| openssl x509 -noout -dates
# IMAPS — implicit TLS, no -starttls needed
openssl s_client -connect mail.example.com:993 </dev/null 2>/dev/null \
| openssl x509 -noout -datesWhich mail hosts to test comes straight from your MX records, and whether the port is reachable at all is a separate question — see Port 465 vs 587 vs 25 and the Port Checker. If you publish an MTA-STS policy, an expired certificate on an MX host will cause sending servers to refuse delivery outright rather than fall back to plaintext, so mail-server certificates deserve the same alerting as your web certificates — verify the policy side with the MTA-STS & TLS-RPT Checker.
What should I check alongside the dates?
An expiry check answers one question out of several, and the other answers are cheap to collect in the same pass. Before you close the ticket, confirm the chain builds (Verify return code: 0), the hostname is in the SAN list, and the certificate was issued by a CA your zone actually authorises. That last one bites during migrations: a CAA record added for one CA silently blocks reissuance by another, and you only find out when a renewal that used to work stops working.
It is also worth confirming that the hostname you tested is the one users reach. A site that redirects example.com to www.example.com needs valid certificates on both, and HSTS makes a certificate error unbypassable for anyone who has visited before. Trace the hops with the Redirect Checker and read the HSTS policy with the Security Headers Checker, then test each hostname in the chain of redirects rather than only the final one.
When you want the same information without a shell, or you need to confirm the view from outside your own network, run the hostname through our checker.
And if the certificate turns out to be in-date but a browser still complains, the cause is elsewhere — start with Fix ERR_CERT_DATE_INVALID, which covers client clock skew and SNI mismatches, then check whether your zone permits your CA to issue at all with the CAA Record Lookup.