TL;DR: Your server is supposed to hand the client every cert needed to walk from your leaf back to a trusted root, minus the root itself. If you only send the leaf, modern desktop browsers quietly paper over the bug by fetching the missing intermediate themselves — but mobile apps, IoT devices, older Java stacks, and most curl/API clients fail with a cryptic “TLS error.” It’s the single most common real-world cert misconfiguration, and the hardest to spot, because it works in the browser you’re testing from.

What is it?

Browsers don’t trust your certificate directly. They trust a small set of “root” certificates baked into the OS or browser. Your cert is signed by an “intermediate” cert, which is signed by a root. So validating “this is yourdomain.com” walks a chain:

your leaf cert  →  Let's Encrypt R3  →  ISRG Root X1  →  trusted by the OS

The rule for a working HTTPS deployment is simple: the server sends the leaf cert plus every intermediate, in order, during the TLS handshake. The root is omitted — every client already has it, and sending it is at best wasted bytes and at worst flagged by some validators as a chain issue.

Most cert bundle files do this correctly out of the box. Let’s Encrypt’s fullchain.pem is leaf + intermediates concatenated and ready to serve. Where things go wrong is when someone manually assembles the bundle and uses cert.pem (leaf only) instead of fullchain.pem, or when a paid CA delivers the leaf and intermediate as separate files and only one of them ends up on the server.

Why does it matter?

Because most of the web hides the bug.

When a modern desktop browser sees a leaf cert with no intermediate, it looks for the AIA (Authority Information Access) extension inside the leaf. That extension contains a URL pointing to the issuing CA’s intermediate cert. The browser fetches it, completes the chain itself, and the page loads. The user never knows.

Almost no other client does this. Specifically:

  • iOS and Android apps that hit your API don’t fetch via AIA.
  • IoT devices with embedded TLS stacks don’t fetch via AIA.
  • Older Java versions (anything before recent OpenJDK updates) don’t fetch via AIA by default.
  • Most curl builds, Go’s net/http, Python’s requests, and the long tail of API clients don’t fetch via AIA.

So the failure pattern looks like this: the marketing site loads fine, ranks fine in Google, looks fine in your laptop’s Chrome — but your iOS app throws “the network connection was lost,” your Android release crashes on launch, and a partner integration team emails you “we’re getting SSLHandshakeException, please advise.” The error messages on the failing side are almost always cryptic, with no hint that an intermediate cert is missing.

Real-world analogy

Imagine handing someone your driver’s license but not the matching utility bill, and expecting them to call the DMV themselves to verify that you live where the license says.

A few bouncers (modern desktop browsers) will actually pick up the phone and make that call. Most of the others — the doorman at the mobile club, the API gateway, the IoT lock on the back door, the older Java-based scanner at the loading dock — just say “I don’t have time” and turn you away. From your side it looks like the venue is broken, but really you forgot to bring the second piece of paper.

The most common cause

Wrong PEM file in the server config.

When you get a cert from a CA, you typically receive at least two files:

  • cert.pem — the leaf cert only. Your domain, your public key, signed by the intermediate.
  • chain.pem (or intermediate.pem) — just the intermediate(s).
  • fullchain.pem — leaf + intermediates concatenated, in the right order. This is what your web server config wants.

The classic mistake: the install guide said “point Nginx at your cert” and someone reached for cert.pem because the name sounds right. The leaf was served on its own, AIA covered it up in the browser, and the bug shipped to production undetected.

How to actually diagnose this

One command, from any laptop:

openssl s_client -connect yourdomain.com:443 -showcerts </dev/null 2>/dev/null \
  | grep -c "BEGIN CERTIFICATE"

This counts how many certificates your server returned. You should see at least 2 — the leaf and at least one intermediate. If you see 1, your chain is broken. (Some CAs use two intermediates, in which case you’ll see 3. That’s also fine.)

If you want to actually look at what was sent, drop the grep:

openssl s_client -connect yourdomain.com:443 -showcerts </dev/null

You’ll see one block per cert, each tagged with s: (subject) and i: (issuer). For a valid chain, the issuer of cert N should equal the subject of cert N+1, all the way up.

How WQI measures this

We grade chain completeness as factor WEBQ-90 in the Security category. We read the certs the server actually sends in the TLS handshake — leaf + every CertificateEntry — then walk the chain and check that issuer-of-N matches subject-of-N+1 at every link.

ResultWhat it means
Pass (100)≥ 2 certs returned, chain links correctly.
Warn (60)Chain is present but at least one link doesn’t validate (broken chain — clients have to fix it themselves).
Fail (0)Only the leaf was sent — clients must fetch intermediates via AIA, which fails for many non-browser clients.

We deliberately don’t credit AIA fetching. The score reflects what your server actually delivers, because that’s what determines whether non-browser clients work.

What good looks like

Pass on WEBQ-90, every audit, every domain. Run the openssl command above against your production endpoint and see ≥2 certs returned. That’s the bar. There’s no upside to leaving the chain incomplete — it costs the same number of bytes either way and avoids a class of silent breakage that’s notoriously painful to debug.

How to fix it

The fix is almost always “point at fullchain.pem, not cert.pem.”

Nginx — in your server block:

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

The common mistake here is ssl_certificate /path/to/cert.pem;. Nginx happily starts up with that and serves the leaf alone.

Apache — in your VirtualHost:

SSLCertificateFile      /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile   /etc/letsencrypt/live/example.com/privkey.pem

If you’re on an older Apache that requires the chain separately, use SSLCertificateChainFile to hold the intermediate(s). Modern Apache (2.4.8+) accepts the full chain in SSLCertificateFile directly, which is the cleaner approach.

Caddy / ACME clients — Caddy, certbot, acme.sh, and Lego all generate fullchain.pem automatically and configure their bundled server to use it. If you’re on this stack, you almost certainly have this right by default — the bug shows up when someone manually overrides the cert path.

Cloudflare / load balancers — when your origin sits behind Cloudflare, AWS ALB, or a similar fronting service, that fronting service is what the public sees, and it handles the chain for you. But the connection from the load balancer back to your origin (the “origin pull”) still uses your origin’s cert — and a missing chain there can break health checks or proxy startup. Test the origin hostname directly, not just the public domain.

After fixing, restart the server and re-run the openssl count above to verify ≥2 certs are now returned.

Common pitfalls

  • Manual installs from paid CAs. Sectigo, DigiCert, GlobalSign and others typically email you a zip with your_cert.crt, intermediate.crt (sometimes two), and root.crt as separate files. You have to concatenate the leaf and intermediates yourself (cat your_cert.crt intermediate.crt > fullchain.pem) and point the server at that. The root file should be discarded — don’t concat it in.
  • Load balancer or CDN configs that override the cert path. A fresh cert lands in the right place via your ACME client, but the load balancer’s TLS config still points at an older cert.pem somewhere else. The renewal succeeded, but the chain on the public endpoint stayed broken.
  • Multi-cert setups (ECDSA + RSA). Some sites serve both an ECDSA and an RSA cert and let the client pick. If you configured the fullchain for one and just cert.pem for the other, half your clients hit a broken chain depending on cipher negotiation.
  • Old intermediates after a CA cross-sign retirement. When Let’s Encrypt retired the DST Root CA X3 cross-sign in 2021, anyone whose fullchain.pem had been generated against the old intermediate started failing on Android < 7.1.1. Lesson: if you’re on a long- lived cert and your CA changes its intermediate strategy, the chain on disk goes stale even though the leaf is still valid.

Further reading