Why Your Certificate Fails Even Though It's Not Expired
You check your certificate's expiry date. It's months away. But clients are rejecting connections with errors like certificate verify failed or CERT_HAS_EXPIRED. You didn't change anything on your server. What happened?
The answer almost always involves an intermediate certificate you forgot existed.
The hidden certificates in your chain
Your leaf certificate doesn't connect directly to a trusted root. Between them sit one or more intermediate certificates, and those intermediates have their own expiry dates, their own trust constraints, and their own potential for revocation.
When any certificate in the chain is expired or untrusted, the entire chain fails -- even if your leaf certificate is perfectly valid.
The Let's Encrypt DST Root CA X3 disaster
The most famous example hit millions of sites on September 30, 2021.
Here's what happened: Let's Encrypt's root (ISRG Root X1) was relatively new and not in older trust stores. To get broad compatibility, they cross-signed their intermediate (R3) with an older, widely-trusted root: DST Root CA X3, owned by IdenTrust.
The chain looked like this:
yoursite.com leaf
└── R3 (Let's Encrypt intermediate)
└── DST Root CA X3 (cross-signed, trusted everywhere)
On September 30, 2021, DST Root CA X3 expired. Overnight:
- OpenSSL 1.0.2 (default on Ubuntu 16.04, CentOS 7, and many embedded systems) rejected every Let's Encrypt certificate. It followed the chain to the expired cross-signed root and failed, even though the direct ISRG Root X1 path was valid.
- Older Android devices (pre-7.1.1) lost access to Let's Encrypt sites -- though a clever workaround using a cross-sign with a longer validity period kept many working.
- curl, wget, Python requests, and every other OpenSSL-based client on affected systems broke.
Browsers were fine because they had ISRG Root X1 in their trust stores and used the shorter, valid chain. But servers talking to other servers went down hard.
How intermediate expiry bites you
Beyond cross-signing disasters, here are the common scenarios:
1. You pinned an old intermediate in your chain file
When you first set up your server, you concatenated leaf.crt + intermediate.crt into a chain file. Two years later, your CA rotated their intermediate. You renewed your leaf certificate, but your chain file still contains the old intermediate.
If the old intermediate expired, the chain breaks. If it was revoked, some clients reject it.
2. Your CA rotated intermediates silently
CAs periodically rotate their intermediate certificates. When you renew, the new leaf might be signed by a different intermediate than the old one. If your automation only replaces the leaf certificate and keeps the old chain file, you end up with a leaf signed by "Intermediate B" but serving "Intermediate A" -- the chain doesn't link up at all.
3. Cross-signed chains aged out
Cross-signing is common when CAs establish new roots. The cross-signed bridge certificate has its own expiry, typically shorter than the new root's lifetime. Once it expires, clients that don't have the new root in their trust store lose the ability to validate the chain.
How to diagnose it
Check every certificate in your chain
openssl s_client -connect yoursite.com:443 -servername yoursite.com -showcerts </dev/null 2>&1
This prints every certificate the server sends. For each one, check the dates:
# Save each cert from the output to a file, then:
openssl x509 -in intermediate.pem -noout -dates -subject -issuer
Output:
notBefore=Mar 13 00:00:00 2024 GMT
notAfter=Mar 12 23:59:59 2027 GMT
subject=C = US, O = Let's Encrypt, CN = R3
issuer=C = US, O = Internet Security Research Group, CN = ISRG Root X1
If notAfter is in the past, you found your problem.
Verify the full chain explicitly
# Download the chain from your server
openssl s_client -connect yoursite.com:443 -servername yoursite.com -showcerts </dev/null 2>&1 | \
awk '/BEGIN CERT/,/END CERT/{ print }' > server-chain.pem
# Verify against the system trust store
openssl verify -verbose server-chain.pem
If an intermediate is expired, you'll see:
error 10 at 1 depth lookup: certificate has expired
The depth number tells you which certificate in the chain failed (0 = leaf, 1 = first intermediate, etc.).
Check for revocation
# Extract the OCSP URI from the intermediate
openssl x509 -in intermediate.pem -noout -ocsp_uri
# Query OCSP
openssl ocsp -issuer root.pem -cert intermediate.pem \
-url http://ocsp.example.com -resp_text
A revoked intermediate returns Cert Status: revoked.
How to fix it
Step 1: Get the current intermediate from your CA
Every major CA publishes their current intermediates:
- Let's Encrypt: https://letsencrypt.org/certificates/
- DigiCert: https://www.digicert.com/kb/digicert-root-certificates.htm
- Sectigo: https://support.sectigo.com/articles/Knowledge/Sectigo-Intermediate-Certificates
Download the intermediate that matches the issuer of your leaf certificate.
Step 2: Rebuild the chain file
# Verify the leaf was signed by this intermediate
openssl verify -CAfile intermediate-new.pem yoursite.crt
# Build the new chain
cat yoursite.crt intermediate-new.pem > fullchain.pem
Step 3: Reload your server
# nginx
nginx -t && systemctl reload nginx
# Apache
apachectl configtest && systemctl reload apache2
Step 4: Verify
openssl s_client -connect yoursite.com:443 -servername yoursite.com </dev/null 2>&1 | \
grep "Verify return code"
# Should show: Verify return code: 0 (ok)
Preventing this in the future
Use your CA's fullchain file. Most CAs (and certbot/ACME clients) provide a fullchain.pem that includes the current leaf and intermediates. Use that instead of assembling the chain yourself.
With certbot:
# certbot already gives you the right files:
ssl_certificate /etc/letsencrypt/live/yoursite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yoursite.com/privkey.pem;
Monitor your chain, not just your leaf. Set up monitoring that checks the full chain validity. A simple cron job works:
#!/bin/bash
expiry=$(openssl s_client -connect yoursite.com:443 -servername yoursite.com </dev/null 2>&1 | \
openssl x509 -noout -enddate | cut -d= -f2)
if openssl x509 -checkend 2592000 -noout <<< "$(openssl s_client -connect yoursite.com:443 \
-servername yoursite.com </dev/null 2>&1 | openssl x509)"; then
echo "Certificate OK, expires: $expiry"
else
echo "WARN: Certificate expires within 30 days: $expiry"
fi
Update your trust stores. On servers that call external APIs, keep ca-certificates current:
# Debian/Ubuntu
apt update && apt install -y ca-certificates
# RHEL/CentOS
yum update ca-certificates
The lesson from every intermediate certificate disaster is the same: your leaf certificate is only as trustworthy as the weakest link in its chain. Check the whole chain, not just the expiry date on your cert.