dotvitals

SSL certificate expired: how to check and renew

Updated ·16 min read

A TLS certificate carries two dates, notBefore and notAfter, and RFC 5280 section 6.1.3 makes checking them a mandatory part of validating any certificate chain. Once the current time is past notAfter, every client that validates certificates at all refuses the connection. There is no grace period, no warning phase, and nothing about the rest of the certificate that can compensate: the date alone is fatal.

This is worth stating bluntly because expiry is usually discussed as a deadline approaching, and by the time most people look it has already passed. An expired certificate is not a risk to manage — the site is down right now, for every visitor and every integration, and our scanner grades it as critical rather than as an approaching-deadline warning for exactly that reason.

This guide covers what expiry does to different kinds of client, how to check it properly including the parts of the chain that are easy to miss, how to renew with ACME and with a manual certificate authority, the certificate lifetime schedule that is making all of this happen more often, how to undo a bad certificate deployment, and the several failures that look like expiry and are not.

Check yours now

What an expired certificate actually does

For someone in a browser, the site is replaced by a full-page interstitial: Chrome shows NET::ERR_CERT_DATE_INVALID, Firefox shows SEC_ERROR_EXPIRED_CERTIFICATE, Safari shows its own equivalent. The page they wanted is not rendered at all. The warning is bypassable — there is an Advanced link and a proceed button — but it is deliberately several clicks deep and worded as a security threat, and the overwhelming majority of visitors leave instead.

For anything that is not a person in a browser, it is worse, because there is no interstitial to click through. A mobile app, a payment webhook, a monitoring agent, a cron job running curl, a server-to-server API call: all of them fail the TLS handshake outright and return an error. Nobody sees a warning; a queue just stops draining.

There are two important cases where the warning is not bypassable at all. If the site sent an HSTS header that the browser is still remembering, the browser refuses to offer a proceed option — that is precisely what HSTS is for, and it means an expired certificate takes the site completely offline for returning visitors rather than merely discouraging them. The same applies to any hostname on the HSTS preload list. And a certificate pinned by a mobile app fails with no user-facing choice either.

So the impact is not uniform. A brochure site with no HSTS loses most of its traffic; a site with HSTS and an API loses all of it, plus every integration, with no way for anyone to work around it from their end.

How to check expiry, properly

The one-line check is openssl s_client piped into openssl x509. The -servername flag is not optional: it sends SNI, and without it a host serving several sites returns whichever certificate is on the default virtual host rather than yours, which is a good way to diagnose the wrong certificate entirely.

In a browser, click the padlock, then the certificate details. This tells you the truth about what that browser sees, which is useful, but it has a specific blind spot that matters here: browsers cache intermediate certificates and fetch missing ones on their own, so a chain problem alongside the expiry may be invisible to you and very visible to everyone else. Check from a client with no cache before concluding the chain is fine.

Two things are worth checking at the same time as the date. Confirm the certificate actually covers the hostname visitors type, by reading the subjectAltName list — browsers have not consulted the Common Name since 2017, so a certificate whose CN looks right but whose SAN does not is a mismatch. And confirm the server's own clock is correct, because a host whose clock is wrong will produce a certificate that looks expired or not-yet-valid to everybody else.

The checks worth running before you change anything
# the dates on the certificate actually being served
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

# does the whole chain validate? expect "Verify return code: 0 (ok)"
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>&1 \
  | grep 'Verify return code'

# which hostnames does it actually cover
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName

# is this host's own clock right? a wrong clock breaks issuance as well as validation
timedatectl status

The chain, not just the leaf

Your certificate is one link. Between it and a root your visitors trust sit one or more intermediate certificates, and each of those has its own validity dates. An intermediate that expires takes your site down just as completely as your own certificate expiring, and it does so without anything about your certificate changing — which makes it genuinely confusing when it happens.

