Skip to content

Check SSL Expiry with OpenSSL

Short answer

Pipe an s_client connection into x509 to read the dates: echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates. For scripting, use -checkend <seconds>, which exits non-zero when the certificate expires inside that window.

Check it yourself
SSL Certificate Checker
Expiry, chain completeness, and certificate details

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 GMT
Not-before and not-after for a live host

If 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.

All OpenSSL certificate dates are GMT. If you compare them against local time in a script without normalising, you will be off by your UTC offset — enough to make a "expires in 1 day" alert fire on the wrong side of midnight.

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 days
Exit-code check: does this certificate survive the next 30 days?

How 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 R2
Every certificate the server presents, with subject and issuer

While 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 -serial
Read the SAN list, subject, issuer, and serial
A wildcard such as DNS:*.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 GMT
Expiry date for a list of hosts

How 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"
cert-expiry-check.sh — warn on anything expiring within 21 days

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 -dates
Mail and other TLS services

Which 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.

SSL Certificate Checker
Expiry, chain completeness, and certificate details

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.

Frequently asked questions

Why does my openssl command hang and never return?
You have not closed stdin. Without echo | at the start or </dev/null at the end, s_client keeps the session open waiting for input to send to the server. Both forms work; use whichever reads better in your script.
Why do I get the wrong certificate for a hostname?
You almost certainly omitted -servername. Without SNI, a server hosting many sites on one IP returns its default vhost certificate. Always pass -servername with the same hostname you used in -connect unless you are deliberately testing the SNI-less case.
What is the difference between -dates, -enddate, and -checkend?
-dates prints both notBefore and notAfter, -enddate prints only notAfter, and -checkend prints a sentence but more importantly sets the exit code based on whether the certificate survives the given number of seconds. Use -checkend for anything automated.
Does openssl x509 check the whole chain's expiry?
No. It parses only the first certificate it reads, which is your leaf. To check intermediates, pass -showcerts to s_client and split the output, or read the "Verify return code" line from s_client, which does evaluate the whole path including intermediate validity.
Can I check expiry for a host that is not yet live on public DNS?
Yes. Connect to the IP and pass the hostname in -servername: openssl s_client -connect 203.0.113.10:443 -servername example.com. That sends the right SNI to the right box, which is how you test a certificate on a staging origin before cutting DNS over.
How early should my alert window be?
Long enough to cover a failed automated renewal plus a human response. With 90-day certificates that renew at 30 days remaining, warning at 21 days gives you a week of failed renewals before anyone is paged, and still leaves three weeks to fix it manually.

Last updated: 2026-09-01