Incomplete certificate chain: why your site works in Chrome but fails in curl
Your site loads fine in the browser. Then a customer's integration fails with unable to get local issuer certificate, the Android app throws CertPathValidatorException, or a webhook provider reports an SSL error. The certificate is valid and not expired. What's going on?
Almost always: the server isn't sending the intermediate certificate.
How a certificate chain works
Your certificate isn't signed directly by a root that clients trust. It's signed by an intermediate CA, which is signed by the root:
example.com ← your certificate (leaf)
└─ R11 / Sectigo DV / … ← intermediate, YOU must send this
└─ ISRG Root X1 / … ← root, already in the client's trust store
Clients ship with roots, not intermediates. So during the handshake the server has to send the leaf plus every intermediate. If it sends only the leaf, the client can't connect it to a root.
Why browsers don't notice
Desktop browsers are forgiving. Chrome and Safari can download the missing intermediate from the URL in the certificate's Authority Information Access (AIA) field, and Firefox ships a list of known intermediates. They also cache intermediates from other sites you've visited. The page works, and the bug ships to production.
Most other clients do none of that: curl, wget, OpenSSL, Python requests, Java, Go, Node.js, many Android versions, IoT devices, payment and webhook providers. For them the chain is simply untrusted.
How to check
Run the domain through sslverify.net. If the server's chain alone doesn't verify but adding the AIA-downloaded intermediate does, you'll see “Certificate chain is incomplete” and the grade is capped at B. From a terminal:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
Count the -----BEGIN CERTIFICATE----- blocks. One block usually means the intermediate is missing.
How to fix it
Serve the full chain file instead of the leaf alone:
- Let's Encrypt / certbot: use
fullchain.pem, notcert.pem.# nginx ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; - Commercial CAs: concatenate your certificate followed by the CA bundle they gave you, in that order:
cat example.com.crt ca-bundle.crt > fullchain.crt - Apache 2.4.8+: point
SSLCertificateFileto the full-chain file. - Load balancers and CDNs: upload the intermediate into the “certificate chain” field.
Two related mistakes to avoid: putting the certificates in the wrong order (each one must be followed by its issuer), and adding the root. The root is harmless but useless, since clients already have it and it only makes every handshake larger. sslverify.net flags both.
Prevent it from coming back
Chains break most often during manual renewals and certificate swaps. Automate renewal, deploy the full-chain file, and re-check after every change.