Order matters too. The server is expected to send the leaf first, then each issuer in turn, and not to send the root at all: the root is what the client already has, so sending it is wasted bytes on every handshake. TLS 1.3 explicitly allows clients to accept an unordered chain and most do, so a misordered chain usually works — right up until it meets an embedded device or a strict TLS library that follows the specification literally, and then it fails in a way nobody can reproduce. Our scanner grades a misordered chain as a low-severity warning and a genuinely missing intermediate as a failure, which is the right proportion.

A missing intermediate is the most confusing TLS fault there is, because it works in the browser you tested with. Browsers fetch the missing certificate themselves using the AIA extension, or reuse one they already have cached from another site. Mobile apps, curl, Java clients and payment webhooks generally do not, so the symptom is that the site works fine for you and fails for a fraction of clients apparently at random. The fix is to point the server at the CA's fullchain file rather than the leaf-only cert file.

There is also the case where the root itself is the problem. Root certificates are retired and replaced on the CA's schedule, and browsers and operating systems distrust roots from time to time. When a root expires or is distrusted, every certificate under it stops validating on clients whose trust store does not have a replacement path — which is why old Android devices and unpatched systems sometimes break while everything current is fine. Cross-signing exists to soften exactly this: a CA arranges for its intermediate to be signed by a second, older root as well, so clients with either root in their store can still build a path. If you are being told the site fails only on old devices, a cross-signed chain from your CA is usually what you are looking for.

Read the chain the server is actually sending
# every certificate in the chain, with subject and issuer for each
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
  | openssl pkcs7 -print_certs -noout

# how many certificates were sent: 1 means the intermediates are missing
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | grep -c 'BEGIN CERTIFICATE'

# the expiry date of a specific intermediate you have on disk
openssl x509 -in /etc/ssl/certs/intermediate.pem -noout -dates -subject

Renewing with ACME

If the certificate came from Let's Encrypt or another ACME certificate authority, renewal is one command, and the important work is finding out why it did not happen on its own. Run the renewal first to get the site back, then investigate — in that order, because the site is down.

After the certificate is reissued, the server must be reloaded. A configuration test is not a reload: nginx and Apache both hold the old certificate in memory until they re-read it, so a successful renewal with no reload leaves the expired certificate being served and everyone reasonably confused.

Then find the cause. In order of how often they turn out to be the answer: the HTTP-01 challenge path is being redirected, because a blanket HTTP-to-HTTPS redirect was added and /.well-known/acme-challenge/ was not excluded from it; port 80 has been closed by a firewall or security-group change; the renewal timer is not enabled or has never actually fired; a deploy hook fails and the client treats the whole renewal as failed; or the ACME account key was lost when the host was rebuilt.

Check the renewal actually works rather than trusting the exit code. A dry run exercises the whole path against the staging environment without consuming rate limits, and its output names the failure where the exit code does not.

Renew now, then find out why it did not happen by itself
# get the site back up
certbot renew --force-renewal --cert-name example.com
nginx -t && systemctl reload nginx        # Apache: apachectl configtest && systemctl reload apache2

# is the renewal job actually scheduled, and has it run?
systemctl list-timers | grep -i certbot
journalctl -u certbot.timer -u certbot.service --since '60 days ago' | tail -40

# does the whole renewal path work, end to end, without using up rate limits?
certbot renew --dry-run

# is the challenge path reachable over plain HTTP, unredirected?
# expect a 404 from your own server, NOT a 301
curl -sI http://example.com/.well-known/acme-challenge/test | head -1

Renewing from a manual certificate authority

Where the certificate was bought rather than issued over ACME, the shape is different: generate a key and a certificate signing request, give the CSR to the CA, complete whatever validation they ask for, and install what comes back. The validation step is the one that takes unpredictable time — it may be a DNS record, a file on the web server, or an email to an address at the domain — so start it before the certificate expires rather than after.

