How to Monitor SSL Certificate Expiry Automatically
Learn to monitor SSL certificate expiry automatically with openssl, cron scripts, and alerting to avoid outages from expired certs.

Expired SSL certificates still cause preventable outages every single day. As certificate lifetimes shrink and automation becomes the norm, manual tracking in a spreadsheet is no longer viable. This article walks through practical ways to check certificate expiry from the command line, automate the checks with cron, and understand why the industry's move toward short-lived certificates makes monitoring more critical than ever.
Checking Certificate Expiry with openssl
The fastest way to inspect a live certificate is openssl s_client piped into x509:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates
This returns notBefore and notAfter timestamps. To get just the days remaining:
end_date=$(echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
end_epoch=$(date -d "$end_date" +%s)
now_epoch=$(date +%s)
echo $(( (end_epoch - now_epoch) / 86400 )) days remaining
Run this against every hostname that terminates TLS, including internal load balancers, API gateways, and third-party integrations you depend on.
A Cron Script for Daily Checks
A minimal cron-driven checker looks like this:
#!/usr/bin/env bash
# check-ssl.sh
DOMAINS=("example.com" "api.example.com" "shop.example.com")
THRESHOLD_DAYS=14
for domain in "${DOMAINS[@]}"; do
end_date=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$end_date" ]; then
echo "ALERT: could not retrieve certificate for $domain"
continue
fi
days_left=$(( ($(date -d "$end_date" +%s) - $(date +%s)) / 86400 ))
if [ "$days_left" -lt "$THRESHOLD_DAYS" ]; then
echo "ALERT: $domain expires in $days_left days"
fi
done
Add it to crontab to run daily:
0 6 * * * /usr/local/bin/check-ssl.sh | mail -s "SSL Report" ops@example.com
This works, but it has real limitations: no historical trend, no escalation policy, no on-call routing, and it silently fails if the cron daemon itself stops running. That's the gap dedicated SSL monitoring tools are built to close - they track expiry continuously, alert through multiple channels, and catch renewal failures before customers do.
Certificate Chain Issues
A certificate can be valid and still break clients if the chain is incomplete. Servers must send the leaf certificate plus any intermediate certificates, in the correct order. Check the full chain with:
openssl s_client -servername example.com -connect example.com:443 -showcerts
Count the certificates returned. If only one appears (the leaf), most modern browsers that cache intermediates from prior visits may still succeed, but mobile clients, IoT devices, curl, and API consumers without a cached intermediate will fail with a chain validation error.
Common causes of chain issues:
- Web server config references only the leaf cert file, not the "fullchain" bundle.
- An intermediate certificate was rotated by the CA and the old one is now expired or revoked.
- A load balancer terminates TLS with a stale certificate bundle pushed before the last renewal.
The 47-Day Certificate Trend
The CA/Browser Forum has approved a phased reduction of maximum TLS certificate validity, moving from 398 days down to 200, then 100, and eventually 47 days by 2029. This is a direct response to key compromise risk and the push toward automated issuance.
What this means operationally:
| Certificate lifetime | Renewals per year | Manual tracking feasibility |
|---|---|---|
| 398 days | ~1 | Feasible with a calendar reminder |
| 200 days | ~2 | Tedious but possible |
| 100 days | ~3.65 | Requires automation |
| 47 days | ~7.8 | Automation mandatory |
At 47-day lifetimes, any team still relying on manual renewal reminders will inevitably miss one. Automated issuance (ACME) combined with independent expiry monitoring becomes the only sustainable model.
ACME Renewal Failure Modes
ACME clients like certbot, acme.sh, or Caddy's built-in issuer automate renewal, but automation introduces its own failure modes that monitoring must catch:
- Rate limiting: Let's Encrypt limits certificates per registered domain per week; repeated failed attempts can exhaust the quota.
- DNS-01 challenge failures: API credentials for the DNS provider expire or get rotated, silently breaking wildcard renewals.
- HTTP-01 challenge blocked: A WAF rule or redirect-to-HTTPS policy intercepts the
/.well-known/acme-challenge/path. - Renewal hook errors: The post-renewal hook that reloads nginx or restarts a load balancer fails silently, so the new cert is issued but never deployed.
- Clock drift: NTP desync on the server causes ACME validation timestamp checks to fail.
A renewal checklist worth running after every deploy or automation change:
- Confirm certbot/acme.sh cron or systemd timer is active (
systemctl list-timers) - Verify the renewal hook actually reloads the TLS-terminating service
- Check that DNS API credentials used for DNS-01 haven't expired
- Confirm rate limit headroom via the CA's staging environment first
- Independently verify the live certificate's
notAfterdate after renewal, don't just trust exit code 0
Independent verification is the key point. An ACME client reporting success doesn't guarantee the certificate was deployed correctly to every edge node, CDN, or load balancer in your fleet. Tools like ServerInspector poll the actual TLS endpoint from the outside, the same way a browser or API client would, so a mismatch between "renewal ran" and "certificate is actually live" gets caught immediately.
Building an Alerting Strategy
A mature SSL expiry monitoring setup should include:
- Checks against every public-facing hostname, not just the primary domain
- Checks against internal services if they use internally-trusted CAs
- Multiple alert thresholds (30 days, 14 days, 3 days) with escalating urgency
- Alerting via a channel your team actually monitors - Slack, PagerDuty, SMS, not just email
- A public status page so customers see transparency during incidents rather than silence
FAQ
How often should I check SSL certificate expiry?
Daily checks are the practical minimum. With certificate lifetimes shrinking toward 47 days, daily or even hourly checks give you enough lead time to fix ACME failures before they become outages.
Can I monitor certificates I don't control, like third-party APIs I depend on?
Yes. Because expiry checks are done from the outside against the public TLS handshake, you can monitor any hostname you can reach over port 443, regardless of who manages the origin server.
What's the difference between certificate expiry and chain validation errors?
Expiry is a simple date comparison against notAfter. Chain validation errors happen when the server fails to present a complete, correctly ordered certificate chain, even if the leaf certificate itself is still valid.
Does a successful ACME renewal guarantee the new certificate is live?
No. The renewal process can succeed while the reload/deploy hook fails, leaving the old certificate in production. Always independently verify the live endpoint after renewal.
What happens if I ignore certificate expiry until it's too late?
Browsers block the connection outright with interstitial warnings, API clients throw TLS handshake errors, and mobile apps using certificate pinning can fail even harder. There's no graceful degradation with SSL failures - it's a hard stop for users.
Monitoring Beyond a Single Endpoint
Large deployments often terminate TLS at many different layers: CDN edge nodes, regional load balancers, and origin servers. A certificate that renewed correctly on the origin can still be stale at a CDN edge if the CDN caches its own certificate bundle separately. Check each layer independently:
for edge in edge1.example.com edge2.example.com origin.example.com; do
echo "=== $edge ==="
echo | openssl s_client -servername example.com -connect "$edge:443" 2>/dev/null | openssl x509 -noout -enddate
done
Treat each TLS-terminating node as its own monitored entity rather than assuming a single check for the public hostname covers the whole fleet.