Back to Glossary
medium severity · Vulnerability Glossary

Expired SSL Certificate

Updated August 24, 2026

4 min read

expired ssl certificate
ssl certificate expired
check ssl certificate expiry
openssl check certificate expiration
NET::ERR_CERT_DATE_INVALID
renew ssl certificate
certbot renew automation
certificate notAfter

An expired SSL certificate is one whose notAfter date has passed, so every conforming client refuses the handshake: Chrome hard-fails with NET::ERR_CERT_DATE_INVALID, Firefox with SEC_ERROR_EXPIRED_CERTIFICATE, and API clients, webhooks and mobile apps error out. Traffic would still be encrypted, but the certificate no longer proves who is on the other end — the half of TLS that matters. Renewal is free with Let's Encrypt; the only real cost is automating it.

What is an expired SSL certificate?

Every X.509 certificate carries a validity window, notBefore and notAfter. Past notAfter, path validation fails and the client aborts. Lifetimes are shrinking by design: CA/Browser Forum ballot SC-081v3 caps publicly trusted certificates at 200 days since 15 March 2026, then 100 days in 2027 and 47 days in 2029. Let's Encrypt still defaults to 90 days. Revocation never worked reliably — Let's Encrypt switched off OCSP in August 2025 and publishes CRLs only — so the validity window is what caps the blast radius of a stolen key. Expiry maps to OWASP A04:2025 Cryptographic Failures and, as an operational lapse, A02:2025 Security Misconfiguration.

Why it's a risk / how it's exploited

Nobody exploits expiry directly — the key is no weaker on day 91 than day 90. The damage arrives two other ways.

First, availability. On an HSTS domain, especially a preloaded one, browsers drop the "proceed anyway" escape hatch entirely. Non-browser clients fail more quietly: curl, Java's HttpClient, Python requests and iOS App Transport Security all fail closed, and Stripe refuses to deliver webhooks to an endpoint whose certificate won't validate — deliveries retry, then dead-letter.

Second, trust erosion — where a real attacker gets in. The panic fix is always the same: click through, add -k to a deploy script's curl, set verify=False in the integration, untick "SSL verification" on the GitHub webhook. Those workarounds outlive the incident by years, and a client that skips verification can be MITM'd with a self-signed certificate by anyone on-path.

Rated medium, in line with how scanners score it (Tenable rates SSL Certificate Expiry 5.3). Expiry alone hands an attacker nothing — the outage is near-certain but is an availability problem, and interception needs a second condition: someone who clicks through, or a client left with verification off. Treat it as high once that workaround ships.

How to detect it

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

A healthy host prints a future notAfter and Certificate will not expire, exit 0; anything expired or inside the 14-day window set by -checkend prints Certificate will expire and exits 1 — that exit code is your monitoring hook. curl -I against an expired host prints curl: (60) SSL certificate problem: certificate has expired and exits 60.

OpenSSL 1.1.1+ fills in SNI from the -connect hostname automatically, but pass -servername explicitly when testing by IP or pinning one node behind a load balancer — otherwise you get the default vhost's certificate, the trap behind an SSL certificate name mismatch. Add -showcerts: an expired intermediate fails validation even when the leaf is valid.

How to fix it

Renew, then automate.

sudo certbot renew --dry-run
sudo certbot renew --quiet --deploy-hook "systemctl reload nginx"
systemctl list-timers | grep -i certbot   # is the renewal timer armed?

Point the server at the live paths:

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

Apache 2.4.8 and later reads the intermediate chain out of fullchain.pem; older builds need SSLCertificateChainFile. The forgotten step is the reload: a running server serves the old certificate from memory until signalled, so --deploy-hook is not optional.

The caveat that bites: renewing on the origin does nothing if a CDN or load balancer terminates TLS. Cloudflare, ACM on an ALB and Fastly each hold their own copy, and full-strict origin pull leaves two chains on two schedules — renew where the handshake happens. Automation without monitoring is half a fix: alert on notAfter from outside your network at 30, 14 and 7 days, alongside your other scheduled vulnerability checks. Wildcards need DNS-01 plus API credentials; manual DNS-01 is a reminder, not automation.

FAQ

Is traffic to an expired certificate still encrypted? Yes — key exchange and record encryption are unchanged, so a passive eavesdropper still sees ciphertext. What breaks is authentication: the client can no longer tell your server from an impostor, which makes an active MITM undetectable. Encryption without identity is not secure transport — the same reason TLS 1.0 is unacceptable despite still "working".

Can I just tell users to click Advanced → Proceed? On a plain HTTPS site they can — the worst outcome available, because you have trained users to dismiss certificate warnings, exactly the reflex a phishing MITM needs. On an HSTS site there is no bypass, and no API client has a button to click.


Certificate expiry is trivial to catch automatically and a leading cause of self-inflicted downtime. Run a free website vulnerability scan to check your chain, expiry window, protocol versions and weak cipher suites — verified findings, no false positives.