· Updated

Why Your SSL Certificate Expired Without Warning

SSL expired despite auto-renewal? Fix it now with certbot and openssl, then learn the seven failure modes that break renewal silently.

Get the site back up first

If a client is calling you about “connection not secure” warnings, don’t start debugging the renewal pipeline yet. Get a valid certificate served, then figure out why it broke.

Force a renewal with certbot

If the server runs certbot, SSH in and force it:

sudo certbot renew --force-renewal

If that succeeds but the site is still showing the old cert, the web server hasn’t reloaded its config. Reload it explicitly:

sudo systemctl reload nginx
# or
sudo systemctl reload apache2

If certbot renew fails, get a fresh certificate for the specific domain rather than troubleshooting the renewal logic under pressure:

sudo certbot certonly --nginx -d example.com -d www.example.com

Swap --nginx for --apache or --standalone depending on what’s running. --standalone requires stopping whatever is bound to port 80/443 first, so only use it if the other plugins fail.

Renewing through a hosting panel

If the site lives on shared hosting or a managed panel (cPanel, Plesk, a host’s custom dashboard), there’s usually an SSL/TLS section with a “renew” or “reissue” button, and it’s often faster than shell access. Look for:

  • cPanel: Security → SSL/TLS Status → run AutoSSL for the domain.
  • Plesk: Domains → the domain → SSL/TLS Certificates → Let’s Encrypt renew.
  • Managed platforms (Vercel, Netlify, Cloudflare Pages): these usually reissue automatically, but there’s typically a “renew certificate” or “retry” action in the domain settings if it’s stuck.

If you don’t have direct access and are working through a client’s host, this is the fastest path — it sidesteps whatever is wrong with the server’s own renewal setup.

Check what’s actually being served

Before declaring victory, verify the live certificate, not just what certbot says it installed. Browser padlock UI is often cached or DNS-split (you might be hitting a different server than clients are). Use openssl directly:

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

This prints notBefore, notAfter, and the subject the server is actually presenting. If notAfter is still in the past after you “fixed” it, you’re either hitting a cache, a CDN edge with a stale cert, or a load balancer pointing at the wrong origin — see the CDN section below before assuming the fix didn’t take.

Once notAfter shows a date months out, the immediate fire is out. Now find out why this happened, because “it auto-renews” clearly wasn’t true, and it will happen again in 60-90 days unless you know why.

The auto-renewal failure modes nobody mentions

Let’s Encrypt certificates are valid for 90 days, and the tooling is genuinely designed to renew automatically well before that. “It auto-renews” is a reasonable assumption. It’s also the assumption that makes this failure mode so common — nobody’s watching a system they believe is self-healing. Here’s what actually breaks it, roughly in order of how often we see each one.

DNS changes broke the validation challenge

