· 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.
If you arrived here from a specific error message, these cover the exact wording and the client-side causes that look identical:
- Chrome’s red page: NET::ERR_CERT_DATE_INVALID
- A script or CI job: curl (60) SSL certificate problem: certificate has expired
- Any platform’s renewal steps, including cPanel, Plesk, Cloudflare, and IIS: Certificate has expired: how to fix it
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.
Windows and IIS: the scheduled task died with the password
On Windows servers using win-acme, Certify The Web, or a vendor’s renewal script, the renewal runs as a scheduled task under a service account. When that account’s password rotates, the task silently stops running. The certificate in IIS then expires on schedule with nothing in the event log except “task failed to start”. Check Task Scheduler for the renewal task’s last run result before assuming the ACME client is at fault.
The wildcard was replaced by a list of names, and one was missed
A *.example.com wildcard covered every subdomain. During a migration someone reissued a certificate listing example.com, www, and shop explicitly. Everything looks fine until a visitor opens portal.example.com and gets a name-mismatch warning that reads, to the client, exactly like an expiry. Every hostname in use needs to be on the certificate, and every hostname needs its own check.
An intermediate certificate expired
Your certificate’s dates are fine, but the intermediate that signed it has expired and the server is still sending the old one. Desktop browsers that cached the newer intermediate load the site; phones, scripts, and fresh installs fail. This happens every time a certificate authority rotates intermediates and the server keeps a stale bundle. With certbot, serve fullchain.pem, never cert.pem; with a purchased certificate, reinstall with the bundle the CA ships today, not the one from the last renewal.
Telling the client what happened
The client does not need the CAA record explanation. They need three sentences: what visitors saw, for how long, and what changed so it will not recur. A version that has worked for us:
Between 09:10 and 10:40 today, visitors to the site saw a browser security warning because the site’s HTTPS certificate expired. The automatic renewal had been failing since the DNS change on the 3rd; we’ve renewed the certificate, fixed the renewal, and added monitoring that alerts us 30, 14, and 7 days before any future expiry.
Put the monitoring line in even if it is the only new thing, because it answers the question the client is actually asking: “how do I know this won’t happen again?”
A 10-minute post-incident checklist
Run this before closing the ticket, in this order:
openssl s_clientagainst the public hostname showsnotAftermonths out.- Same check against every other hostname on the certificate (
www, apex, subdomains). systemctl list-timers | grep certbot(or the panel’s auto-SSL status, or the Windows scheduled task) shows renewal is scheduled and enabled./etc/letsencrypt/renewal/<domain>.confhas arenew_hookthat reloads the web server.dig CAA <domain>permits the certificate authority in use.- If a CDN or load balancer sits in front, its certificate is the renewed one and its renewal path is known.
- The site is in a monitor that checks the served certificate and alerts more than a week out.
- The cause is written down somewhere the next person will find it.
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.
If you want the answer without a terminal, the SSL certificate checker reports the certificate’s validity, chain, and days-to-expiry from a URL alone.
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 plan checks the served certificate daily and emails at 30, 14, and 7 days out, alongside uptime, broken links, and DNS changes, so a slipping renewal shows up as a warning while there is still time 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.