← Back to Blog
When a browser validates your SSL certificate, it doesn't just check the certificate itself — it checks the entire chain from your certificate back to a root certificate authority (CA) that the browser already trusts. If any link in that chain is missing or broken, visitors see "NET::ERR_CERT_AUTHORITY_INVALID" or "Your connection is not private," even with a valid certificate installed.
↓ signs
🔗
Intermediate Certificate Authority
DigiCert TLS RSA SHA256 2020 CA1, Let's Encrypt R11 — signed by root, used to sign end-entity certificates. Must be sent by your server. Most chain issues come from this being missing.
↓ signs
🔒
End-Entity (Leaf) Certificate
Your domain's certificate: yourdomain.com — issued to your domain, installed on your web server, sent to browsers during the TLS handshake.
Root CAs don't directly sign website certificates for security reasons. If a root CA's private key were compromised, every certificate it signed would need to be revoked. Intermediate CAs create an extra layer — if an intermediate is compromised, only certificates signed by that intermediate need revocation.
Why Intermediates Are Often Missing
Your browser knows about root CAs (they're pre-installed), but it doesn't know about intermediate CAs unless your server sends them. When you install an SSL certificate, you should always install the full chain — your certificate plus any intermediate certificates the CA provides.
The most common SSL installation mistake: installing only the end-entity certificate and forgetting the intermediate. Desktop Chrome and Firefox may still connect successfully (they cache intermediates or use AIA fetching), but mobile Safari, curl, and API clients will fail with ERR_CERT_AUTHORITY_INVALID.
Diagnosing Chain Problems with OpenSSL
# Full chain verification — shows the complete chain:
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null
# Count how many certificates are in the chain:
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| grep "issuer\|subject" | head -20
# Verify chain against the system trust store:
openssl verify -CApath /etc/ssl/certs yourdomain.pem
# Check who issued each certificate:
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -text | grep -E "Issuer:|Subject:"
Output from a correct 3-certificate chain looks like this:
$ openssl s_client -connect yourdomain.com:443 2>/dev/null | grep -E "^depth|^verify"
depth=2 C=US, O=DigiCert Inc, CN=DigiCert Global Root G2
verify return:1
depth=1 C=US, O=DigiCert Inc, CN=DigiCert TLS RSA SHA256 2020 CA1
verify return:1
depth=0 CN=yourdomain.com
verify return:1
Verify return code: 0 (ok)
# A broken chain shows:
depth=0 CN=yourdomain.com
verify error:num=20:unable to get local issuer certificate
verify return:1
Verify return code: 20 (unable to get local issuer certificate)
Building and Installing the Correct Chain
When your CA sends you a certificate, they typically provide:
yourdomain.crt — your end-entity certificate
intermediate.crt or ca-bundle.crt — the intermediate(s)
- Some CAs provide a
fullchain.crt with everything bundled
# Create a combined chain file (end-entity first, then intermediate):
cat yourdomain.crt intermediate.crt > fullchain.crt
# Verify the combined chain (should show OK):
openssl verify -CApath /etc/ssl/certs -untrusted intermediate.crt yourdomain.crt
# yourdomain.crt: OK
# Apache — use SSLCertificateChainFile (deprecated) or include intermediates in fullchain:
SSLCertificateFile /etc/ssl/certs/fullchain.crt
SSLCertificateKeyFile /etc/ssl/private/yourdomain.key
# Or separately (older Apache):
SSLCertificateFile /etc/ssl/certs/yourdomain.crt
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
# Nginx — always use the combined fullchain file:
ssl_certificate /etc/ssl/certs/fullchain.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
Let's Encrypt Chain (ISRG Root X1)
Let's Encrypt uses the ISRG Root X1 root certificate. Certbot installs the full chain automatically. The fullchain.pem file it generates contains your certificate plus the R11 (or R10) intermediate:
# Let's Encrypt certificate locations:
/etc/letsencrypt/live/yourdomain.com/cert.pem # end-entity only
/etc/letsencrypt/live/yourdomain.com/chain.pem # intermediate only
/etc/letsencrypt/live/yourdomain.com/fullchain.pem # end-entity + intermediate
# ALWAYS use fullchain.pem in your web server config:
# Nginx:
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
# Apache:
SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
# Verify the Let's Encrypt chain:
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -issuer
# issuer=C=US, O=Let's Encrypt, CN=R11
Cross-Signed Certificates and Root Transitions
Root CAs are replaced over time (old roots expire or are distrusted). During transitions, CAs use cross-signed certificates — the same intermediate is signed by both old and new roots. This ensures compatibility with both old and new devices:
# Let's Encrypt ISRG Root X1 → X2 transition example:
# Old Android devices trust DST Root CA X3 (expired Sept 2021)
# New ISRG Root X1 chain works on all devices with Android 7.1.1+
# For older Android, certbot --preferred-chain "ISRG Root X1" uses the cross-signed path
# Check which chain your server sends to older clients:
openssl s_client -connect yourdomain.com:443 \
-servername yourdomain.com \
-tls1_2 2>/dev/null | grep "issuer"
Common Chain Error Messages
- NET::ERR_CERT_AUTHORITY_INVALID — Missing intermediate, wrong chain order, or self-signed without trust
- SSL_ERROR_RX_RECORD_TOO_LONG — Often means HTTPS traffic hitting an HTTP port (check port config), not a chain issue
- UNABLE_TO_GET_ISSUER_CERT_LOCALLY — Your system's CA bundle doesn't contain the root for this chain (common with newer roots on older servers)
- CERTIFICATE_VERIFY_FAILED in curl/Python — Missing intermediate or chain in wrong order
# Debug curl certificate errors verbosely:
curl -vvv https://yourdomain.com 2>&1 | grep -i "cert\|SSL\|verify"
# On an API client (Python), add verbose SSL debug:
import ssl, http.client
http.client.HTTPSConnection.debuglevel = 1
conn = http.client.HTTPSConnection("yourdomain.com")
conn.request("GET", "/")
# This shows the full handshake + chain verification
Verifying the Certificate Chain on Windows
Windows
PowerShell / certutil
Using certutil (Built-in Windows Tool)
:: View the certificate chain from a file
certutil -dump yourdomain.crt
:: Verify the chain builds to a trusted root
certutil -verify -urlfetch yourdomain.crt
:: Check which certificates are in the Windows Trust Store
certutil -store "Root"
:: Test the chain from a live server
:: First download the certificate:
openssl s_client -connect yourdomain.com:443 -showcerts 2>nul > chain.pem
certutil -dump chain.pem
Using PowerShell
# Load certificate and build chain
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\path\yourdomain.crt")
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
$chain.Build($cert) | Out-Null
# Display each certificate in the chain
for ($i = 0; $i -lt $chain.ChainElements.Count; $i++) {
$el = $chain.ChainElements[$i]
Write-Host "[$i] $($el.Certificate.Subject)"
Write-Host " Issued by: $($el.Certificate.Issuer)"
Write-Host " Expires: $($el.Certificate.NotAfter)"
Write-Host ""
}
# Check for chain errors
if ($chain.ChainStatus.Count -gt 0) {
Write-Host "Chain errors:"
$chain.ChainStatus | ForEach-Object { Write-Host " - $($_.StatusInformation)" }
} else {
Write-Host "Chain is valid!"
}
Windows Trust Store: Unlike Linux (which uses a system CA bundle file), Windows maintains its trust store in the Windows Certificate Store. Open certmgr.msc to browse trusted root CAs and intermediate certificates installed on your machine.
Verifying the Certificate Chain on macOS
macOS
Terminal / Keychain
# View the full chain from a live server
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null
# Verify chain completeness (save the server chain first)
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null > server-chain.pem
# Verify each cert in the chain
openssl verify -CAfile /opt/homebrew/share/ca-certificates/cacert.pem server-chain.pem
# Or use the macOS system CA bundle
openssl verify -CAfile /etc/ssl/cert.pem yourdomain.crt
# macOS security command — verify against Keychain trust store
security verify-cert -c yourdomain.crt -v
On macOS, the system trust store is managed by Keychain Access and updated via macOS Software Update. To add a custom CA certificate (for internal/self-signed chains):
# Add a CA certificate to macOS System Keychain (requires admin)
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain your-ca-cert.crt
# Verify it was added
security find-certificate -c "Your CA Name" -p /Library/Keychains/System.keychain | \
openssl x509 -noout -subject -issuer
Check your certificate chain instantly
Our SSL checker validates your complete certificate chain, detects missing intermediates, and shows the full chain in order.