Security

How To Check SSL Certificate Expiration with OpenSSL

An expired TLS certificate is an outage with a scheduled start time. The expiry date sits inside the certificate itself, and openssl reads it in one command, whether the certificate is a file on disk, a live HTTPS endpoint, or a PKCS#12 bundle exported from a Java keystore.

Original content from computingforgeeks.com - post 25529

This guide covers every practical way to check SSL certificate expiration with OpenSSL: local .crt and .pem files, remote websites over SNI, .p12/.pfx bundles, the full certificate chain, and a -checkend script you can drop into cron so a renewal never surprises you. Every command below was run with OpenSSL 3.0.13 on Ubuntu 24.04 in August 2026, and works the same on any Linux distribution. On macOS, Apple ships LibreSSL and a BSD date without the -d flag, so the certificate reads work but the date arithmetic further down needs GNU coreutils (gdate).

Check a certificate file’s expiration date

For a PEM-encoded certificate (.crt, .pem, .cer), ask x509 for the notAfter field. One command:

openssl x509 -in /etc/ssl/certs/ISRG_Root_X1.pem -noout -enddate

The expiry date prints as a single line:

notAfter=Jun  4 11:04:38 2035 GMT

Swap the path for your own certificate, for example /etc/letsencrypt/live/yourdomain/cert.pem or a cert you issued for MySQL TLS connections. To see the full validity window, use -dates instead:

openssl x509 -in /etc/ssl/certs/ISRG_Root_X1.pem -noout -dates

Both the start and end of validity print together:

notBefore=Jun  4 11:04:38 2015 GMT
notAfter=Jun  4 11:04:38 2035 GMT

Error: “Unable to load certificate”

This error means the file you passed is not a PEM certificate. The usual cause is pointing -in at the private key or a CSR instead of the cert. This is exactly what it looks like when you feed x509 a key file:

Could not read certificate from app.key
4097524F7A710000:error:1608010C:STORE routines:ossl_store_handle_load_result:unsupported:../crypto/store/store_result.c:151:
Unable to load certificate

Run head -1 on the file. A certificate starts with -----BEGIN CERTIFICATE-----. Anything else (BEGIN PRIVATE KEY, BEGIN CERTIFICATE REQUEST) is the wrong input. If the file is DER-encoded rather than PEM, add -inform DER to the same command.

Check a live website’s certificate

For a running site you connect with s_client and pipe the presented certificate into x509. Set the domain once as a variable so the rest of the commands paste cleanly:

export SITE_DOMAIN="example.com"
echo | openssl s_client -connect "${SITE_DOMAIN}:443" -servername "${SITE_DOMAIN}" 2>/dev/null | openssl x509 -noout -dates

The validity window of the certificate the server actually served comes back:

notBefore=Jul 29 22:10:08 2026 GMT
notAfter=Oct 27 22:17:21 2026 GMT

The -servername flag matters. It sends SNI, and on any host behind a CDN or serving multiple sites from one IP, skipping it gets you the default certificate instead of the one for your domain. The leading echo closes the connection immediately so the command returns instead of hanging on an open session.

Add -subject -issuer when you also want to confirm which CA issued what the server is presenting:

echo | openssl s_client -connect "${SITE_DOMAIN}:443" -servername "${SITE_DOMAIN}" 2>/dev/null | openssl x509 -noout -subject -issuer -enddate

Subject, issuer, and expiry in one shot:

subject=CN = example.com
issuer=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
notAfter=Oct 27 22:17:21 2026 GMT

To turn the raw date into days remaining, parse it with date:

