When your browser shows a padlock icon, it’s not because the server has a certificate. It’s because the browser successfully validated a chain of trust — a sequence of cryptographic signatures linking the server’s certificate to a root CA that the browser already trusts.
This chain is the foundation of all TLS security. If any link is missing, expired, or invalid, the connection fails. Understanding how chains work — and why they break — is essential for anyone managing TLS infrastructure.
The Chain: End Entity → Intermediate → Root
A typical certificate chain has three levels:
End-Entity Certificate (your server's cert)
↑ signed by
Intermediate CA Certificate (the issuing CA)
↑ signed by
Root CA Certificate (pre-installed in browsers/OS trust stores)
End-Entity (Leaf) Certificate:
- Identifies your specific server (e.g.,
api.example.com) - Contains your server’s public key
- Signed by the Intermediate CA’s private key
- Short-lived (90-398 days)
Intermediate CA Certificate:
- Identifies the CA that issued your certificate
- Contains the Intermediate CA’s public key
- Signed by the Root CA’s private key
- Medium-lived (5-10 years)
Root CA Certificate:
- Self-signed (signs itself — it’s the trust anchor)
- Pre-installed in every browser and OS trust store
- Long-lived (20-30 years)
- Never used to sign end-entity certificates directly
How Chain Validation Works
When a client connects to your server over TLS:
Step 1: Server sends its end-entity certificate + intermediate certificate(s)
Step 2: Client receives the chain and starts validation from the leaf:
Is the end-entity cert expired? → Check notBefore/notAfter
Does the hostname match? → Check SAN extension
Who signed it? → Find the Issuer field → match to next cert in chain
Verify signature → use Intermediate's public key to verify leaf's signature
Step 3: Move up the chain:
Is the Intermediate cert expired? → Check validity
Is it a CA? → Check Basic Constraints (CA:TRUE)
Who signed it? → Find the Issuer field → look in local trust store
Verify signature → use Root's public key to verify Intermediate's signature
Step 4: Root lookup:
Is the Root in my trust store? → Yes → CHAIN VALID
→ No → CHAIN INVALID (untrusted root)
Step 5: Revocation check (optional, often skipped):
Check OCSP/CRL for end-entity → not revoked → OK
Check OCSP/CRL for intermediate → not revoked → OK
If all steps pass: connection proceeds. If any step fails: connection rejected with a certificate error.
Why the Server Must Send the Intermediate
The Root CA certificate is already in the client’s trust store — it doesn’t need to be sent. But the Intermediate CA certificate is NOT in the client’s trust store. The server must include it.
What the server sends:
[End-Entity Certificate]
[Intermediate CA Certificate]
What the server does NOT send:
[Root CA Certificate] ← client already has this
If the server only sends the end-entity certificate (without the intermediate), the client can’t build the chain. It has the leaf and the root, but nothing connecting them.
Common Chain Errors and How to Fix Them
Error: “unable to verify the first certificate”
Cause: Server sends only the leaf certificate. Intermediate is missing.
Diagnosis:
# Check what the server sends
openssl s_client -connect example.com:443 -showcerts
# Look for "Certificate chain" section
# If only 1 certificate is shown (depth 0), the intermediate is missing
Fix (Nginx):
# Combine leaf + intermediate into one file (order matters!)
cat server.crt intermediate.crt > fullchain.pem
# Nginx config
ssl_certificate /etc/ssl/certs/fullchain.pem; # Full chain
ssl_certificate_key /etc/ssl/private/server.key;
Fix (Apache):
SSLCertificateFile /etc/ssl/certs/server.crt
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
SSLCertificateKeyFile /etc/ssl/private/server.key
Error: “certificate has expired” (but your cert is valid)
Cause: The intermediate CA certificate has expired. Your end-entity cert is fine, but the intermediate it chains to is expired. The chain breaks at the intermediate link.
Diagnosis:
openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
# Shows YOUR cert dates (probably fine)
# Check the INTERMEDIATE:
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
awk '/BEGIN CERT/,/END CERT/{print}' | \
csplit - '/BEGIN/' '{*}' && \
openssl x509 -in xx01 -noout -dates
# If this shows expired → your intermediate needs updating
Fix: Download the current intermediate certificate from your CA’s repository and update your chain file.
Error: “self-signed certificate in certificate chain”
Cause: The server is sending the Root CA certificate in the chain. Some clients reject this (they expect to find the root in their own trust store, not receive it from the server).
Fix: Remove the root certificate from your chain file. Only include: leaf + intermediate(s).
Error: “certificate chain too long”
Cause: Path length constraint violated. The intermediate CA has pathLenConstraint:0 (can only sign end-entity certs), but there’s another intermediate below it.
Fix: This is a CA configuration issue. The hierarchy must respect path length constraints. You may need a certificate from a different intermediate that allows deeper chains.
Works in Chrome, fails in curl/Java/Python
Cause: Chrome has a feature called AIA (Authority Information Access) fetching — if the intermediate is missing, Chrome downloads it from the URL in the certificate’s AIA extension. Other clients (curl, Java, Python requests, Go) do NOT do this. They require the full chain from the server.
Lesson: Always test with curl or openssl s_client, not just a browser. If it works in Chrome but fails elsewhere, your chain is incomplete.
Verifying Your Chain
Quick check with OpenSSL:
# Verify the full chain
openssl verify -CAfile root-ca.pem -untrusted intermediate.pem server.crt
# Output: server.crt: OK
# Check what a server actually sends
openssl s_client -connect example.com:443 -servername example.com
# Look for:
# depth=2 ... (Root CA)
# depth=1 ... (Intermediate CA)
# depth=0 ... (Your certificate)
# "Verify return code: 0 (ok)"
Online tools:
# SSL Labs (comprehensive)
# https://www.ssllabs.com/ssltest/
# Check chain completeness
curl -sI https://example.com
# If this succeeds, your chain is complete (curl doesn't fetch missing intermediates)
Programmatic check:
import ssl
import socket
context = ssl.create_default_context()
with socket.create_connection(("example.com", 443)) as sock:
with context.wrap_socket(sock, server_hostname="example.com") as ssock:
cert = ssock.getpeercert()
print(f"Subject: {cert['subject']}")
print(f"Issuer: {cert['issuer']}")
print(f"Expires: {cert['notAfter']}")
# If this succeeds without SSLCertVerificationError, chain is valid
Cross-Signing: When Chains Get Complicated
Sometimes a certificate has multiple valid chain paths. This happens with cross-signing — where a new Root CA gets its certificate signed by an existing trusted Root, creating two paths to trust:
Path 1 (modern clients):
Your Cert → Intermediate → New Root (in trust store)
Path 2 (older clients):
Your Cert → Intermediate → New Root → Cross-Sign by Old Root (in trust store)
Real example: Let’s Encrypt’s ISRG Root X1 was cross-signed by IdenTrust’s DST Root CA X3. Modern clients trust ISRG Root X1 directly. Older clients (Android 7.0 and below) didn’t have ISRG Root X1 in their trust store, so they used the cross-sign path through DST Root CA X3.
When DST Root CA X3 expired in September 2021, older clients that relied on that path started failing — even though the primary chain via ISRG Root X1 was perfectly valid.
Lesson: If you serve certificates from a cross-signed hierarchy, test on your oldest supported clients. Different clients may build different chain paths.
Chain Best Practices
- Always serve the full chain (leaf + all intermediates, NOT the root)
- Test with curl, not just browsers (browsers hide chain problems)
- Monitor intermediate CA expiry (not just your end-entity cert)
- Keep chain files ordered correctly (leaf first, then intermediates in order up to root)
- Update intermediates when your CA rotates them (CAs periodically issue new intermediates)
- Don’t include the root in the chain file (clients have their own trust stores)
- Use automation (cert-manager, ACME clients) that handles chain assembly automatically
FAQ
Q: Why can’t the server just send the root certificate too? A: It can, and some servers do. Most clients ignore it (they use their own trust store copy). But some strict implementations reject chains that include the root. Best practice: don’t send it.
Q: How do I know which intermediate to use? A: Your CA provides it. When you receive your signed certificate, the CA also provides the intermediate certificate (or a “CA bundle” / “full chain” file). If you lost it, download it from your CA’s repository page.
Q: Can a certificate have multiple intermediates in the chain? A: Yes. A 3-tier PKI has: leaf → Issuing CA → Policy CA → Root. The server must send both the Issuing CA and Policy CA certificates. This is less common for public CAs but normal in enterprise PKI.
Q: What’s the maximum chain length? A: There’s no hard protocol limit, but path length constraints in certificates limit it. Most public CA chains are 3 levels (leaf + 1 intermediate + root). Enterprise PKI may be 4 levels. Longer chains add latency (more signatures to verify) and complexity.