All articles
·9 min read

The SSL certificate chain explained (and how to debug chain errors)

A certificate that your browser trusts on your laptop can still fail in production, on a mobile client, or inside a Java service. The usual culprit isn't an expired or invalid cert — it's a broken ssl certificate chain, where the server fails to send the intermediate certificate that links your cert to a trusted root. This post explains how the chain of trust works, why a "valid" certificate can still be rejected, and how to inspect and fix it with openssl.

What the SSL certificate chain actually is

TLS trust is hierarchical. Three kinds of certificates form the path:

  • Leaf (end-entity) certificate — the cert issued for your domain, e.g. example.com. This is the one you install on your server.
  • Intermediate certificate(s) — issued by the Certificate Authority's root to one or more intermediate CAs. Your leaf is signed by an intermediate, not by the root directly. There can be more than one intermediate in the path.
  • Root certificate — the self-signed anchor that lives in the client's trust store (the OS or browser). Roots are never served by your web server; the client already has them.

Each certificate is signed by the private key of the one above it. The intermediate's public key verifies the leaf's signature; the root's public key verifies the intermediate's signature. The root verifies itself (it's self-signed).

Root CA            (in client trust store, self-signed)
   └── signs Intermediate CA
          └── signs Leaf (your domain)

How a client builds a path to a trusted root

When a client connects, the server presents a list of certificates. The client's job is to build a path from your leaf up to a root it already trusts:

  1. Take the leaf and read its issuer field.
  2. Find a certificate whose subject matches that issuer (the intermediate).
  3. Verify the leaf's signature using the intermediate's public key.
  4. Repeat up the chain until it reaches a certificate that chains to a root in its local trust store.
  5. Along the way, check validity dates, name constraints, and revocation.

The client only has the root locally. It relies on the server to supply the intermediate(s) so the path can be completed. That single dependency is the source of most chain errors.

The most common production failure: a missing intermediate

Here's the trap. Your certificate is genuinely valid — correct domain, not expired, issued by a real CA. You test it in Chrome on your machine and it's green. But other clients reject it.

This happens when the server sends only the leaf and omits the intermediate. The client can't find the certificate that signed your leaf, so it can't build a path to any trusted root. The errors look like:

  • OpenSSL / Node.js: unable to verify the first certificate or UNABLE_TO_VERIFY_LEAF_SIGNATURE
  • Chrome and Chromium-based browsers: NET::ERR_CERT_AUTHORITY_INVALID

We cover both of these in depth at /errors/unable_to_verify_leaf_signature and /errors/err_cert_authority_invalid.

Why does it pass on your laptop but fail elsewhere? Some clients cache intermediates they've seen before, and some (notably recent Chrome/Firefox builds) can fetch a missing intermediate using the Authority Information Access (AIA) extension in the leaf. Many clients do neither: curl, most OpenSSL-based tooling, Java's default trust manager, and a lot of mobile and IoT stacks expect the server to send the full chain. Relying on AIA fetching is unsafe — it's a fallback, not a guarantee.

Inspect what the server actually sends

Don't guess. Look at the exact certificates on the wire with openssl s_client. The -showcerts flag prints every certificate the server presents:

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

-servername sends SNI, which matters when one IP hosts multiple certificates. Read the output top to bottom — it lists the chain in the order the server sends it:

Certificate chain
 0 s:CN = example.com
   i:C = US, O = Let's Encrypt, CN = R10
 1 s:C = US, O = Let's Encrypt, CN = R10
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1

s: is the subject, i: is the issuer. The leaf is index 0. Its issuer (R10) should appear as the subject of index 1, the intermediate. If index 0 is the only entry, the intermediate is missing — that's your bug.

You can also ask OpenSSL to verify the chain directly. A healthy chain ends with:

Verify return code: 0 (ok)

A broken one returns something like Verify return code: 21 (unable to verify the first certificate).

To inspect a single PEM file on disk without connecting:

openssl x509 -in cert.pem -noout -subject -issuer -dates

For a quick remote view of the dates and chain without a local terminal, you can use our /tools/ssl-check checker, which shows the certificates a host serves and flags a missing intermediate.

The fix: serve the full chain

The fix is almost always "serve the full chain, not just the leaf." When you obtain a certificate, the CA gives you the intermediate(s) too. Configure your server to present leaf + intermediate(s), in that order.

If you use Certbot (Let's Encrypt), it writes the files you need into /etc/letsencrypt/live/<domain>/:

  • cert.pem — the leaf only. Do not use this alone.
  • chain.pem — the intermediate(s) only.
  • fullchain.pem — leaf plus intermediate(s). This is what you almost always want.
  • privkey.pem — your private key.

For Nginx, point ssl_certificate at fullchain.pem, not cert.pem:

server {
    listen 443 ssl;
    server_name example.com;

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

Then reload:

nginx -t && nginx -s reload

Our full /guides/nginx walkthrough covers TLS configuration, OCSP stapling, and HTTP-to-HTTPS redirects. If you build the chain by hand, concatenate leaf first, then intermediate:

cat cert.pem intermediate.pem > fullchain.pem

Order matters: the leaf must come first, intermediates after, each one signing the previous. Don't append the root — it's redundant and clients ignore it.

Cross-signing and root expiry

Two wrinkles worth knowing about:

  • Cross-signing. A newer root sometimes isn't yet trusted by older devices. To bridge the gap, a CA can have the new intermediate cross-signed by an older, widely-trusted root, so there are effectively two valid paths. Which intermediate you serve affects which clients succeed. When in doubt, serve the chain your CA recommends for the broadest compatibility.
  • Root expiry. Roots have expiry dates too, and old devices that stopped receiving trust-store updates can suddenly reject a chain when a root they relied on expires — even though your leaf is fine. The Let's Encrypt DST Root CA X3 expiry in 2021 broke many older Android and OpenSSL clients for exactly this reason. The fix is to serve a chain that terminates at a root those clients still trust, or accept that very old devices are out of scope.

Monitor it automatically

Chain problems are silent until a client complains, and a renewed cert can quietly drop its intermediate or chain to a soon-to-expire root. SSLNudge checks your certificates every day — leaf expiry, the full chain a host actually serves, and missing or misordered intermediates — and emails you well before anything expires, so you fix it on your schedule instead of during an outage.

Stop tracking expiry dates by hand

SSLNudge checks your certificates daily and alerts you before they expire.

Start free