Your SSL certificate expired: how to diagnose, renew and verify the fix

The padlock is gone and someone has sent you a screenshot with NET::ERR_CERT_DATE_INVALID in it. An expired certificate is the most common TLS outage and the most fixable: nothing is compromised, and the repair is a renewal plus a correct install. This runbook goes from that screenshot to a fix proven from outside your own network, including the cases that catch experienced administrators — an expired intermediate under a valid leaf, one node behind the load balancer that missed the deploy, and a new certificate paired with the old key.

What the error looks like in each client

Every client compares the same two fields, notBefore and notAfter, with its own clock, but each reports the failure differently. Chrome and Edge show Your connection is not private with NET::ERR_CERT_DATE_INVALID. Firefox shows Warning: Potential Security Risk Ahead with SEC_ERROR_EXPIRED_CERTIFICATE — or, more usefully, SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE when a certificate higher up the chain lapsed. Safari says This Connection Is Not Private and its certificate sheet reads This certificate has expired. Outlook shows The server you are connected to is using a security certificate that cannot be verified; iOS Mail shows Cannot Verify Server Identity.

Command-line tools quote the specification. curl exits with curl: (60) SSL certificate problem: certificate has expired; openssl s_client prints verify error:num=10:certificate has expired and Verify return code: 10 (certificate has expired). Java throws ValidatorException: PKIX path validation failed: … validity check failed with CertificateExpiredException: NotAfter: … deeper in the stack — validation failed, not building failed, which is the missing-intermediate error. Node.js sets code: 'CERT_HAS_EXPIRED'.

The clock variant

