Your SSL certificate expired: what it means and how to fix it
An expired SSL certificate is one of the most common — and most avoidable — outages in production. The moment your certificate's validity window closes, browsers and API clients stop trusting it, and your traffic drops to angry interstitial pages. The good news: an expired SSL certificate is fast to diagnose and fix once you know exactly what "expired" means and which command to run.
What "expired" actually means
Every TLS certificate carries two timestamps in its body: notBefore and notAfter. The certificate is only valid for the window between them. When the current time passes notAfter, the certificate is expired — not revoked, not broken, just past its sell-by date.
The certificate file on disk does not change. It's the client that does the math: during the TLS handshake, the browser or library compares its own clock against notAfter. If the current time is later, the client refuses to complete the handshake and aborts the connection. There is no grace period and no "it's only a few hours late" leniency. This is by design — accepting an expired certificate would defeat the purpose of certificate-based trust.
Most certificates today are short-lived. Let's Encrypt issues 90-day certificates and recommends renewing at 60 days, which is why the renewal step is almost always automated and why a silently broken automation is the usual root cause.
What visitors see
The exact wording depends on the client, but they all fail closed:
- Chrome / Edge: A full-page warning,
Your connection is not private, with the error codeNET::ERR_CERT_DATE_INVALID. See what NET::ERR_CERT_DATE_INVALID means for the browser-side details. - Firefox:
Warning: Potential Security Risk Ahead, error codeSEC_ERROR_EXPIRED_CERTIFICATE. - Safari:
This Connection Is Not Private. - curl:
curl: (60) SSL certificate problem: certificate has expired. - OpenSSL / many language runtimes: the underlying verification error is
certificate has expired. Our breakdown of thecert has expirederror covers how this surfaces in libraries and CLIs.
Importantly, these are hard failures. Users can sometimes click through a browser warning, but automated clients — mobile apps, webhooks, payment callbacks, server-to-server API calls — will not. They simply error out, which is how an expired certificate quietly breaks integrations long before anyone reports the website looking wrong.
Why it happens
A handful of causes account for nearly every expired SSL certificate:
- The renewal job failed silently. A
certbot renewcron or systemd timer that has been failing for weeks — DNS challenge broke, a plugin changed, the disk filled up — but nobody was watching the logs. - The certificate was renewed but never deployed. A new certificate was issued and sits on disk (or in your CA dashboard), but the web server, load balancer, or CDN is still serving the old one because it was never reloaded or uploaded.
- The wrong clock. A client (or, rarely, a server) with a badly skewed system clock will reject a perfectly valid certificate, or accept an expired one. This is why an "expired" error on one machine but not another usually points at time, not the cert.
- A second server you forgot about. The cert renewed on the primary but a canary, a staging box, or one node behind the load balancer still has the old file.
Step-by-step fix
1. Confirm the dates
Before touching anything, verify what the server is actually serving. Don't trust the dashboard — check the live endpoint:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -subject
You'll get something like:
notBefore=Mar 3 00:00:00 2026 GMT
notAfter=Jun 1 00:00:00 2026 GMT
subject=CN=example.com
Compare notAfter against date -u. If notAfter is in the past, the live certificate is genuinely expired. If it looks current, the problem is a stale node or a client-side clock — fix that instead of reissuing. You can also run a quick browser-free check with our SSL checker tool to see the full chain and expiry at a glance.
2. Renew the certificate
For Let's Encrypt via certbot, renew all installed certificates:
sudo certbot renew
certbot renew only acts on certificates within the renewal window (typically 30 days from expiry), which an expired cert always satisfies. To force a renewal of a single certificate regardless of timing:
sudo certbot renew --cert-name example.com --force-renewal
If certbot reports a challenge failure, fix that first — an expired cert is almost always a renewal that has been failing. Test non-destructively with a dry run before relying on the timer again:
sudo certbot renew --dry-run
For a commercial CA, you'll download the newly issued certificate and intermediate chain and place them where your server expects them.
3. Redeploy and reload
Issuing a new certificate does nothing until the running server picks it up. With the certbot nginx plugin this happens automatically, but if you manage config by hand, reload after the files are in place:
sudo nginx -t && sudo systemctl reload nginx
nginx -t validates the config first so a typo doesn't take the server down on restart; reload swaps in the new certificate with zero dropped connections. For the full nginx setup, including paths and ssl_certificate directives, see the nginx TLS guide.
On Kubernetes with cert-manager, force a fresh issuance and let the ingress reload:
kubectl get certificate -A
kubectl delete certificaterequest <name> -n <namespace> # triggers reissue
If you terminate TLS at a CDN or managed load balancer (CloudFront, ALB, Cloudflare), the renewal often lives there, not on your origin — update the certificate in that console, not on the box.
4. Verify
Re-run the same openssl command from step 1 and confirm notAfter is now in the future. Then confirm a real client is happy:
curl -sSI https://example.com >/dev/null && echo "TLS OK"
A clean exit with no (60) error means the handshake succeeded against a valid certificate. Check from outside your network too — a reload that didn't propagate to every node will pass locally and still fail for users.
Never let it expire again
Fixing the expired SSL certificate is the easy part. The real work is making sure it never recurs:
- Don't trust silent automation. A renewal timer with no alerting is a future outage. Confirm
systemctl list-timers | grep certbotshows it firing, and pipe failures somewhere a human sees them. - Monitor the live endpoint, not the file. The only
notAfterthat matters is the one your server is actually serving on port 443 — that's what catches the "renewed but never deployed" class of failure. - Watch every host and SAN. Multi-node and multi-domain setups expire one node at a time. Track each endpoint independently.
Monitor it automatically
SSLNudge checks your certificates every day against the live endpoint and emails you well before notAfter — typically at 30, 14, and 3 days out — so a failed renewal becomes a calm heads-up instead of a 2 a.m. NET::ERR_CERT_DATE_INVALID page. Add your domains once and let it watch the expiry dates for you. Sign in to get started.
Stop tracking expiry dates by hand
SSLNudge checks your certificates daily and alerts you before they expire.