TL;DR
You're getting SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE because one of the intermediate CA certificates in your server's chain has expired. Either your CA issued a replacement intermediate cert, or you're serving the wrong intermediate altogether. Quick path:
- Download the new intermediate cert bundle from your CA.
- Update your server config to use it.
- Reload/restart the web server.
If you're on Let's Encrypt and hit this after September 2021, you almost certainly need to drop the expired DST Root CA X3 cross-signed chain โ keep reading.
What's Actually Happening
When a browser validates your TLS certificate, it walks up the chain: your leaf cert โ one or more intermediate CAs โ a trusted root. If any intermediate in that chain has a notAfter date in the past, Firefox throws SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE rather than accepting the connection.
Common triggers:
- Your CA rotated its intermediate and you never updated the bundle on the server.
- Let's Encrypt's old DST Root CA X3 cross-signed chain expired (September 30, 2021) โ older chain still served by some configs.
- You pinned a specific intermediate in your deployment and it quietly aged out.
- An internal PKI intermediate expired and no one noticed until Firefox started blocking.
Diagnose First โ Don't Guess
Run this before touching anything:
# Check the full chain your server is actually sending
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null | openssl x509 -noout -text | grep -A2 'Validity'
# Better: see every cert in the chain with dates
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null \
| awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' \
| csplit -z -f cert- - '/-----BEGIN CERTIFICATE-----/' '{*}' 2>/dev/null
for f in cert-*; do
echo "=== $f ==="
openssl x509 -noout -subject -issuer -dates -in "$f"
done
Look at the notAfter line for each cert. The one with a past date is your culprit.
Quick check with curl if you just want a fast answer:
curl -vI https://yourdomain.com 2>&1 | grep -E 'expire|SSL|certificate'
Fix 1 โ Update the Intermediate Bundle (Most Common)
Get the current intermediate from your CA's website. For Let's Encrypt:
# Download the active ISRG Root X1 chain (not the expired DST cross-signed one)
wget https://letsencrypt.org/certs/lets-encrypt-r3.pem -O /etc/ssl/intermediate.pem
# Or the full chain (intermediate + root)
wget https://letsencrypt.org/certs/lets-encrypt-r3-cross-signed.pem
For commercial CAs, grab the bundle from their support portal. It's usually labeled "intermediate certificate" or "CA bundle."
Apache
# In your VirtualHost block
SSLCertificateFile /etc/ssl/certs/yourdomain.crt
SSLCertificateKeyFile /etc/ssl/private/yourdomain.key
SSLCertificateChainFile /etc/ssl/intermediate.pem
# Test config before reloading
apachectl configtest
systemctl reload apache2
Nginx
# Nginx wants leaf cert + intermediates in one file
cat yourdomain.crt intermediate.pem > /etc/ssl/certs/yourdomain_bundle.crt
# In server block
ssl_certificate /etc/ssl/certs/yourdomain_bundle.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
nginx -t
systemctl reload nginx
HAProxy
# HAProxy needs key + cert + chain all in one PEM
cat yourdomain.key yourdomain.crt intermediate.pem > /etc/haproxy/yourdomain.pem
# In frontend/backend section
bind *:443 ssl crt /etc/haproxy/yourdomain.pem
systemctl reload haproxy
Fix 2 โ Let's Encrypt Specific (DST Root CA X3 Expiry)
If certbot is still generating certs using the old expired DST Root CA X3 cross-signed chain, force it to use the ISRG Root X1 preferred chain:
# Force renewal with the non-expired chain
certbot renew --preferred-chain "ISRG Root X1" --force-renewal
# Or add to /etc/letsencrypt/cli.ini permanently
preferred-chain = ISRG Root X1
Then rebuild your bundle and reload the server.
Fix 3 โ Internal PKI (Self-Managed CA)
If you run your own CA, you need to issue a new intermediate cert, distribute it, and update every server in your fleet. Steps:
# Generate new intermediate cert (valid 5 years this time)
openssl genrsa -out intermediate.key 4096
openssl req -new -key intermediate.key -out intermediate.csr
# Sign with root CA
openssl x509 -req -in intermediate.csr \
-CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-out intermediate.crt -days 1825 -sha256 \
-extfile intermediate_ext.cnf
# Distribute intermediate.crt to all servers
ansible all -m copy -a "src=intermediate.crt dest=/etc/ssl/intermediate.crt" \
&& ansible all -m service -a "name=nginx state=reloaded"
Verify the Fix
After reloading, confirm the chain is clean:
# All dates should be in the future
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null \
| openssl x509 -noout -dates
# Use SSL Labs for a full chain grade (A or A+)
# https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com
# Or testssl.sh locally
./testssl.sh --chain yourdomain.com
In Firefox, open DevTools โ Security tab after a hard reload (Ctrl+Shift+R). You should see "Connection secure" with no certificate warnings.
Prevent This Next Time
- Set a calendar reminder 60 days before your intermediate CA's
notAfterdate. - Add a cron job that alerts when any cert in the chain expires within 90 days:
# /etc/cron.weekly/check-chain-expiry
#!/bin/bash
HOST="yourdomain.com"
EXPIRY=$(openssl s_client -connect $HOST:443 -showcerts 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
[ $DAYS -lt 90 ] && echo "WARNING: chain cert on $HOST expires in $DAYS days" | mail -s "Cert Expiry Alert" ops@yourcompany.com
- Use Certbot's auto-renewal timer (
systemctl status certbot.timer) and verify it's actually running. - Monitor with an external service like UptimeRobot or Checkly โ they'll catch expiries before users do.

