Fix SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE When Your Intermediate CA Has Expired

intermediate๐Ÿ”’ SSL/TLS2026-05-31| Firefox, Chrome, curl, openssl โ€” any OS; Apache/Nginx/HAProxy servers serving TLS

Error Message

SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE
#ssl#firefox#intermediate-ca#certificate-chain#pki

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 notAfter date.
  • 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.

Related Error Notes