Generate a fresh private key rather than reusing the old one. Reusing a key means a key that leaked at any point in the past is still the key protecting the site. Prefer an elliptic-curve P-256 key over RSA: it is the same security level as RSA 3072 with much smaller handshakes and better performance on mobile, and every public CA issues for it.

Install the leaf and the intermediates the CA supplies as a single file, leaf first. On Apache 2.4.8 and later, the intermediates go in SSLCertificateFile alongside the leaf — SSLCertificateChainFile is obsolete and ignored, and configuration copied from older guides that still uses it will leave you serving an incomplete chain. HAProxy wants one file containing the private key, the leaf and the intermediates together.

Whatever the CA is, add a CAA record for the domain so only the authorities you actually use are permitted to issue for it. It costs one DNS record and it closes off a whole category of mis-issuance.

A new key and CSR, listing every hostname the certificate must cover
# a fresh EC P-256 key — do not reuse the old one
openssl ecparam -name prime256v1 -genkey -noout -out example.com.key
chmod 600 example.com.key

# a CSR listing every name as a subjectAltName; the CN alone is not read by browsers
openssl req -new -key example.com.key -out example.com.csr \
  -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:www.example.com"

# check the CSR says what you think before sending it
openssl req -in example.com.csr -noout -text | grep -A1 'Subject Alternative Name'

# assemble what the CA sends back: leaf first, then intermediates, never the root
cat example.com.crt intermediate.crt > /etc/ssl/example.com/fullchain.pem

Certificate lifetimes are shrinking on a published schedule

The reason this is becoming a more frequent problem rather than a less frequent one is that the maximum lifetime of a publicly trusted certificate is falling on a fixed, published schedule. CA/Browser Forum ballot SC-081v3 passed on 11 April 2025 and its schedule is now in the Baseline Requirements — the figures below are from Baseline Requirements version 2.3.0, dated 7 September 2026, section 6.3.2.

  • Issued before 15 March 2026: maximum 398 days.
  • Issued on or after 15 March 2026: maximum 200 days. This is the limit in force today.
  • Issued on or after 15 March 2027: maximum 100 days.
  • Issued on or after 15 March 2029: maximum 47 days.

A second schedule in section 4.2.1 shortens how long a CA may reuse a completed domain validation, on the same dates: 398 days, then 200, then 100, and then 10 days from 15 March 2029. That one matters more than it sounds. Today a CA can reuse your validation for the better part of a year, so a manual renewal often needs no validation step at all; from 2029 a validation older than ten days must be redone, which means the validation itself has to be automated too, not just the certificate request.

Let's Encrypt's default certificate is still 90 days as of September 2026, issued under what it calls the classic profile. Its published plan takes that default to 64 days on 10 February 2027 and to 45 days on 16 February 2028; a 45-day certificate is already available today under the tlsserver profile, and a roughly six-day certificate under the shortlived profile, both opt-in.

The practical conclusion is not that any of these dates is urgent. It is that manual renewal has a visible end date. A process that works today because somebody puts a reminder in a calendar twice a year does not survive a 47-day maximum with a 10-day validation window, and the time to automate is while the current certificate still has months on it rather than during the outage.

Automation and monitoring

Automated renewal and expiry monitoring are two separate things and you need both. Renewal automation is what stops the certificate expiring. Monitoring is what tells you the automation has stopped working — and it fails silently by design, because a renewal that errors out looks exactly like one that was not due yet.

Certbot 4.0.0 and later treats a certificate as ready for renewal when less than a third of its lifetime remains, and half the lifetime for certificates of ten days or less; before 4.0.0 the threshold was a fixed 30 days. For a 90-day certificate those two rules give the same answer, 30 days out, which is why the old figure is still quoted everywhere. For a 45-day certificate it means 15 days. Caddy manages renewal itself and needs no timer at all, which moves the failure mode from a missing cron job to a storage directory that is not persistent across restarts.

