The certificate works in your browser and fails everywhere else
A certificate renewal goes out. The site loads perfectly on your laptop. Then
the reports start: a payment webhook is failing, an Android app cannot connect,
a partner's Java service throws
PKIX path building failed, and curl on a colleague's machine
says unable to get local issuer certificate.
Nothing is wrong with the certificate. The server is not sending the whole chain.
What a server is supposed to send
Trust in TLS is a path, not a single document. Three links:
- The leaf — your certificate, naming your domain.
- One or more intermediates — issued by the root, and the thing that actually signed your leaf.
- The root — already in the client's trust store.
The client has the root. It does not have the intermediate. Your server is responsible for sending it, and the rule is straightforward: send the leaf and every intermediate, but not the root. The root is redundant — the client either already trusts it, in which case sending it wastes bytes on every handshake, or it does not, in which case sending it changes nothing.
Why your browser hid the problem
Clients differ in how hard they try to recover from a missing intermediate, and that difference is the whole reason this bug reaches production.
- Browsers cache intermediates. Once a browser has seen an intermediate from any site, it may reuse it. Your machine had visited other sites using the same issuer, so it already had what your server failed to send.
- Some clients fetch the missing certificate. The leaf contains an Authority Information Access extension pointing at where to download the issuer. Several platforms will follow it. This masks the misconfiguration and adds a network round trip to the handshake.
- Many clients do neither. Most non-browser stacks — curl, Go, Java, Python, OpenSSL, and the HTTP libraries inside payment gateways and mobile apps — verify strictly against what the server presented. They fail, correctly.
So the failure is not random. It sorts neatly into "clients that guessed for you" and "clients that did not", and the second group contains most of your machine-to-machine traffic.
Seeing what your server actually sends
One command settles it:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
Count the certificate blocks that come back — each one is bracketed by a
pair of -----BEGIN and -----END lines. One block means the
leaf alone: that is the bug. You should also read the certificate chain summary near
the top, where each entry has a subject s: and an issuer i:.
In a correct chain, each certificate's issuer is the next one's subject, and the
final issuer is a root you never sent.
A clean way to prove it is to verify against nothing but the system store, which is what a strict client does:
curl -vI https://example.com
If this fails on a machine that has never visited the site, while a browser on the same machine succeeds, you have your answer.
How to fix it
Almost always, the server is pointed at the wrong file. Certificate authorities hand you several, and the names are not helpful:
cert.pemordomain.crt— the leaf only. Pointing your server at this is the mistake.chain.pem— the intermediates only.fullchain.pemorbundle.crt— leaf plus intermediates, in that order. This is the one you want.
In Nginx, ssl_certificate must point at the full chain, not the leaf.
In Apache, modern versions read the chain from
SSLCertificateFile if it contains one. Caddy and most managed platforms
handle this automatically, which is one fewer thing to get wrong.
Order matters: leaf first, then each intermediate in the order it signs. Some clients tolerate a shuffled chain; the specification permits them not to.
The related failure: an expired intermediate
A chain can be complete and still broken. When a certificate authority retires an intermediate or a cross-sign expires, servers that hardcoded the old bundle keep serving a certificate that no longer builds a path to any trusted root. Your leaf is valid for months; the chain under it is not.
This is worth checking whenever an issuer announces a root or intermediate rotation, because renewal alone does not fix it if your deployment pins an old bundle file.
What to check after every renewal
- The server sends more than one certificate.
- The chain builds to a trusted root without the client having to fetch anything.
- The hostname matches a Subject Alternative Name — the Common Name has not been used for hostname matching by browsers for years.
- No certificate in the chain expires sooner than the leaf.
Test from something that has never seen your site before. A machine that has already cached the intermediate cannot tell you whether you are sending it.
The SSL/TLS Grading module reports exactly what a host presents: how many certificates are in the chain, whether it builds to a trusted root, hostname match, expiry, protocol support and known weaknesses.