Let’s Encrypt proves you control a domain one of two ways: HTTP-01 (it fetches a file from http://yourdomain/.well-known/acme-challenge/...) or DNS-01 (it checks for a specific TXT record). Both depend on DNS pointing where the renewal process expects.

If someone moved the site behind a new CDN, changed A records for a migration, or repointed a subdomain, the domain might still resolve and serve traffic fine through the new path while the HTTP-01 challenge silently fails, because the challenge request lands on infrastructure that never sees it, or resolves to an IP the certbot process doesn’t control anymore. The site looking “up” tells you nothing about whether renewal can succeed.

A CAA record got added later

CAA (Certification Authority Authorization) DNS records restrict which certificate authorities are allowed to issue certs for a domain. If someone (a security-conscious sysadmin, a compliance requirement, a different vendor’s setup script) added a CAA record that doesn’t include letsencrypt.org, every renewal attempt will fail issuance — even though nothing else about the DNS or server changed. This one is sneaky because it’s invisible unless you specifically check for it:

dig CAA example.com

If there’s a CAA record and Let’s Encrypt isn’t in it, that’s your answer.

Rate limits from a prior mess

Let’s Encrypt limits issuances per registered domain (currently 50 certificates per domain per week, with tighter limits on duplicate certificates). If a migration, a scripting bug, or a CI pipeline triggered repeated re-issuance for the same name in a short window, subsequent legitimate renewal attempts get rate-limited and fail quietly in a cron log nobody reads. This tends to compound with the DNS issue above — someone was debugging a migration, kept re-running certbot, and burned the rate limit budget in the process.

The renewal cron or systemd timer is dead

Certbot’s auto-renewal isn’t magic — it’s a cron job or a systemd timer running certbot renew on a schedule, typically twice a day. Check it exists and is actually enabled:

systemctl list-timers | grep certbot
# or
sudo crontab -l | grep certbot

Servers get rebuilt, OS upgrades sometimes drop cron entries, and systemd timers can end up disabled after a package update replaces the unit file. If the timer doesn’t exist or shows as inactive, nothing has been attempting renewal at all — this is different from a renewal attempt failing, and it’s worth distinguishing when writing up what happened for a client.

The cert renewed, but the server never reloaded

This is arguably the most common one because it fails silently in the most literal sense: certbot’s renewal succeeded, the new certificate files are sitting on disk with a valid expiry, and the web server is still serving the old one from memory because nothing told it to reload its TLS context. Certbot renewal hooks (--deploy-hook) are supposed to handle this, but if they were never configured, or a server migration lost the hook config, renewal “works” and the outage happens anyway. Confirm the deploy hook exists:

sudo cat /etc/letsencrypt/renewal/example.com.conf

Look for a renew_hook line. If it’s missing, add one:

renew_hook = systemctl reload nginx

A CDN or load balancer is serving a cached, older cert

If traffic goes through Cloudflare, a load balancer, or any TLS-terminating proxy, the certificate your origin server renews may not be the one clients actually receive. The proxy has its own certificate (often origin-issued and separately managed, or a cached copy pulled from the origin on some schedule) and that’s what needs renewing. This is why the openssl check above matters — it tells you what’s actually being served at the edge, which can lag behind what your origin thinks is current.

The site moved hosting, but the old box is still “renewing” into the void

After a migration, it’s common for the old server to keep running its renewal cron for weeks, still successfully obtaining certificates that now apply to infrastructure nobody uses. Meanwhile the new host either never had auto-renewal set up, or has its own separate (and untested) renewal path. Everyone assumes renewal is handled because something, somewhere, is renewing a certificate — just not the one in front of visitors.

Checking days-left yourself, without waiting for a browser warning

The openssl one-liner from earlier can be adapted to just print days remaining, which is worth keeping in a notes file or a script if you manage several client sites:

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

That gives you notAfter=<date>. Compare against today manually, or pipe it through date -d for exact day counts on Linux.

In a browser, click the padlock icon next to the address bar, then the certificate details, then look for the expiry/validity dates — different browsers label this slightly differently, but it’s always a couple of clicks in.

Both of these work fine as a one-off check. Neither works as a monitoring strategy, because checking manually requires remembering to check, and the entire premise of this incident was that nobody was watching. If you manage more than one or two client sites, that arithmetic doesn’t hold up for long.

The durable fix is having something check for you and flag it before the browser does it for your client. SitesRadar’s free checker reports SSL expiry alongside uptime and other health signals, so a slipping renewal shows up as a warning days out instead of as a support ticket. It’s a smaller ask than rebuilding your renewal pipeline from scratch, and it catches every failure mode above — DNS drift, dead timers, stale CDN certs — regardless of which one caused it.

For a repeatable client-site process — including alert thresholds and what to check when a warning fires — see our guide to SSL certificate expiration monitoring.

If this cert expiry was the symptom and the client is now asking “what else could be wrong,” it’s worth running through a broader outage checklist rather than treating SSL as an isolated issue. Our client site down triage checklist covers the other places a site quietly breaks before anyone notices.

← More from the SitesRadar blog