Rule out the client first. Chrome swaps the heading for Your clock is behind or Your clock is ahead when the dates look like clock skew; Firefox says Your computer clock is set to …. The tell is scope: one device failing on every HTTPS site is a clock, many devices failing on one site is the certificate. Compare date -u with a server’s Date: header (curl -skI https://example.com | grep -i '^date:'); timedatectl set-ntp true fixes the Linux cases.

Confirm what expired: leaf, intermediate or root

A server sends a chain: the leaf, one or more intermediates and sometimes the root. Each has its own notAfter, and which one lapsed decides the fix, so look at all of them. Ask the server directly, always with -servername: without SNI many hosts answer with a default virtual host’s certificate and you diagnose the wrong one.

# the leaf only: subject and both dates
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -dates
#   notBefore=Jun  3 11:00:00 2026 GMT
#   notAfter=Sep  1 11:00:00 2026 GMT

# everything the server actually sends, saved to a file
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | awk '/BEGIN CERT/,/END CERT/' > served-chain.pem

# one subject + expiry line per certificate in that file
while openssl x509 -noout -subject -enddate 2>/dev/null; do :; done < served-chain.pem

# a certificate file you already have
openssl x509 -in cert.pem -noout -dates

To read rather than parse, drop served-chain.pem (or the .crt, .p7b or .pfx the CA sent) on the SSL certificate checker. It labels each certificate Leaf, Intermediate or Root (self-signed), prints expired 3 days ago or 27 days remaining beside its validity window, and its banner separates the two cases: Chain links correctly, but a certificate has expired versus Chain is broken — a signature or issuer link does not match. It works on files, not hostnames — a web page cannot open a raw TLS connection to another server — which is why the capture step comes first.

Why an expired intermediate breaks only some clients

When the leaf is in date but an intermediate is not, the site works in current Chrome, Firefox and Safari and fails in curl on an older distribution, and in some Java and Android versions. Modern verifiers do not take the served chain literally: they add the intermediates and roots they already hold and search for anyvalid path. Older ones — OpenSSL before 1.1.0, most embedded TLS stacks — walk the chain as served and stop at the first expired link. The best-known incident was of this kind: on 30 May 2020 Sectigo’s AddTrust External CA Root expired and servers still sending its cross-certificate for USERTrust RSA Certification Authority broke OpenSSL 1.0.2 clients while browsers never noticed. The fix is not a renewal: download the current intermediate from your CA, rebuild and redeploy. The chain builder orders whatever you paste leaf-first, verifies each signature, flags an expired intermediate in a validity warning and lists anything that does not link as left out instead of including it silently. The incomplete-chain guide covers the neighbouring failure where the intermediate is missing rather than expired.

Renew: three paths, and why the automatic one failed

ACME and Let’s Encrypt

Let’s Encrypt certificates last 90 days and certbot renews anything with 30 days or fewer left, twice a day, from a systemd timer or cron. An expired Let’s Encrypt certificate therefore means about 60 days of failed attempts nobody saw — and since 4 June 2025 Let’s Encrypt no longer sends expiry emails. Check the state, then dry-run against staging, which spends none of production’s rate limits:

certbot certificates
#   Expiry Date: 2026-09-01 11:00:00+00:00 (INVALID: EXPIRED)
certbot renew --dry-run
systemctl list-timers | grep -i certbot        # is the timer even there?

The dry run’s Detail: line names the cause. Invalid response from http://example.com/.well-known/acme-challenge/…: 404: the HTTP-01 file was not served — the webroot moved, a new vhost or proxy swallowed the request, or a redirect went elsewhere. Timeout during connect (likely firewall problem): port 80 is closed; the challenge is always fetched on port 80, and while Let’s Encrypt follows redirects to HTTPS and deliberately ignores the certificate there, it cannot get through a filtered port. Incorrect TXT record … found at _acme-challenge.example.com (DNS-01): the DNS provider API token expired or was rotated. CAA record for example.com prevents issuance: a CAA record now excludes letsencrypt.org — select CAA on the DNS lookup tool to see it; the DNS records guide explains the syntax.

Two rate limits bite during an incident: five certificates per week for the same exact set of names, renewals included — too many certificates (5) already issued for this exact set of identifiers in the last 168h0m0s — and five failed validations per hostname per hour. Fix the cause under --dry-run before spending real attempts. A certificate renewed on disk but still expired in the browser is the deploy-hook problem: with --webroot or --standalone certbot writes the file and does nothing else, so the running process keeps the old one in memory until you add --deploy-hook 'systemctl reload nginx' or a script in /etc/letsencrypt/renewal-hooks/deploy/.

Commercial CA

A commercial renewal is a new order producing a new certificate; the old one is never extended. You repeat domain control validation and choose between last year’s CSR and a fresh one. Re-using the CSR keeps the existing private key — the safe choice under pressure; a new CSR means a new key that must travel with the certificate to every server (the CSR guidecovers generating one). Either way, download the current intermediate from the order page: CAs rotate them, and a fresh leaf under a stale intermediate produces exactly the “works in Chrome, fails in Java” pattern above.

Managed platforms

When a CDN, cloud load balancer or hosting platform terminates TLS, the visitor sees the platform’s certificate and it renews itself — unless a condition it depends on changed. Cloudflare’s edge certificate renews automatically, but in Full (strict) mode it also validates your origin certificate and answers Error 526: Invalid SSL certificate when that expires; renew the origin, or issue a Cloudflare Origin CA certificate, valid for up to 15 years because only Cloudflare trusts it. cert-manager in Kubernetes renews at two-thirds of the lifetime; kubectl describe certificate says why a renewal stalled.

Install it correctly

A renewal delivers two things that must be replaced together — the leaf and its chain, the whole fullchain.pem for nginx and Apache — and one thing that must match: the private key. Before reloading anything, put the new certificate and the key you intend to use into the certificate ↔ key matcher; it derives the public key from each, compares the bytes and answers Everything matches — these belong to the same key pair or MISMATCH — at least one item belongs to a different key pair. The key is compared inside the browser tab and never sent anywhere. Skip the check and nginx refuses to start with SSL: error:05800074:x509 certificate routines::key values mismatch; Apache logs AH02565: Certificate and private key … do not match. Run nginx -t or apachectl configtest, then systemctl reload nginx or apachectl graceful — a process never reloaded serves the old certificate indefinitely.

Then list every other place a copy lives, because a certificate is installed wherever TLS is terminated, not where the CSR was made. HAProxy wants certificate, chain and key concatenated in one file. IIS keeps the old binding until you change it — netsh http show sslcert shows which hash port 443 is really bound to. Postfix (smtpd_tls_chain_files) and Dovecot (ssl_cert) each need a reload, and are why mail clients keep complaining after the website is fixed. If you copied a file out of certbot’s archive/ directory instead of pointing at the live/ symlink, you froze that version. And if you rotated the key, DANE TLSA records pinning the old key must change before the switch.

Behind a load balancer every node is a place: run the hostname through the DNS lookup tool for A and AAAArecords and test each address, as in the next section. Behind a CDN, if the browser’s certificate viewer shows the CDN’s CA as issuer with dates that are fine, the failure is the CDN refusing your origin, and the origin is what you renew. Last, HSTS: with a Strict-Transport-Security header browsers hide the proceed link entirely — Chrome says You cannot visit example.com right now because the website uses HSTS — so visitors have no workaround.

Verify from outside

Your own browser is the worst witness: it may hold an open HTTP/2 connection negotiated with the old certificate, and its padlock was checked when the connection opened, not per request. Close the tab — or in Chrome open chrome://net-internals/#sockets and click Flush socket pools — and test from a phone on mobile data, which also bypasses split-horizon DNS and any corporate proxy. Then let tools do the checking:

# curl prints the dates it saw; look for "expire date"
curl -vI https://example.com/ 2>&1 | grep -E 'expire date|SSL certificate problem'
#   *  expire date: Nov 30 11:00:00 2026 GMT

# one node at a time, using the addresses from the DNS lookup
curl -vI --resolve example.com:443:203.0.113.9 https://example.com/ 2>&1 | grep 'expire date'
curl -vI --resolve 'example.com:443:[2001:db8::9]' https://example.com/ 2>&1 | grep 'expire date'

# mail and other ports are separate installs
openssl s_client -connect mail.example.com:993 -servername mail.example.com </dev/null 2>/dev/null | openssl x509 -noout -enddate
openssl s_client -starttls smtp -connect mail.example.com:587 -servername mail.example.com </dev/null 2>/dev/null | openssl x509 -noout -enddate

Every node should print the same expire date; the one printing the old date missed the deploy. Capture the served chain again with -showcerts and run it through the checker so the intermediate is verified too — Chain of 2 certificates links and verifies correctly is the sentence you want.

Symptom → likely cause → fix

SymptomLikely causeFix
One device fails on every HTTPS site; Chrome says Your clock is behindDevice clock is wrongEnable NTP; compare date -u with a server’s Date: header
Everyone fails; the checker shows the leaf expired N days agoLeaf expired — auto-renewal broke or a manual renewal was missedRenew; install certificate and chain with the matching key; reload
Works in current browsers, fails in curl, Java, old Android or monitoringExpired intermediate or cross-sign in the served chainDownload the current intermediate; rebuild fullchain.pem in the chain builder; redeploy
Renewed on disk, browsers still show expiredService never reloaded; config points at an archive/ copy; no deploy hookReload; point config at live/; add --deploy-hook
Fixed for some visitors, not othersOne node behind the load balancer, or the IPv6 address, still serves the old certificateTest each A and AAAA record with curl --resolve; deploy to the stragglers
Cloudflare page Error 526: Invalid SSL certificateOrigin certificate expired under Full (strict)Renew the origin certificate, or issue a Cloudflare Origin CA certificate
nginx will not start: key values mismatchNew certificate with the old key, or the reverseRun the pair through the matcher; install the key the CSR was made with

Prevent the next one

Certificates expire on a schedule readable months in advance, so an expiry outage is a monitoring failure first. The schedule is tightening: publicly trusted certificates have been capped at 398 days since September 2020, and the CA/Browser Forum’s ballot SC-081, passed in April 2025, cut the cap to 200 days from 15 March 2026 and schedules 100 days from March 2027 and 47 days from March 2029.

Where ACME is possible, add a deploy hook and prove it with certbot renew --dry-run on the real server. For the rest, put the date somewhere that interrupts someone: the certificate checker’s Renewal reminder (.ics) button creates an all-day calendar event 30 days before the leaf expires, and openssl x509 -in cert.pem -noout -checkend 2592000 exits 1 when fewer than 30 days remain, which makes a cron job a one-liner. Monitor endpoints, not files — in most of the incidents above the file on disk had been renewed — and keep the list from the install step as an inventory: every place TLS is terminated, its expiry and its renewal method.

Do this

  • Rule out the clock first: one device failing everywhere is a clock; many devices failing on one site is the certificate.
  • Capture the served chain with openssl s_client -showcerts (always with -servername) and check every certificate’s dates in the checker, not just the leaf’s.
  • Renew by the path the certificate came from — certbot renew --dry-run says why automation failed; a commercial CA needs a new order and its current intermediate.
  • Confirm certificate and key belong together in the matcher, then reload every node, the mail server, the CDN origin and every other copy.
  • Prove the fix from mobile data and per IP with curl --resolve, then add a deploy hook or a -checkend cron so it does not recur.

Frequently asked questions

Can I just click through an expired certificate warning?

For a site you run and are about to fix, briefly — Chrome hides the link under Advanced and Firefox under Accept the Risk. If the site sends an HSTS header the browsers remove that option entirely, and no visitor should be asked to do it: the warning means the connection cannot be authenticated, so fix the certificate instead.

Does an expired SSL certificate mean my site was hacked?

No. Expiry is a date check, not a security event: the private key is intact and nothing was intercepted. The damage is the outage itself, plus the habit it teaches users of clicking past warnings.

How long does a renewed certificate take to start working?

Instantly, once the process that terminates TLS has reloaded the new file. Anything that still looks expired is a client holding an open connection, a second server or CDN edge that was not updated, or a device with a wrong clock — there is no propagation delay for certificates.

Why is my site expired on some devices but fine on others?

Usually an expired intermediate or cross-sign in the served chain. Modern browsers build their own path to a trusted root and ignore the expired certificate; OpenSSL 1.0.x, older embedded clients and some Java and Android versions use the chain exactly as sent and fail. Fetch the current intermediate from your CA and rebuild fullchain.pem.

Do I need a new private key when I renew?

No — you can reuse the CSR at a commercial CA, or pass --reuse-key to certbot. Rotating the key is good practice, but the new key then has to be installed with the new certificate everywhere, and any DANE TLSA records that pin the old key must be updated first.

Tools used in this guide

Every one of these runs in your browser — the files you work on never leave your device.

More certificates & keys guides