END=$(echo | openssl s_client -connect "${SITE_DOMAIN}:443" -servername "${SITE_DOMAIN}" 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
echo $(( ($(date -d "${END}" +%s) - $(date +%s)) / 86400 )) days

The countdown prints as a plain number you can alert on:

77 days

Check .p12 and .pfx files

PKCS#12 bundles hold the certificate and key together, so x509 cannot read them directly. Pipe the extracted certificate through instead. The -nokeys flag keeps the private key out of the pipe, and you will be prompted for the bundle’s import password:

openssl pkcs12 -in app.p12 -nokeys | openssl x509 -noout -subject -enddate

The first certificate in the bundle prints its subject and expiry:

subject=CN = app.example.com
notAfter=Aug 11 11:15:38 2027 GMT

The same command handles .pfx files, which are the same format under a Windows extension. If you are producing these bundles yourself at any scale, it is worth automating the issuance, and you can generate self-signed certificates with Ansible instead of minting them by hand.

Error: “unsupported … RC2-40-CBC” on older .p12 files

Bundles exported by older Java or Windows tooling use legacy encryption that the OpenSSL 3 line removed from its default provider. Reading one fails like this:

Error outputting keys and certificates
40E746D5B6790000:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:../crypto/evp/evp_fetch.c:386:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()

The fix is the -legacy flag, which loads the legacy provider for that one command:

openssl pkcs12 -legacy -in app.p12 -nokeys | openssl x509 -noout -enddate

This trips people up right after a distro upgrade, because the same file read fine under the OpenSSL 1.1 line. Nothing is wrong with the bundle; re-export it with current tooling when you get the chance. On RHEL-family servers still shipping an older toolchain, you can install a newer OpenSSL from source without touching the system copy.

Walk the full certificate chain

The leaf certificate is not the only one that can expire. Intermediates and roots have their own dates, and an expired intermediate breaks validation even while the leaf looks fine. Pull everything the server sends with -showcerts:

echo | openssl s_client -connect "${SITE_DOMAIN}:443" -servername "${SITE_DOMAIN}" -showcerts 2>/dev/null | awk '/BEGIN CERT/,/END CERT/{print}' > chain.pem
while openssl x509 -noout -subject -enddate 2>/dev/null; do :; done < chain.pem

Every certificate in the chain reports its own expiry, leaf first:

subject=CN = example.com
notAfter=Oct 27 22:17:21 2026 GMT
subject=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
notAfter=May 27 19:49:44 2035 GMT
subject=C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
notAfter=Oct 17 17:02:22 2037 GMT
subject=C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
notAfter=Dec 31 23:59:59 2028 GMT

The while loop works because stdin is a regular file here: each x509 run reads one certificate and leaves the file offset at the start of the next. It only works with the file redirect, since piping into the loop would feed everything to the first iteration. This catches the case that pure leaf monitoring misses. For internal services where you control the whole chain, locally trusted certificates with mkcert sidestep public-CA chain issues entirely.

Script the check with -checkend

Parsing dates is for humans. For scripts, x509 has a purpose-built flag: -checkend N answers whether the certificate expires within the next N seconds, in the exit code. Check whether a cert survives the next 30 days (2592000 seconds):

openssl x509 -in /etc/ssl/certs/ISRG_Root_X1.pem -noout -checkend 2592000

A healthy certificate returns exit code 0 with this message:

Certificate will not expire

A certificate inside the window prints Certificate will expire and returns exit code 1, which is what makes it scriptable. For live sites, this script combines the remote check with a day threshold. Create it:

sudo vim /usr/local/bin/check-cert-expiry.sh

Add the following contents, adjusting the domain and warning threshold:

#!/bin/bash
DOMAIN="example.com"
WARN_DAYS=21
END=$(echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2)
LEFT=$(( ($(date -d "${END}" +%s) - $(date +%s)) / 86400 ))
if [ "${LEFT}" -lt "${WARN_DAYS}" ]; then
  echo "WARNING: ${DOMAIN} certificate expires in ${LEFT} days (${END})"
else
  echo "OK: ${DOMAIN} certificate valid for ${LEFT} more days"
fi

Make it executable and run it once to confirm:

sudo chmod +x /usr/local/bin/check-cert-expiry.sh
check-cert-expiry.sh

Against a healthy certificate the output reads:

OK: example.com certificate valid for 77 more days

Schedule it daily and route the output somewhere you will actually see it:

echo "0 8 * * * root /usr/local/bin/check-cert-expiry.sh | logger -t cert-check" | sudo tee /etc/cron.d/cert-check

If your certificates come from Let’s Encrypt with certbot, renewal is already automated, and this check becomes your safety net for the day the renewal silently fails. That is the day it earns its place in cron.

Which command to reach for

Every check above is a variation on one idea: get the certificate in front of x509 -noout -enddate. The quick reference:

What you haveCommand
PEM file (.crt, .pem)openssl x509 -in cert.pem -noout -enddate
Live websiteecho | openssl s_client -connect host:443 -servername host | openssl x509 -noout -dates
PKCS#12 (.p12, .pfx)openssl pkcs12 -in cert.p12 -nokeys | openssl x509 -noout -enddate
Full served chains_client -showcerts piped into an x509 loop
Script / cron alertopenssl x509 -in cert.pem -noout -checkend 2592000

Wire the -checkend variant into whatever already pages you. A certificate check that only runs when someone remembers to run it protects nothing.

Keep reading

UFW Firewall Commands with Examples on Ubuntu 24.04 / 22.04 Security UFW Firewall Commands with Examples on Ubuntu 24.04 / 22.04 Setup WireGuard VPN on Ubuntu 24.04 / Debian 13 / Rocky Linux 10 Debian Setup WireGuard VPN on Ubuntu 24.04 / Debian 13 / Rocky Linux 10 Setup Fingerprint Reader Authentication with PAM on Linux Desktop Setup Fingerprint Reader Authentication with PAM on Linux Best CISA Books for the Certified Information Systems Auditor Exam Books Best CISA Books for the Certified Information Systems Auditor Exam Best CompTIA CySA+ Books for CS0-004 and CS0-003 Books Best CompTIA CySA+ Books for CS0-004 and CS0-003 How To Setup SSH and MySQL Bastion Server using Warpgate Databases How To Setup SSH and MySQL Bastion Server using Warpgate

2 thoughts on “How To Check SSL Certificate Expiration with OpenSSL”

Leave a Comment

Press ESC to close