Because renewal starts at a third of the lifetime, a certificate still unrenewed inside 30 days of expiry on a 90-day cycle is not a tight schedule — it is evidence that renewal has already failed at least once. That is the point at which to investigate, and it is why our scanner warns at 30 days and escalates at 7 rather than waiting for the date itself.

For monitoring, alert on remaining days rather than on a renewal job's exit status, because the thing you care about is the certificate a client is offered, checked from outside your own network. A check that connects to the public hostname catches everything a check on the file system misses: a renewal that succeeded but was never reloaded, a load balancer still holding the old certificate, a second server in the pool that nobody renewed. Alert at 21 days for a 90-day certificate, and check every hostname rather than only the apex.

A remaining-days check you can run from cron or a monitor
#!/bin/sh
# exits non-zero when fewer than $DAYS days remain — check from outside your network
HOST="example.com"
DAYS=21

end=$(openssl s_client -connect "$HOST:443" -servername "$HOST" </dev/null 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2)
left=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))

echo "$HOST expires in $left days ($end)"
[ "$left" -ge "$DAYS" ]

When a certificate deployment goes wrong, and how to roll back

Replacing a certificate is a low-risk change that occasionally goes badly, and the failures are recognisable. The server will not start or reload, because the certificate and the private key do not match, or the file is truncated. The site loads but a fraction of clients fail, because the new file has the leaf without the intermediates. The site is fine on one server and broken on another, because a multi-server deployment updated some of them. Or the certificate is correct and the site still serves the old one, because nothing was reloaded.

Confirm the key and certificate match before reloading anything. The modulus of the certificate's public key and the private key must hash to the same value; if they do not, the reload will fail and you will have taken the site down to find out.

Rolling back is only possible if you kept the old files, so copy them somewhere before you overwrite anything. A rollback to a certificate that has not yet expired is clean and takes effect as soon as the server reloads. A rollback to an already-expired certificate is not a rollback — it is the outage you were trying to fix — so if the old certificate has expired, the only way out is forward: fix the new one.

Two things do not roll back. If you have already revoked the old certificate, it will not work again, so never revoke until the replacement is confirmed working. And a certificate whose private key you have published or lost control of should be revoked and never restored, whatever the inconvenience.

Check before you reload, and keep a way back
# keep the old files first
cp -a /etc/ssl/example.com /etc/ssl/example.com.bak-$(date +%F)

# do the certificate and the key actually match? the two hashes must be identical
openssl x509 -noout -modulus -in fullchain.pem | openssl sha256
openssl rsa  -noout -modulus -in privkey.pem   | openssl sha256
# for an EC key:
openssl ec -in privkey.pem -pubout 2>/dev/null | openssl sha256
openssl x509 -in fullchain.pem -pubkey -noout  | openssl sha256

# test the configuration, then reload — a test is not a reload
nginx -t && systemctl reload nginx

# confirm from outside, not from the server
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -dates

Revocation: what OCSP and CRLs do now

Revocation is a separate question from expiry — it is how a certificate is invalidated before its end date, usually because the private key was compromised — and how it works has changed substantially, so advice written a few years ago is now wrong.

OCSP, the protocol that let a client ask a CA in real time whether a certificate was still valid, is no longer required of certificate authorities. CA/Browser Forum ballot SC-063v4 made it optional and made CRLs mandatory instead, effective 15 March 2024. Let's Encrypt removed OCSP URLs from its certificates on 7 May 2025 and shut its OCSP responders down on 6 August 2025, publishing revocation exclusively through CRLs since.

OCSP Must-Staple, the extension that told browsers to refuse a connection with no stapled OCSP response, is a consequence of this. Let's Encrypt stopped issuing Must-Staple certificates on 7 May 2025, and a Must-Staple certificate cannot work against a CA that has no OCSP responder to staple a response from. If you have a Must-Staple certificate from earlier, it needs replacing with one without the extension — this is a case where an old configuration file quietly becomes an outage.

