The worst certificate problems are the ones that look fine to you. You open the site, the padlock is there, checkout works. Meanwhile an API client rejects the connection, a mobile app throws an error, and an old payment terminal cannot finish a handshake. The certificate itself is valid and unexpired. The problem is the chain, and the reason it hides from you is the most useful thing to understand about it.

What the Chain Is and the Order It Must Follow

A leaf certificate does not stand alone. A client only trusts it if it can build a path from that leaf up to a root already in its trust store, and between the two sit one or more intermediates. Your server sends the leaf plus every intermediate needed to reach a root. The client supplies the root from its own store. Roots stay offline and rarely change; intermediates do the daily signing and rotate often.

Order is part of the contract. The server must send the leaf first, then each intermediate moving toward the root, with the root itself omitted because the client already has it. Here is a correct chain, from openssl s_client against a public test host. The -showcerts PEM blocks and the key and validity lines are trimmed for readability; the subject (s:) and issuer (i:) lines are what matter.

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

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

    Verify return code: 0 (ok)

Read it as a ladder. Cert 0 is the leaf, issued by R13. Cert 1 is R13, the intermediate, issued by ISRG Root X1. The root is not in the list because it lives in the client's store, and Verify return code: 0 (ok) is the line you are looking for. Two flags in that command are not optional. Without -servername, an older OpenSSL sends no SNI and you inspect the default vhost on a shared IP instead of the site you meant. Without </dev/null, the command holds the connection open and waits on your keyboard.

Why a Broken Chain Still Passes in Your Browser

Browsers repair broken chains, quietly, and this is what sends people chasing the wrong thing. Chrome, Edge, and Safari read the URL in the leaf's Authority Information Access extension and fetch the missing intermediate straight from the CA while they build the path. Firefox does not fetch; it ships a preloaded bundle of common intermediates and fills the gap from that. Either way you get a padlock, and you get it on a fresh install and in a new incognito window, on a machine that has never seen your site.

That last point trips up the debugging. There is no leaf-certificate cache in the browser to clear, so incognito changes nothing: the intermediate is being fetched or preloaded on demand, every time. A minimal Docker image, a Java or Python client, an Android app, or a payment terminal has none of that machinery. It uses exactly what your server sends plus its root store. Omit the intermediate and every one of those clients fails while every browser you own succeeds.

Reading the Chain From Outside

Point the same command at a host that sends the leaf and nothing else, and the failure is plain:

$ openssl s_client -connect incomplete-chain.badssl.com:443 \
    -servername incomplete-chain.badssl.com -showcerts </dev/null

Certificate chain
 0 s:CN=*.badssl.com
   i:C=US, O=Let's Encrypt, CN=R13

Verification error: unable to verify the first certificate
    Verify return code: 21 (unable to verify the first certificate)

The chain has one entry where the good one had two. The server never sent the R13 intermediate, so openssl cannot connect the leaf to anything trusted and reports code 21. A real client phrases the same gap differently. Here is curl against that host, which is the error you will actually see in a failing job or app log:

$ curl https://incomplete-chain.badssl.com/
curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)

Same missing intermediate, two different messages: unable to verify the first certificate from openssl, unable to get local issuer certificate from curl and most libraries built on OpenSSL. When either one lands in a ticket, the intermediate is the first thing to check.

When the Order Is Wrong

Some stacks are strict about ordering and reject a chain whose certs arrive out of sequence, even when every cert is present. You can catch this before deploying by inspecting the bundle file itself. Split it and print the subject of each cert in the order it appears:

$ awk '/BEGIN CERT/{n++} {print > "c" n ".pem"}' fullchain.pem
$ for c in c*.pem; do openssl x509 -in "$c" -noout -subject; done

A correct bundle lists the leaf first:

subject=CN = shop.example.com
subject=C = CA, O = Example Root CA, CN = Example Intermediate R3

A scrambled one, usually from concatenating files in the wrong order, puts an intermediate at position 0:

subject=C = CA, O = Example Root CA, CN = Example Intermediate R3
subject=CN = shop.example.com

If the leaf is not the first cert in the file, fix the concatenation order before it reaches a client that cares.

The Usual Cause: An Nginx Config Pointed at the Wrong File

Most missing-intermediate outages trace back to one line. Nginx serves whatever is in ssl_certificate, in file order, and if you point it at the leaf-only cert your CA handed you, that is all it sends. It must point at the full chain:

# wrong: sends the leaf only, browsers hide it, clients break
ssl_certificate     /etc/nginx/ssl/cert.pem;

# right: leaf plus intermediates, in order
ssl_certificate     /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;

Let's Encrypt writes both cert.pem and fullchain.pem into the same directory, which is exactly why this is so easy to get wrong. The goodtls.com Nginx guide has the full server block. Reload, then re-run the openssl check from outside to confirm the second cert now shows up.

Matching the Symptom to the Cause

Broken chains announce themselves through a recognizable set of symptoms. What you observe usually points straight at what is wrong.

What you see What it usually means
Padlock in your browser, hard TLS error in curl or an API client Missing intermediate; the browser fetches it over AIA and the client cannot
Works on your desktop, fails on a phone or a fresh VM Same missing intermediate; that client has no preloaded or cached intermediate to fall back on
A picky client (some Java, Go, mobile stacks) rejects a chain that others accept Certs sent out of order, leaf not first
Worked for months, then broke with no deploy on your side CA rotated an intermediate or retired a root; your bundle still ships the old path
Passes for some visitors, fails for others on the same domain One IP behind the DNS record serves a good chain, another serves a stale or leaf-only one

The last row is the mean one. Behind a single hostname you may have several IPs, from a load balancer, a CDN, or round-robin DNS, and a checker that resolves to one address tells you nothing about the others.

Catching It Before a Client Does

The Mr.DNS SSL certificate checker prints the chain a host serves so you can eyeball whether the intermediates are present, which is a fast first look after any deployment. Make that check a standing step in every deployment: our SSL certificate renewal checklist folds outside-in chain verification into its post-renewal steps.

A one-time check still misses the failures that arrive on their own schedule. A CA rotating an intermediate or retiring a root can break a chain you have not touched in months, and it will break for clients long before it breaks for you. Generator Labs certificate monitoring resolves every IP behind a DNS record and runs a real chain verification against each one, so a path that stops reaching a trusted root on a single backend raises an alert instead of becoming an outage. Start monitoring your chains before a client finds the gap first.

Back to Blog