All articles
·6 min read

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 code NET::ERR_CERT_DATE_INVALID. See what NET::ERR_CERT_DATE_INVALID means for the browser-side details.
  • Firefox: Warning: Potential Security Risk Ahead, error code SEC_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 the cert has expired error 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:

  1. The renewal job failed silently. A certbot renew cron 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.
  2. 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.
  3. 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.
  4. 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 certbot shows it firing, and pipe failures somewhere a human sees them.
  • Monitor the live endpoint, not the file. The only notAfter that 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.

Start free