← Back to the library
OperationsCore · 7 min

The site suddenly shows certificate errors

“Browsers show a full-page certificate warning, API clients fail with CERT_HAS_EXPIRED or "unable to verify the first certificate", and nobody deployed anything.”

ERROR CODES YOU MAY SEE

A code is a clue. Use its meaning and surrounding evidence to narrow the cause.

CERT_HAS_EXPIREDNode.js TLS (X509 verification) · platform

A certificate in the presented chain is past its notAfter date.

Check the leaf and intermediates with openssl s_client -showcerts. If the leaf is fresh, a server or load balancer may still be serving an old certificate or chain, or the client clock may be wrong.

Official reference for CERT_HAS_EXPIRED (opens in new tab)Code definition reviewed
UNABLE_TO_VERIFY_LEAF_SIGNATURENode.js TLS (X509 verification) · platform

The client could not verify the first certificate: no trusted issuer was found for the leaf.

Usually the server sends only the leaf, without the intermediate. Browsers may hide this by fetching or caching intermediates; Node, curl, and mobile clients will not. Serve the full chain file.

Official reference for UNABLE_TO_VERIFY_LEAF_SIGNATURE (opens in new tab)Code definition reviewed

THE PRINCIPLE

Monitor what clients actually receive on every endpoint, not whether the renewal job ran. Assume a renewal can fail at any step.

FIRST MOVES

  1. Check what is served: openssl s_client with -servername against each IP or load balancer, then read notAfter and the issuer. One stale node is enough to cause intermittent errors.
  2. If the leaf is valid but non-browser clients fail, the intermediate is missing. Serve fullchain, not only the leaf.
  3. Run the ACME client's renewal (certbot renew --dry-run or your client's equivalent) and read the error. Common causes are HTTP-01 blocked by a redirect or WAF, a DNS-01 token that expired, and rate limits.
  4. Make sure renewal reloads the consumer: a deploy hook for nginx or HAProxy, a Secret update for ingress, and an upload for CDNs that keep their own copy.
  5. Add external expiry monitoring. Let's Encrypt stopped sending expiration emails in June 2025.

TOOLS: THEN → NOW

Cron renewal on a fixed 60-day scheduleACME clients with ARI support, or renew at about two-thirds of the lifetime. Let's Encrypt's default moves to 64 days in Feb 2027 and 45 days in Feb 2028
Let's Encrypt expiry emailsProbe-based checks (blackbox_exporter probe_ssl_earliest_cert_expiry, uptime monitors) or certificate monitoring services
Certificates copied onto servers by handcert-manager, Caddy, or managed certificates at the load balancer or CDN

PATTERN SNAPSHOT

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

# Exit status 1 if it expires within 21 days: wire it into CI or a cron alert.
echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
  | openssl x509 -noout -checkend $((21 * 86400))

CLOSE THE AI. EXPLAIN THIS.

Renewal logs say success, yet users still get an expired certificate from one region. What are the three places that could still hold the old certificate?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed