Here’s one of the most common TLS mistakes to reach production, and it gets there precisely because it doesn’t look like a bug. The site works. You open it in Chrome, there’s a padlock, the certificate is valid, everyone moves on. Then a backend service tries to call the same host and dies with unable to get local issuer certificate. Or the mobile app rejects it. Or an uptime monitor starts paging. Nothing changed on the server — so how can it work in the browser and fail everywhere else?
Because your certificate chain is incomplete, and Chrome is covering for you.
What the server is supposed to send
A TLS certificate doesn’t stand alone. It’s the bottom of a chain: your leaf certificate (the one for your domain) is signed by an intermediate CA, which is signed — directly or through more intermediates — by a root CA. The client trusts the root because the root is baked into its trust store. It does not have your intermediate; intermediates come and go, and there are thousands of them. So the client has to build a path from your leaf up to a root it trusts, and to do that it needs every link except the root.
That’s the server’s job. During the handshake, the server is supposed to send the leaf and the intermediate(s) — everything between your certificate and the root. The root it can skip, because the client already has it.
The single most common way to get this wrong is to configure the server with only the leaf. In nginx it’s the difference between pointing ssl_certificate at fullchain.pem (leaf + intermediates, correct) and at the bare cert.pem (leaf only, broken). A dozen certificate tools and control panels make the same mistake in their own dialect. The result is a server that hands out its leaf and stops, leaving the client holding a certificate signed by an authority it was never given and can’t verify.
And here’s the thing: that server is misconfigured, unambiguously. It is not sending what the TLS specification says it must. It just happens to work in the one place you looked.
Why Chrome doesn’t care
Buried in your leaf certificate is an extension called Authority Information Access — AIA, defined in RFC 5280. One of its fields, caIssuers, is a URL where the issuing CA publishes the certificate that signed yours. It’s meant as a hint.
Chrome treats it as an instruction. When Chrome hits a server that forgot to send the intermediate, it reads the AIA URL out of the leaf, fetches the missing intermediate over plain HTTP, slots it into the chain, and completes verification — all before showing you anything. You get a padlock. You get no warning. You get no indication whatsoever that the server failed to do its job, because Chrome silently did the job for it. This is called AIA fetching, and it means a desktop Chrome user is essentially immune to incomplete-chain misconfigurations.
Which sounds helpful, and is exactly the trap. The one client almost everyone tests with is the one client that will lie to you about whether your chain is correct.
Everyone else verifies what you actually sent
Step outside the browser and the safety net vanishes:
- curl / OpenSSL:
unable to get local issuer certificate. - Java:
PKIX path building failed … unable to find valid certification path to requested target. - Go:
x509: certificate signed by unknown authority. - Python, Node, Ruby, Android apps, and most machine-to-machine clients: some flavor of the same hard failure.
None of these fetch the missing intermediate. They build the path from exactly what the server sent, and if the intermediate isn’t in that bundle, the path doesn’t reach a trusted root and verification fails — correctly. They’re not being pedantic. They’re doing what the browser only pretends to do, which is check that the server presented a complete, verifiable chain. (The exceptions are clients that hand verification to Windows or Apple’s system libraries — those platforms fetch missing intermediates too, which is one more way “works on my machine” gets reinforced.)
Firefox took a different route to the same result. Mozilla declined to implement AIA fetching, largely for privacy — every fetch tells a CA which site you just visited — and since Firefox 75 (2020) desktop Firefox instead preloads every intermediate disclosed to Mozilla’s root program, over two thousand of them at launch. So desktop Firefox also works on most incomplete chains from public CAs. Different mechanism, same effect on you: the browser you test with fills the gap, and nothing tells you there was a gap to fill.
Why this is such a good trap
Run the sequence forward. A developer sets up TLS, opens the site in Chrome, sees the padlock, and ships. There is no error to notice, because for them there is no error. The misconfiguration goes completely undetected through development, review, and deploy.
Then it surfaces somewhere with no human and no AIA fetching: a server-to-server API call, a webhook a partner’s backend sends to you, a payment processor’s callback, a Kubernetes liveness probe, an Android app, an email server validating STARTTLS. These are the connections where TLS actually protects something valuable, and they are exactly the connections that don’t get the browser’s quiet rescue. The failure lands far from the cause, often on someone else’s infrastructure, described by an error message — PKIX path building failed — that says nothing about the real problem being a missing intermediate on your side.
I’ll go further: AIA fetching was a mistake. Not because fetching a certificate is dangerous, but because making the dominant client tolerant of a specific misconfiguration guaranteed that misconfiguration would become endemic. Chrome externalized the cost of broken chains onto every other TLS client on the internet, and onto every developer who trusts “it works in my browser” to mean “it works.” A warning in Chrome’s address bar would have fixed more incomplete chains in a month than a decade of RFC-citing blog posts.
How to actually know
The fix for a broken chain is boring: serve the full chain. Point your server at the bundle that includes the intermediates, reload, done. Modern ACME clients hand you a fullchain.pem for exactly this reason, and if you use one and point it at the right file, you will likely never meet this bug.
Knowing whether you have it is the part that needs discipline, because your instinct — open it in a browser — is the one test that can’t detect it. Check with a client that doesn’t fetch. openssl s_client -connect host:443 -showcerts prints every certificate the server actually sent, and if you see only your leaf, the chain is incomplete no matter how green the padlock looks. Any honest chain check works the same way: it reads what the server transmits on the wire, not what a browser can reconstruct after a few helpful HTTP requests you never see.
Your certificate chain’s correctness is a property of what your server sends, full stop. It is not a property of what your browser shows you — and the gap between those two is where this bug lives, cheerfully, until the day something that isn’t a browser tries to connect.