← All posts

The certificate works in your browser and fails everywhere else

6 September 2026 · 6 min read · TLS and certificates

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

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:

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

  1. The server sends more than one certificate.
  2. The chain builds to a trusted root without the client having to fetch anything.
  3. The hostname matches a Subject Alternative Name — the Common Name has not been used for hostname matching by browsers for years.
  4. 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.