If the certificate has already expired

If you’re a visitor and a site shows NET::ERR_CERT_DATE_INVALID (Firefox: SEC_ERROR_EXPIRED_CERTIFICATE), first check your device’s date and time. A wrong clock makes valid certificates look expired. If the clock is right and the error appears on other devices too, the site’s certificate has expired and only its operator can fix it. Details are in the date-invalid guide.

If you run the site, here is the shortest path back:

  1. Confirm what is actually being served, for the exact name that fails:

    openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null \
      | openssl x509 -noout -subject -issuer -enddate
    subject=CN = www.example.com
    issuer=C = US, O = Let's Encrypt, CN = R11
    notAfter=Oct  2 08:14:51 2026 GMT
  2. Check whether a newer certificate already exists on disk. With certbot, certbot certificates lists each lineage with its expiry date. If the file on disk is newer than what’s served, the renewal worked and only the reload or deployment didn’t. Skip to step 4.

  3. If not, renew now: certbot renew --cert-name example.com. If that fails, the error tells you whether it’s validation (port 80, DNS, CAA) or something else; the causes are below.

  4. Reload every service that uses the certificate, not just the web server. nginx -t && systemctl reload nginx, then mail, proxies, anything else.

  5. Re-check from outside, for every name and every endpoint (see “All the places a certificate lives”). The SSL check shows the expiry date and chain as the internet sees them.

Then come back and fix why it happened, because it will happen again.

Why this is getting more urgent

For years the maximum lifetime of a publicly trusted certificate was 398 days, so many teams renewed by hand once a year from a calendar reminder. That no longer works.

The CA/Browser Forum, the body of CAs and browser vendors that sets the rules, voted to cut maximum validity in steps. Certificates issued from 15 March 2026 may be valid for at most 200 days. Further cuts follow: 100 days in 2027 and 47 days in 2029. Commercial CAs still sell one-year or multi-year plans, but the certificate inside the plan has to be reissued and redeployed at least every 200 days now, and far more often later.

Two more things changed. Let’s Encrypt certificates have always been 90 days, and Let’s Encrypt stopped sending expiry reminder emails in 2025, so if you relied on those, nothing warns you anymore. And every shorter lifetime makes renewal more frequent, which means a broken automation path shows up sooner, but only if something is watching.

The rest of this page is about making renewal boring: automated, deployed everywhere, and monitored from outside.

Checking expiry from the command line

openssl x509 -checkend N answers “will this certificate expire within N seconds?” It prints a sentence and, more usefully, sets the exit code: 0 if it will not expire in that window, 1 if it will.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate -checkend 1209600
notAfter=Oct  2 08:14:51 2026 GMT
Certificate will expire

1209600 seconds is 14 days. For a file on disk:

openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate -checkend 1209600

Comparing the two is the quickest way to tell “renewal failed” from “renewal worked but the server wasn’t reloaded”.

A minimal check you can run from cron on a different machine:

#!/bin/sh
# Report any endpoint whose certificate expires within 14 days or can't be read
for target in example.com:443 www.example.com:443 api.example.com:443 mail.example.com:465; do
  host=${target%:*}
  if ! echo | openssl s_client -connect "$target" -servername "$host" 2>/dev/null \
       | openssl x509 -noout -checkend $((14*86400)) >/dev/null 2>&1; then
    echo "EXPIRING OR UNREACHABLE: $target"
  fi
done

If the connection fails, openssl x509 gets no input and also exits non-zero, so an unreachable endpoint is reported instead of silently passing. That’s deliberate.

For mail on STARTTLS ports, add -starttls:

openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -enddate

Use -starttls imap for 143, and connect directly (no -starttls) for 465, 993 and 995.

Picking a threshold

The alert should fire when renewal has clearly failed, early enough to fix it calmly.

For 90-day ACME certificates that renew automatically about 30 days before expiry, anything under roughly 20 days means the renewal job has been failing for over a week. Alert at 14 days and treat it as an incident.

For commercial certificates where renewal involves a purchase order, an approval or a validation email, alert much earlier, 30 to 45 days, because the slow part isn’t technical.

Run the check from outside your network. A monitor on the same host reads the same files and the same local DNS as the server and can’t see what a CDN or load balancer is serving.

Why automatic renewal fails

Most renewal failures fall into a short list. Check the client’s log first: /var/log/letsencrypt/letsencrypt.log for certbot, or journalctl -u certbot.service on systems that run it from a systemd timer.

The timer or cron job isn’t running. Package installs usually add certbot.timer (or snap.certbot.renew.timer for the snap). Check with systemctl list-timers | grep certbot. Containers and images built from scratch often have no scheduler at all.

HTTP-01 can’t reach port 80. A new firewall rule, a cloud security group, or a CDN that now answers for the name. Let’s Encrypt validates from several locations, so a geo-block or an IP allowlist on port 80 also breaks it. The port check tests 80 and 443 from outside.

The webroot moved. --webroot -w /var/www/html was right when the certificate was created; the site has since moved to /srv/app/public, so the challenge file is written to a directory nginx no longer serves.

DNS-01 credentials expired. API tokens get rotated, revoked when someone leaves, or were scoped to the wrong zone. This is common with wildcard certificates, which can only use DNS-01.

