Fix an Incomplete SSL Certificate Chain
It loads fine in Chrome, then someone's curl script or old Android phone can't connect. This is almost always a missing intermediate certificate.
What an Intermediate Certificate Is
Browsers and operating systems don't trust your leaf certificate directly — they trust a small set of root CAs baked into a trust store, and roots almost never sign leaf certificates themselves. Instead a root signs one or more intermediate CAs, and an intermediate signs your actual certificate. This keeps the root's private key offline and rarely used, which matters because a compromised root would require every device on earth to update its trust store; a compromised intermediate can simply be revoked. The result is a chain: leaf → intermediate (sometimes several) → root. A client verifying your certificate has to walk that entire chain, checking at each step that the Issuer name on one certificate matches the Subject name on the next, and that each signature verifies with the next certificate's public key, until it reaches a root already in its trust store.
The root itself is never supposed to be part of what your server sends — it's already on the client, and sending it wastes bytes on every handshake. What your server does need to send is every intermediate between your leaf and that root. Omit one, and the chain has a gap the client cannot bridge on its own — or rather, some clients can bridge it and some can't, which is exactly what makes this bug so confusing.
Why It “Works in Chrome” and Fails Everywhere Else
Modern desktop browsers implement AIA chasing: if a served chain is missing an intermediate, the browser reads the Authority Information Access extension on the certificate it does have, which often includes a caIssuers URL pointing at the missing intermediate, fetches it over plain HTTP, and completes the chain itself. Chrome and Firefox both do this and cache the result, so a misconfigured server can look completely fine in a browser tab — sometimes for weeks, until the cache expires or someone opens the site in a browser profile that's never fetched that intermediate before.
Most non-browser TLS clients do not do this. curl (via OpenSSL, by default) expects the server to send a complete chain and simply fails with unable to get local issuer certificate if it doesn't. Countless mobile apps, backend HTTP clients, webhooks, payment gateways, and IoT devices behave the same way. This is why the classic symptom is “it works when I check it in my browser, but customers on older Android phones / our server-to-server webhook / a partner's integration can't connect” — the browser is silently patching over a server misconfiguration that every non-chasing client is exposed to directly. Treat AIA chasing as a client-side convenience you cannot rely on, never as a substitute for serving the full chain yourself.
Diagnosing With openssl s_client
openssl s_client shows you exactly what a server sends on the wire, with no chasing or caching to mask the problem:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
Look at two things in the output. First, the numbered Certificate chain list near the top — count the certificates and check each i: (issuer) line matches the s: (subject) line of the certificate above it, with no gap. Second, the final summary line: Verify return code: 0 (ok) means OpenSSL, using your system's trust store and only what the server sent, was able to build a complete path to a trusted root. Anything else — commonly unable to get local issuer certificate — confirms the server is not sending a complete chain, independent of what your browser shows you. Paste the certificates from the -showcerts output straight into the tool below to see the same gap visualized.
Fixing It: Serve a Complete fullchain.pem
The fix is almost always the same regardless of stack: concatenate your leaf certificate with every intermediate, leaf first, and point your server at that combined file instead of the leaf alone. Do not include the root.
cat leaf.pem intermediate.pem > fullchain.pem
Certbot (Let's Encrypt's ACME client) already writes this file for you as fullchain.pem alongside cert.pem — the single most common cause of this exact bug is a server config pointed at cert.pem instead. In nginx, the relevant directive is:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
In Apache, the equivalent is SSLCertificateFile pointed at the full chain (older Apache versions used a separate SSLCertificateChainFile directive — check your version's docs, since mixing the two incorrectly reintroduces the same bug). Cloud load balancers (AWS ALB/NLB with ACM, Cloudflare, GCP load balancers) generally handle chain-building for you once you upload the intermediate along with the leaf during certificate import — the failure mode there is usually an import step where only the leaf got pasted in. Whatever the platform, the fix is the same concept: give the server every certificate between your leaf and a trusted root, in order, and nothing beyond that.
Check Your Own Chain
Paste your chain below — a fullchain.pem, or the output of openssl s_client -showcerts — and the tool reorders it leaf-to-root, flags anything out of order, and tells you whether it terminates in a self-signed root or has a gap. Everything runs locally in your browser; nothing is uploaded.
Drop a .pem, .crt, .cer, .csr, or .der file here or click to browse
.pem,.crt,.cer,.csr,.der
Want to check what a live server is actually serving right now, without running openssl yourself? The Domain Intelligence Dashboard fetches a hostname's served chain directly. And if the fields inside any single certificate in the chain need decoding, see how to read an SSL certificate.
Further Reading
- RFC 5280 — Certification Path Validation
The IETF algorithm for building and validating a certification path from leaf to trust anchor.
- Let's Encrypt: Chain of Trust
Let's Encrypt's own roots, intermediates, and guidance on which chain to serve.
- CA/Browser Forum Baseline Requirements
Defines the rules around intermediate issuance that make chain length and structure predictable.
- Mozilla Wiki: CA/Certificate Policy
Mozilla's root program policy, including intermediate and cross-signing requirements relevant to chain building.