How to monitor SSL certificate expiry (openssl one-liners, a cron script, and the proper fix)
Expired TLS certificates still take down real sites in 2026. Here is the full toolkit: openssl one-liners to check any cert now, a cron script that emails before expiry, the failure modes that make DIY checks lie to you, and how to get continuous monitoring for free.
The AlertKick team
Certificate expiry is the most preventable outage in the industry, and it keeps happening to teams of every size - not because renewal is hard, but because renewal automation fails silently and nothing was watching the certificate itself. The cert that matters is the one a stranger’s browser receives, and checking that is cheap. Here is every level of the toolkit, from a one-liner to done-forever.
Check a certificate right now
The openssl incantation worth memorising (or aliasing):
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate -subject -issuer
notAfter=Oct 12 09:30:00 2026 GMT
subject=CN=example.com
issuer=C=US, O=Let's Encrypt, CN=R13
Three details that save debugging later:
-servernamematters. It sets SNI. Without it, a server hosting several sites may hand you a different certificate than browsers get - the classic way a manual check says “fine” while users see an expired cert.- Check the port you actually serve. The same host’s 443, 8443, SMTP (
-starttls smtp -connect mail.example.com:587), and database ports can all carry different certificates on different renewal schedules. Mail and Postgres certs are where expiry hides, because no browser ever warns anyone. - Days-remaining as an exit code, which is the building block for scripting -
-checkendtakes seconds and returns non-zero if the cert expires within them:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -checkend $((14*86400)) \
|| echo "EXPIRES WITHIN 14 DAYS"
If you would rather not remember any of this, the free SSL certificate checker runs the full inspection - expiry countdown, chain trust, hostname match, protocol, SANs - from a browser.
The DIY monitor: a cron script
The honest minimum viable monitor - a script, a domain list, a daily cron:
#!/bin/bash
# /usr/local/bin/check-certs.sh
DOMAINS="example.com www.example.com api.example.com mail.example.com:587"
WARN_DAYS=14
for entry in $DOMAINS; do
host="${entry%%:*}"; port="${entry##*:}"; [ "$host" = "$port" ] && port=443
if ! echo | timeout 10 openssl s_client -connect "$host:$port" \
-servername "$host" 2>/dev/null \
| openssl x509 -noout -checkend $((WARN_DAYS*86400)) >/dev/null 2>&1; then
echo "WARNING: cert for $host:$port expires within $WARN_DAYS days (or check failed)"
fi
done
0 8 * * * root /usr/local/bin/check-certs.sh | mail -E -s "Certificate warnings" [email protected]
This genuinely works, and for a personal project it may be all you need. Run it daily, warn at 14-21 days - enough runway to debug a broken issuance pipeline calmly instead of during an outage.
Where the DIY monitor lies to you
Before trusting the script with anything that matters, know its failure modes - each one is a real pattern, not a hypothetical:
The monitor dies silently. The cron host gets rebuilt, mail breaks, the script errors early - and “no warnings” looks identical to “all certificates fine”. A monitor whose failure mode is silence needs a heartbeat on the monitor itself, at which point you are building a monitoring stack.
It checks from the wrong place. From inside your network, the script may reach an origin server directly and see a fresh certificate, while the load balancer, CDN edge, or reverse proxy - the thing the public connects to - still serves the old one. Renewed-but-not-deployed is the modern expiry outage, precisely because auto-renewal has made forgot-to-renew rare.
Expiry is not the only way a certificate dies. A chain that drops its intermediate after a manual copy, a renewal that issues for the apex but loses a SAN, a hostname mismatch after an infrastructure move - all produce browser errors with a perfectly valid notAfter. The one-liner checks one field; browsers check everything.
The list rots. New subdomain, new service, new region - none of them add themselves to DOMAINS. Certificate coverage that depends on a human updating a shell variable decays at the rate the team ships.
The renewal-era rule
Auto-renewal (ACME, 90-day and now shorter lifetimes) did not remove the need for expiry monitoring; it moved the target. Renewal is an automated pipeline, and pipelines break: the timer that stops firing, the webroot path that changes, the CAA record someone adds, the rate limit hit after a redeploy loop, the reload that never happens after renewal succeeds. More frequent renewal means more chances to break, with less slack when it does.
So the rule: automation renews, monitoring verifies. The check must be external (see what the public sees), continuous (catch the pipeline breaking, whenever it breaks), and cover the whole estate (every domain, including the mail server nobody thinks about).
The done-forever version
That external, continuous, whole-estate check is a solved problem, and solving it is free: an AlertKick SSL monitor checks each certificate from the outside on schedule - expiry countdown, chain validity, hostname match - and alerts on a widening schedule as expiry approaches, through the same escalation paths as everything else (email, Slack, Telegram, WhatsApp, phone, on-call rotation). Domain expiry monitoring rides along, because a domain that lapses takes every certificate under it down with it, and registrar expiry is even easier to miss.
SSL and uptime monitors are on the free plan - set one up in about two minutes, keep the openssl one-liners for spot checks, and let the cron script retire with honour.