DNS moved. The domain now points at a new host or a CDN, and the old server is still trying to renew a name it no longer receives traffic for.

A CAA record blocks the CA. Someone tightened CAA, or a subdomain got its own record that doesn’t list your CA. The error says CAA record ... prevents issuance. See CAA records.

Rate limits. Repeatedly retrying a broken setup against the production CA can hit Let’s Encrypt’s limits and lock you out for a while. Test with --dry-run, which uses the staging environment.

certbot renew --dry-run runs the whole path, including validation, against staging. Run it after any change to firewalls, DNS hosting, web server layout or CAA.

Renewal is not deployment: hooks

Renewing writes new files. Nothing reads them until each service reloads. nginx, Apache, HAProxy, Postfix and Dovecot all load the certificate at start or reload and keep it in memory. Java applications usually read a keystore once at startup.

certbot’s answer is the deploy hook, which runs only after a successful renewal. Put executable scripts in /etc/letsencrypt/renewal-hooks/deploy/ and they run for every certificate:

#!/bin/sh
# /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh  (chmod +x)
set -e
nginx -t && systemctl reload nginx
systemctl reload postfix dovecot

Or attach one to a single certificate with --deploy-hook, which certbot saves in /etc/letsencrypt/renewal/<name>.conf:

certbot reconfigure --cert-name example.com --deploy-hook "systemctl reload nginx"

reconfigure exists in recent certbot releases. On older ones, add a renew_hook = systemctl reload nginx line under [renewalparams] in that renewal config file.

Inside a deploy hook, $RENEWED_LINEAGE is the certificate’s directory (for example /etc/letsencrypt/live/example.com) and $RENEWED_DOMAINS lists its names. That’s what you need for services that want a different file layout. HAProxy is usually configured with one PEM containing the chain and key:

cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" > /etc/haproxy/certs/example.com.pem
systemctl reload haproxy

acme.sh does the same with --install-cert, which copies the files where you want them and stores a reload command:

acme.sh --install-cert -d example.com \
  --key-file /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.crt \
  --reloadcmd "systemctl reload nginx"

Deploy fullchain.pem, not cert.pem. The leaf alone works in some browsers, which fill in missing intermediates themselves, and fails in curl, Java, Android apps and mail clients. A renewal can come with a different intermediate, so a hard-coded chain file left from last year can break things too. See chain mismatch.

All the places a certificate lives

Most expired-certificate incidents aren’t at the web server. The certificate was renewed there and nowhere else. Make a list once, and make the deploy hook or your runbook cover every line of it.

WhereWhat to watch for
Web server (nginx, Apache, Caddy)Needs a reload after renewal. Caddy manages its own certificates.
CDN edgeCloudflare’s own edge certificates renew automatically. A custom certificate you uploaded does not; it must be replaced by hand or by API.
Origin behind the CDNSeparate certificate on a separate schedule. With Cloudflare in Full (strict), an expired origin certificate shows as error 526, not as a browser certificate error.
Cloud load balancerAWS ACM renews certificates it issued with DNS validation as long as the validation CNAME stays in DNS. Email-validated ones need someone to click, and imported certificates never renew.
Every node in a poolIf www resolves to several IPs, or a load balancer has several backends doing TLS, one stale node gives intermittent errors. Check each IP (below).
Mail serverPostfix (SMTP 25/465/587) and Dovecot (IMAP 993, POP3 995) usually have their own paths and need their own reload. Expiry here shows up as delivery failures and client warnings, not as a website error.
Kubernetescert-manager renews into a Secret. An ingress controller picks it up; an application that read the mounted file at startup may need a restart.
Java and .NET appsKeystores and the Windows certificate store hold a copy. They need importing and usually a restart.
AppliancesVPN gateways, firewalls, NAS boxes, printers and admin panels, often renewed by hand from a vendor UI.
Baked-in copiesDocker images, mobile apps that pin the certificate or key, partner systems that pinned your certificate. Certbot creates a new key at each renewal unless you use --reuse-key, which breaks key pinning.

To check every IP behind one name:

for ip in $(dig +short A www.example.com | grep -E '^[0-9.]+$'); do
  printf '%s  ' "$ip"
  echo | openssl s_client -connect "$ip:443" -servername www.example.com 2>/dev/null \
    | openssl x509 -noout -enddate
done

If the name is behind a CDN, these IPs are the CDN’s edge, and you’re checking its certificate. To check the origin, connect to the origin IP the same way with the public hostname as -servername.

After every renewal

  • Each public name (apex, www, API, mail host) shows the new expiry date from outside. The SSL check is enough for the web ones.
  • The chain verifies: openssl s_client ... -verify_return_error exits cleanly, and curl without -k works.
  • HTTPS still answers end to end, with redirects intact. The HTTP check shows the redirect chain and final status.
  • The monitor would have caught it if this renewal had failed. If you’ve never seen your expiry alert fire, point it once at a test endpoint with a short-lived certificate and make sure the alert reaches a person.

A chain problem after renewal looks like NET::ERR_CERT_AUTHORITY_INVALID on some clients and works on others. And chains expire too: when an old Let’s Encrypt root expired in September 2021, some older clients failed even though the server certificates themselves were valid. Your monitor should check that the chain verifies, not only the leaf’s date.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.