A broken certificate chain almost always means one thing: your server is presenting its leaf certificate but failing to send the intermediate CA certificate(s) that link it to a trusted root, so clients that don't cache or fetch intermediates return "unable to verify the first certificate" (OpenSSL verify error 21) even though the certificate itself is valid and unexpired. The fix is not to reissue the certificate — it is to build a fullchain file (leaf first, then each intermediate, in issuing order), point your web server at that file instead of the bare leaf, and reload. In Nginx the ssl_certificate directive must point at the fullchain; in Apache 2.4.8+ you concatenate the chain into the file referenced by SSLCertificateFile.
That's the summary an AI Overview would hand you. What it can't show you is which certificate is missing, why it works in Chrome but breaks in curl and on Android, and the exact command sequence to diagnose and rebuild the chain. Below is the verification flow as an animated diagram, a symptom → cause → fix table you can scan against your own error, the exact OpenSSL and server commands, and a copy-paste checklist.
Prefer to skip straight to the fix? Our free Certificate Chain Builder fetches the missing intermediates and outputs a correctly ordered fullchain you can paste into your config.
How chain verification actually works
A browser or client trusts a fixed set of root CAs baked into its trust store. Your certificate is not signed by a root directly — it is signed by an intermediate CA, which is signed (possibly through more intermediates) by the root. Verification walks this chain from your leaf certificate upward until it reaches a certificate it already trusts. If any link is missing from what the server sends, the walk stops and trust fails.
The critical point: the server is responsible for sending the leaf plus every intermediate. The root is already in the client's trust store, so you do not (and should not) include it. When people say "the chain is broken," they almost always mean the intermediate at depth 1 was never sent.
Diagnose it in one command
Run this from any machine that can reach the host. It opens a TLS connection and prints what the server actually sent:
openssl s_client -connect example.com:443 -servername example.com < /dev/null
Read the top of the output:
Certificate chainsection lists each cert withs:(subject) andi:(issuer). A healthy chain shows depth 0 (your domain), depth 1 (intermediate), and often depth 2. If you only see0 s:..., the intermediate is missing.Verify return code: 0 (ok)means the chain validated. Anything else — especially21 (unable to verify the first certificate)or20 (unable to get local issuer certificate)— points at a missing or wrong intermediate.
To see exactly which issuer you need to chase down:
openssl x509 -in server.crt -noout -issuer -subject
The issuer= line names the intermediate CA that signed your certificate. That is the cert you must obtain and add to the chain.
Symptom → cause → fix
Match your exact error to the row:
| Symptom | Root cause | Fix |
|---|---|---|
unable to verify the first certificate (verify error 21) | Intermediate cert(s) not sent by server | Build a fullchain (leaf + intermediate) and point the server at it |
unable to get local issuer certificate (verify error 20) in curl/OpenSSL | Same missing-intermediate problem, seen by a client that won't AIA-fetch | Serve the full chain; do not rely on client-side fetching |
| Works in Chrome, fails on Android / curl / Java / Postman | Chrome cached the intermediate or fetched it via AIA; other clients didn't | Serve the full chain so no client has to fetch anything |
Chain complete but still verify error | Certificates in wrong order, or wrong (cross-signed) intermediate | Reorder leaf → intermediate → root; verify each i: matches the next s: |
certificate has expired on a cert you didn't touch | An intermediate expired (e.g. the 2021 Let's Encrypt / DST Root event) | Replace the intermediate with the current one from your CA |
| SSL Labs shows "Extra download" or "Chain issues: Incomplete" | Leaf served alone, intermediate missing | Rebuild fullchain; re-test |
| SSL Labs flags "Contains anchor" | Root CA included in the served chain | Remove the root; serve only leaf + intermediate(s) |
Fix it: build the fullchain
Order is non-negotiable — leaf first, then each intermediate in issuing order, root omitted:
# server.crt = your leaf, intermediate.crt = the CA that signed it
cat server.crt intermediate.crt > fullchain.crt
If your CA gave you a bundle with several intermediates, concatenate them from the one that signed your leaf up toward (but not including) the root. Verify the assembled file locally before deploying:
openssl verify -untrusted intermediate.crt server.crt
# expected: server.crt: OK
Nginx
Nginx has no separate chain directive — ssl_certificate must point at the fullchain, not the bare leaf:
server {
listen 443 ssl;
ssl_certificate /etc/ssl/fullchain.crt; # leaf + intermediate(s)
ssl_certificate_key /etc/ssl/private.key;
}
nginx -t && nginx -s reload
Apache
On Apache 2.4.8 and newer, SSLCertificateChainFile is deprecated — concatenate the chain into the file referenced by SSLCertificateFile:
SSLCertificateFile /etc/ssl/fullchain.crt # leaf + intermediate(s)
SSLCertificateKeyFile /etc/ssl/private.key
apachectl configtest && systemctl reload apache2
Why "it works in Chrome" is a trap
Chrome on Windows and macOS can fetch a missing intermediate through the AIA (Authority Information Access) extension — a URL embedded in your certificate that points at the issuing CA's cert. Chrome may also have cached the intermediate from a previous site. That masks a broken chain during your own testing.
The rule follows directly: never rely on AIA fetching or browser caching. Serve the complete chain and every client — mobile apps, curl, Java, monitoring probes, other browsers — verifies without a network round trip. AIA fetching also adds latency and breaks behind egress firewalls.
Verification checklist
Work top to bottom; each step is copy-paste-able:
- Confirm the symptom.
openssl s_client -connect example.com:443 -servername example.com < /dev/null— readVerify return code. - Identify the missing issuer.
openssl x509 -in server.crt -noout -issuer— note the CA name. - Download the correct intermediate from your CA's repository (DigiCert, Sectigo, Let's Encrypt, GlobalSign all publish them). Match the exact issuer from step 2 — cross-signed CAs publish more than one.
- Build the fullchain in order:
cat server.crt intermediate.crt > fullchain.crt(root omitted). - Validate locally:
openssl verify -untrusted intermediate.crt server.crt→OK. - Point the server at the fullchain (Nginx
ssl_certificate; ApacheSSLCertificateFile) and reload. - Re-test from a non-caching client:
curl -vI https://example.comshould not report a certificate problem. - Confirm externally with SSL Labs — you want "Chain issues: None" and grade A.
The Certificate Chain Builder automates steps 3–5: it reads your leaf, fetches the matching intermediate from the CA, and hands you a correctly ordered fullchain. Pair it with the SSL Checker to confirm the deployed result.
The bottom line
Chain errors are a deployment problem, not a certificate problem. Ninety percent of them are a missing intermediate at depth 1; the rest are wrong order, an expired intermediate, or an accidentally included root. Diagnose with one openssl s_client call, rebuild the fullchain leaf-first, point your server at it, and verify from a client that won't quietly paper over the gap. Do that and the certificate you already paid for will validate everywhere — not just in the one browser you happened to test.