Browsers do still check revocation; they just do it locally. Firefox enabled CRLite for all desktop users in Firefox 137, downloading a compressed encoding of every revocation visible in Certificate Transparency logs and refreshing it every twelve hours, so no query about your browsing leaves the machine. Chrome uses CRLSets, built by crawling CRLs and CT logs and pushed to clients at most every few hours; Chromium's own documentation says online OCSP and CRL checks are not generally performed, and that CRLSets are a mechanism for blocking certificates in emergencies rather than comprehensive coverage.

What this means practically: if you need a certificate to stop working, revoking it is still correct and still the thing to do, but do not expect it to take effect everywhere immediately. Replace the certificate first, confirm the replacement works, then revoke. And treat short certificate lifetimes as the real mitigation — that is the reasoning behind the shortening schedule in the first place.

Failures that look like expiry and are not

Several distinct certificate faults produce a similar-looking browser interstitial, and treating them all as expiry means reissuing a certificate that was never the problem. The error code on the interstitial distinguishes them, and so does the verify output from openssl.

  • Name mismatch. The certificate is valid but does not cover the hostname typed. Chrome says NET::ERR_CERT_COMMON_NAME_INVALID. Two rules catch people out: browsers have not read the Common Name since 2017, so a certificate needs every name as a subjectAltName; and a wildcard covers exactly one label, so a certificate for the names under a domain does not cover the bare domain itself, and does not cover a name two labels deep either.
  • Self-signed. The certificate names itself as its own issuer. It encrypts the connection but proves nothing about who is on the other end, which is the half that stops interception, and browsers treat it as untrusted. Normal on a staging box, never acceptable on a host the public reaches.
  • Untrusted root. The chain is complete but leads to a root the client does not have — typically an internal corporate CA, which works on company machines and nowhere else.
  • Missing intermediate. As above: works in your browser, fails for other clients. Verify return code will say unable to get local issuer certificate.
  • Not yet valid. The start date is in the future, and browsers reject it exactly as they reject an expired one. This is almost always a wrong server clock, which breaks far more than TLS, so fix the clock before reissuing anything.
  • The wrong certificate entirely. A host serving several sites returns the default virtual host's certificate when SNI does not match a configured name — check server_name or ServerName before assuming the certificate is at fault.

Our SSL checker separates these rather than reporting one generic certificate error, because the fix for each is different and three of them do not involve issuing a new certificate at all.

What commonly goes wrong

  • A blanket HTTP-to-HTTPS redirect that also catches /.well-known/acme-challenge/. Renewal then fails every time, silently, for up to a third of the certificate's lifetime, and the first symptom is the site going down.
  • Renewing successfully and never reloading the server. The new certificate is on disk, the old one is still in memory and still being served, and every check on the file system says everything is fine.
  • Pointing the server at cert.pem instead of fullchain.pem. It works in your browser, because browsers fetch the missing intermediate themselves, and fails for mobile apps, curl and payment webhooks.
  • Monitoring the renewal job's exit status instead of the certificate a client is actually offered. This misses every failure that happens after issuance — an unreloaded server, a load balancer holding the old file, a second host in the pool nobody updated.
  • Revoking the old certificate before confirming the new one works, or leaving an old Must-Staple certificate in place. Revocation is not reversible, so a rollback to a revoked certificate is not available; and with Let's Encrypt's OCSP responders shut down since August 2025 there is no response for a Must-Staple certificate to staple.
  • Assuming the browser padlock proves the chain is correct. It proves it is correct for a browser with a warm intermediate cache, which is the one client most likely to paper over the fault.
  • Checking only the apex. Every hostname on the certificate, and every hostname that has its own certificate, needs its own expiry alert — a forgotten staging or mail hostname expiring is how HSTS with includeSubDomains turns one lapsed certificate into several unreachable services.

Check your domain with the ssl checker