Blog

SSL certificate expiry monitoring: why certificates still expire with auto-renewal and how to catch it

Auto-renewal does not stop certificates from expiring. Here is why renewals fail, which warning thresholds make sense and what to check beyond the expiry date.

SSL certificate expiry monitoring means checking the certificate that each hostname and port actually serves, from outside your infrastructure, and alerting with enough lead time to fix a failed renewal. For auto-renewed 90-day certificates, a sensible setup warns at 21 days, escalates at 7 and pages someone at 3. Monitoring only the date is not enough, though. You also need to verify the chain, the hostname match and that every server behind a name serves the new certificate.

This guide covers why certificates still expire on systems with auto-renewal, how to pick warning thresholds that are neither noisy nor too late, and the concrete checks and commands that catch problems before browsers and API clients do.

Why do certificates still expire with auto-renewal?

Auto-renewal moves the failure point. Instead of a human forgetting a date, a process fails quietly and nobody looks at its output. These are the patterns that cause most expired certificates on setups that were supposed to be automatic.

The renewal runs but fails

ACME clients like certbot retry on a schedule, typically twice a day via a systemd timer or cron. Each attempt can fail for reasons that have nothing to do with the client itself:

  • HTTP-01 challenges fail because port 80 was closed in a firewall cleanup, a redirect rule now sends /.well-known/acme-challenge/ somewhere else, or the domain moved behind a CDN or load balancer that does not forward the challenge path.
  • DNS-01 challenges fail because the API token for the DNS provider expired, was rotated or lost permissions.
  • A CAA record was added that does not list the issuing CA.
  • The domain hit the CA's rate limits after a misconfigured script requested certificates in a loop.

The renewal log fills with errors, but nobody reads it. Let's Encrypt has also discontinued its expiry reminder emails, so the old safety net of a warning in someone's inbox is gone.

The renewal succeeds but nothing reloads

This one is common and confusing. Certbot writes a fresh certificate to disk, certbot certificates shows 89 days remaining, and the site still serves the old one. Nginx, HAProxy, Postfix and most other daemons read certificates at startup and keep them in memory until reloaded. Without a deploy hook, the new file sits unused.

# /etc/letsencrypt/renewal-hooks/deploy/reload.sh
#!/bin/sh
systemctl reload nginx
systemctl reload postfix

The same applies to containers that copy certificates in at build time and to Java keystores that need a conversion step after renewal.

Forgotten hostnames and ports

Auto-renewal covers the certificates you configured. It does nothing for the ones you forgot exist: a staging subdomain set up by a former colleague, a status or docs subdomain on a different host, the mail server on ports 465 and 993, an internal API exposed on 8443, or a certificate uploaded manually to a load balancer or CDN years ago. These rarely appear in the main deployment config, so they rarely appear in monitoring either.

Broken intermediate chains

A server must send its leaf certificate plus the intermediate certificates up to a trusted root. Configuring cert.pem instead of fullchain.pem produces a chain with the leaf only. Desktop browsers often hide the problem because they cache intermediates or fetch them on their own. curl, mobile apps, webhooks from third parties and server-side HTTP libraries do not, so they fail with verification errors while the site looks fine in your browser. Chain issues also appear when a CA rotates its intermediates and a server still sends an old bundle.

How many days of warning are sensible for SSL certificate expiry monitoring?

The right threshold depends on how the certificate is renewed. An alert should mean that the normal process has already failed, not that it is about to run.

For auto-renewed 90-day certificates (certbot renews when 30 days remain by default):

  • 21 days: warning to a team channel. Renewal should have happened around day 30, so roughly 18 attempts have failed. Investigate during working hours.
  • 7 days: escalate to a ticket or the on-call person's notification channel.
  • 3 days: page. At this point a weekend or a holiday can turn this into an outage.

Do not alert at 30 days for auto-renewed certificates. That fires on every renewal cycle that runs a few hours late and trains people to ignore the alert.

For manually renewed certificates with longer lifetimes, the bottleneck is the human process: purchase approvals, domain validation emails, someone with access to the load balancer. Use 45, 30, 14 and 7 days, and make the first alert create a ticket with an owner.

Certificate lifetimes are also getting shorter: CA/Browser Forum rules will reduce maximum validity in steps toward 47 days by 2029. Shorter lifetimes leave less slack, which makes external checks on what is served more valuable, not less.

What to check besides the expiry date

An expiry check that only reads notAfter misses most of the failures above. Monitor the certificate that clients actually receive, not the file on disk or the renewal log. A useful check verifies:

  • Hostname match: the requested name appears in the Subject Alternative Names. A renewal that dropped a SAN, or a default certificate served because SNI did not match, shows up here.
  • Chain completeness: the chain verifies against a standard trust store without fetching missing intermediates.
  • Every backend: if a name resolves to several IPs, or both IPv4 and IPv6, each address can serve a different certificate. One node that missed a reload is enough to cause intermittent errors.
  • Unexpected issuer changes: a certificate suddenly issued by a different CA can indicate a misconfiguration or a CDN default certificate taking over.
  • Protocol and key basics: TLS 1.2 or newer offered, no expired or weak intermediates in the bundle.

How to check a certificate from the command line

OpenSSL covers almost everything. Always pass -servername so SNI is sent, otherwise you may see the server's default certificate instead of the one for your hostname.

