← Back to Blog
fundamentals

Understanding the SSL Certificate Chain of Trust: A Technical Guide (2026)

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.

🔒

Free SSL Checker

Check whether your full certificate chain is correctly served.

Try It Free →
↓ 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:

# 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

# 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.