Incomplete SSL Certificate Chain: How to Diagnose and Fix It
Incomplete SSL Certificate Chain: How to Diagnose and Fix It
An incomplete certificate chain is when the server returns only its leaf cert without intermediates. Desktop browsers can often fetch missing intermediates from cache, but mobile clients, older libraries, curl, Java apps, and API документацию clients cannot — and fail with “unable to get local issuer certificate”. This guide covers diagnosis and assembling a correct fullchain.
Why the chain matters
The trust chain is the hierarchy from your leaf cert up to a root CA the client trusts:
Root CA (in the client's trust store)
└─ Intermediate CA 1
└─ Intermediate CA 2 (optional)
└─ Leaf (your example.com cert)
The client must build a path from your leaf to a trusted root. Missing intermediates — no path. Don't send the root in the chain (not required; wastes bytes).
Symptoms of a broken chain
- Desktop Chrome works, iPhone Safari doesn't.
- curl fails with “unable to get local issuer certificate”.
- Java:
PKIX path building failed. - Python requests:
[SSL: CERTIFICATE_VERIFY_FAILED]. - Telegram bot rejects the webhook even though the cert is valid.
- SSL Labs grade B with “Chain issues: Incomplete”.
Diagnosis
First check:
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null 2>/dev/null \
| grep "^-\|^ *s:\|^ *i:"
Correct output has at least two s: / i: pairs:
s:CN=example.com i:C=US, O=Let's Encrypt, CN=R3 s:C=US, O=Let's Encrypt, CN=R3 i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
Only one block — chain is broken. Also try the enterno.io SSL Checker or SSL Labs — they'll flag chain issues.
Fix: assemble fullchain.pem
Let's Encrypt makes this trivial — use fullchain.pem, it already contains leaf + intermediate:
# Correct
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
# Wrong (leaf only)
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
Commercial CAs ship an archive with multiple files: cert.crt, intermediate.crt, sometimes root.crt. Concatenate leaf and intermediate in the right order:
cat cert.crt intermediate.crt > fullchain.crt
# Your leaf first, intermediate second
# DO NOT include the root
With multiple intermediates, the order is: leaf → intermediate nearest to leaf → next intermediate → etc. Wrong order — clients still fail.
nginx and Apache config
nginx:
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
Apache:
SSLCertificateFile /etc/apache2/certs/cert.crt
SSLCertificateKeyFile /etc/apache2/certs/privkey.key
SSLCertificateChainFile /etc/apache2/certs/intermediate.crt
# Or (Apache 2.4.8+) leaf+intermediate in one file:
SSLCertificateFile /etc/apache2/certs/fullchain.pem
After changes: nginx -t && systemctl reload nginx or apachectl configtest && systemctl reload apache2.
Where to get intermediates if you've lost them
- CA website: Let's Encrypt — letsencrypt.org/certificates, Sectigo, DigiCert — their own pages.
- AIA (Authority Information Access): the cert contains an URL to the intermediate:
openssl x509 -in cert.crt -noout -text | grep "CA Issuers" # CA Issuers - URI:http://r3.i.lencr.org/ curl http://r3.i.lencr.org/ | openssl x509 -inform DER -out intermediate.pem - crt.sh: look up your cert on crt.sh and follow the issuer link.
Automation and prevention
- Use certbot or acme.sh — they always produce
fullchain.pem. - In CI, add
openssl verify -untrusted intermediate.pem leaf.pem. - After deploy, smoke-test with curl (no
-k) from a clean Docker image (e.g.alpine:latest) — no local trust cache. - Use Enterno.io Monitors — it verifies chain integrity on every check and alerts on post-renewal breakage.
Related issues
- Handshake failed — may be cipher suites or SNI.
- Expired certificate — check expiry of every chain node (intermediates expire too).
- Old root CAs on the client — ISRG Root X1 shipped in Android only from 7.1.1; older devices need cross-signs.
Frequently asked questions
Should the root CA be in fullchain.pem?
No. The client already has it in the trust store. Sending it just bloats the handshake.
What about cross-signed intermediates for old Android?
Use Let's Encrypt's “long chain” where the intermediate is signed by the old DST Root CA X3. Certbot defaults to the short ISRG Root X1 chain — for Android < 7.1.1 force --preferred-chain "ISRG Root X1".
Why does Chrome work but Python requests doesn't?
Chrome can fetch intermediates via AIA from its cache. Python requests uses a certifi bundle containing only roots. You need a full chain on the server.
Fastest way to test from mobile?
Use an online service that doesn't share your desktop cache. enterno.io SSL Checker validates from its own environment — equivalent to a “clean” client.
Conclusion
Incomplete chain is a silent issue: invisible on desktop, broken on mobile and API integrations. The fix: build a correct fullchain.pem once and add continuous chain monitoring. enterno.io SSL Checker validates the chain in 15 seconds, and Monitors repeats every 5 minutes 24/7. See also cert date errors and handshake failed.
Path validation — RFC 5280, §6. Let's Encrypt chains — letsencrypt.org/certificates. Test — SSL Labs.
Check your website right now
Check now →