Expiry date, subject and issuer:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate -subject -issuer

Exit with a non-zero code if the certificate expires within 21 days (1814400 seconds):

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 1814400

Subject Alternative Names (OpenSSL 1.1.1 or newer):

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName

Chain verification: look for Verify return code: 0 (ok). A code like 21 (unable to verify the first certificate) means the intermediate is missing.

echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null \
  | grep -E 'Verify return code|^ [0-9] s:'

Check a specific backend IP while still sending the right hostname:

echo | openssl s_client -connect 203.0.113.10:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate

Mail and other STARTTLS services:

echo | openssl s_client -connect mail.example.com:587 -starttls smtp 2>/dev/null \
  | openssl x509 -noout -enddate

A minimal loop for a list of targets, suitable as a stopgap or a sanity check:

#!/usr/bin/env bash
WARN_DAYS=21
for target in example.com:443 api.example.com:443 mail.example.com:465; do
  host=${target%:*}
  if ! echo | openssl s_client -connect "$target" -servername "$host" 2>/dev/null \
     | openssl x509 -noout -checkend $((WARN_DAYS*86400)) >/dev/null; then
    echo "WARN: $target expires within $WARN_DAYS days or returned no certificate"
  fi
done

On the renewal side, certbot renew --dry-run tests the full challenge flow against the staging environment without issuing a real certificate. Run it after any change to firewalls, redirects or DNS.

How to set up SSL certificate expiry monitoring step by step

  1. Build an inventory from Certificate Transparency logs. Every publicly trusted certificate is logged. Searching %.example.com on a CT search service such as crt.sh lists every hostname a certificate was issued for, including the ones nobody remembers. Compare that list with what you monitor.
  2. Add an external check per hostname and port. Include mail ports, non-standard API ports and every subdomain from step 1. Run checks from outside your network, so they see what customers see.
  3. Set tiered thresholds. 21, 7 and 3 days for auto-renewed certificates, 45, 30, 14 and 7 for manual ones.
  4. Route each tier differently. Early warnings go to a team channel, late ones to whoever is on call. An alert that reaches nobody at 3 days is the same as no alert.
  5. Monitor the renewal job itself. Have the renewal script ping a heartbeat URL after a successful run, so a timer that stopped firing is noticed within a day rather than at 21 days. The approach is described in detail in heartbeat monitoring for cron jobs.
  6. Add deploy hooks and test them. Force a renewal on a staging host with certbot renew --force-renewal and confirm the served certificate changes.
  7. Re-check after infrastructure changes. New load balancers, CDN migrations and DNS moves are the moments when challenges and chains break.

Expiry checks complement, but do not replace, regular HTTP uptime checks. A certificate can be perfectly valid on a server that returns errors. If you want the terminology around uptime, SLOs and synthetic checks in one place, the monitoring glossary has short definitions.

Doing this with m0nitor

If you would rather not maintain scripts, m0nitor offers SSL certificate expiry as one of its monitor types, alongside HTTP(S), TCP, DNS and heartbeat monitors. Checks run from distributed probes in several regions, and an outage is confirmed by multiple locations before an alert fires. Alerts can be sent to channels such as email, Slack, Microsoft Teams, Telegram, SMS or PagerDuty. Pairing an SSL monitor with a heartbeat monitor on the renewal job covers both sides: the job that should renew and the certificate that should be served. Any other tool or a well-maintained script that runs the checks above does the job too.

FAQ

How often should SSL certificate expiry be checked?

For the date alone, once a day is enough. Because a check also catches chain errors and missed reloads after deployments, running it every few hours or alongside your regular uptime checks shortens the time a broken chain goes unnoticed.

Does Let's Encrypt still send certificate expiry emails?

No. Let's Encrypt has discontinued its expiry notification emails, so you need your own monitoring to learn about failed renewals before the certificate expires.

Why does the site work in my browser but curl reports a certificate error?

Almost always a missing intermediate certificate. Browsers often cache or fetch intermediates themselves, while curl and most HTTP libraries do not. Serve the full chain, for example fullchain.pem instead of cert.pem, and confirm with openssl s_client that the verify return code is 0.

Can I monitor certificates on ports other than 443?

Yes. Any TLS service can be checked with openssl s_client -connect host:port, and services that upgrade with STARTTLS, such as SMTP on port 587, work with the -starttls option. Mail servers and internal APIs on custom ports are among the most commonly forgotten certificates, so include them explicitly.

Produkt

  • Preise
  • Funktionen
  • Integrationen
  • Sicherheit
  • Kostenloser Netzwerk-Check

Funktionen

  • Uptime-Monitoring
  • Statusseiten
  • Alarm-Workflows
  • Störungsmanagement
  • Abhängigkeiten
  • KI-Assistent
  • Globale Infrastruktur
  • Bereitschaftspläne
  • Enterprise-SSO

Ressourcen

  • Was ist eine Statusseite?
  • Dokumentation
  • API
  • MCP-Server
  • Monitoring-Glossar
  • Blog
  • Changelog
  • Über uns
  • Kontakt

Vergleich

  • Kostenloses Uptime-Monitoring
  • UptimeRobot
  • Better Stack
  • Pingdom
  • StatusCake
  • Uptime Kuma
  • Hyperping
  • Atlassian Statuspage
  • Instatus
  • Cachet