Why “SSL Chain Incomplete” Breaks Your Secure Site
You set up HTTPS, everything looks green on your laptop, but users on Android, older browsers, or automated clients complain the site is “not secure.” Or your CI checks fail with “unable to get local issuer certificate.” Nine times out of ten, the culprit is an incomplete SSL certificate chain.
An “SSL chain incomplete” issue occurs when your server doesn’t present the full set of certificates that link your site’s certificate (the leaf) to a trusted root certificate authority (CA). While some clients can fetch missing intermediates from the network, many do not. If your server doesn’t send the right intermediates, some users will see TLS failures.
This guide walks you through what an SSL/TLS chain is, how to reliably diagnose incomplete chains, and how to fix them across common stacks (Nginx, Apache, HAProxy, Node.js, Java/Tomcat, IIS, and popular cloud/CDN setups). You’ll get clear commands, config snippets, and a battle-tested process.
The TLS Chain of Trust in 90 Seconds
- Your site has a leaf certificate (for your domain).
- The leaf is signed by an intermediate CA certificate.
- That intermediate is signed by a root CA certificate, which is pre-installed in operating systems and browsers.
Clients must be able to build a chain: Leaf → Intermediate(s) → Trusted Root.
Servers are expected to send:
- The leaf certificate, and
- All required intermediate certificate(s).
Servers should not send the root certificate. Roots are already in client trust stores; including the root can sometimes cause issues with certain clients.
If the server omits any required intermediate, many clients can’t build the chain and the handshake fails.
Symptoms and Error Messages You’ll See
Different tools phrase the same underlying problem differently. Common signals include:
- Browser warnings such as “This site’s security certificate is not trusted” or “SEC_ERROR_UNKNOWN_ISSUER”.
- Mobile/legacy device errors when desktop browsers seem fine.
- cURL:
- SSL certificate problem: unable to get local issuer certificate
- OpenSSL:
- verify error:num=20:unable to get local issuer certificate
- verify error:num=21:unable to verify the first certificate
- SSL Labs results:
- Chain issues: Incomplete, or “This server’s certificate chain is incomplete”
- Java clients:
- PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
If you see any of these, it’s time to inspect the served chain.
Common Causes of Incomplete Chains
- You uploaded only the leaf certificate (your domain cert) to the server, but not the intermediate(s).
- The chain file is present but in the wrong order.
- You mixed certificate types (e.g., ECDSA leaf with RSA intermediate, or vice versa).
- You used a deprecated or expired cross-signed chain (e.g., legacy Let’s Encrypt DST Root CA X3 issues).
- You configured one vhost correctly but others are missing SNI-based configuration and serve the wrong bundle.
- Your reverse proxy or load balancer has a different chain than your origin.
- Your PFX/PKCS#12 export didn’t include the intermediate(s).
Step-by-Step Diagnosis Process
Follow this sequence to get a precise, reproducible understanding of the problem.
1) Reproduce with command-line tools
Run OpenSSL against your host to see what the server actually sends:
openssl s_client -connect example.com:443 -servername example.com -showcerts -verify_return_error </dev/null
Pay attention to:
- “Certificate chain” section: Are intermediates present?
- “Verify return code:” Ideally 0 (ok). Problematic codes often 20 or 21.
If the chain is incomplete, you might see:
Verify return code: 21 (unable to verify the first certificate)
Test with cURL (it uses your system trust store):
curl -Iv https://example.com
Look for:
SSL certificate problem: unable to get local issuer certificate
Verify with SSL Labs for a second opinion and global perspective: run a scan and check Chain issues.
2) Inspect the certificate chain as presented by the server
Copy the certificates printed by s_client and save them locally to inspect:
- The first certificate should be your leaf (CN/SAN includes your domain).
- Following certificates should be the intermediate(s).
- There should be no self-signed root sent by the server.
You can also separate and verify locally:
# leaf.pem, intermediate.pem downloaded from your CA
openssl verify -untrusted intermediate.pem leaf.pem
A good result is:
leaf.pem: OK
If not OK, you’re missing the correct intermediate or it’s the wrong one.
3) Fetch the correct intermediates from your CA
Your CA will provide:
- Your domain certificate (leaf)
- One or more intermediate certificates
- Sometimes a “fullchain” file (leaf + intermediates concatenated)
Download the official intermediate(s) that your issuance references. Don’t guess or re-use an old intermediate; they’re specific to the issuing hierarchy.
4) Compare the chain your server sends vs. the CA’s intended chain
- If anything is missing, add it.
- If the order is wrong, re-order to Leaf → Intermediate 1 → Intermediate 2 → … (no root).
- If the intermediates don’t match the CA’s, replace them.
5) Retest after changes
Repeat s_client, curl, and SSL Labs scans. Aim for:
- s_client verify return code: 0 (ok)
- curl successful handshake
- SSL Labs chain: Complete
Fixing Incomplete Chains on Popular Stacks
Below are practical, copy-pasteable fixes for the most common server setups. The key principle: configure your server to present both the leaf and intermediate certificate(s) in the correct order.
Nginx
Nginx expects ssl_certificate to point to a file that contains the leaf certificate followed by intermediates, in PEM format:
-----BEGIN CERTIFICATE----- # leaf
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE----- # intermediate 1
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE----- # intermediate 2 (if any)
...
-----END CERTIFICATE-----
Config:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/example/fullchain.pem; # leaf + intermediates
ssl_certificate_key /etc/ssl/example/privkey.pem;
# For OCSP stapling (use the chain without the leaf if available)
ssl_trusted_certificate /etc/ssl/example/chain.pem;
# other TLS settings...
}
Reload Nginx and retest:
nginx -t && systemctl reload nginx
openssl s_client -connect example.com:443 -servername example.com -showcerts -verify_return_error </dev/null
Apache (httpd)
For Apache 2.4.8+, include the intermediates in SSLCertificateFile (the chain file directive is deprecated):
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/ssl/example/fullchain.pem # leaf + intermediates
SSLCertificateKeyFile /etc/ssl/example/privkey.pem
# Apache 2.4.8+ typically ignores SSLCertificateChainFile
# If older Apache:
# SSLCertificateFile /etc/ssl/example/cert.pem (leaf only)
# SSLCertificateChainFile /etc/ssl/example/chain.pem (intermediates)
</VirtualHost>
Restart and test:
apachectl configtest && systemctl reload httpd
HAProxy
HAProxy expects a single PEM bundle: private key, leaf cert, then intermediates.
cat /etc/ssl/example/privkey.pem \
/etc/ssl/example/cert.pem \
/etc/ssl/example/chain.pem \
> /etc/ssl/example/bundle.pem
In haproxy.cfg:
bind :443 ssl crt /etc/ssl/example/bundle.pem alpn h2,http/1.1
Reload and test:
systemctl reload haproxy
Node.js (HTTPS/TLS servers, Express, etc.)
Node can accept:
cert: the leaf certificate (can include intermediates)ca: the intermediate chain (if not included in cert)
Example:
const fs = require('fs');
const https = require('https');
const options = {
key: fs.readFileSync('/etc/ssl/example/privkey.pem'),
cert: fs.readFileSync('/etc/ssl/example/cert.pem'), // leaf
ca: [
fs.readFileSync('/etc/ssl/example/intermediate1.pem'),
fs.readFileSync('/etc/ssl/example/intermediate2.pem'),
],
};
https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('Hello, TLS!\n');
}).listen(443);
Alternatively, set cert to a concatenation of leaf + intermediates.
Be careful with ECDSA vs RSA: the intermediates must match the leaf’s key type.
Java/Tomcat/Spring Boot (PKCS#12 keystore)
Create a PKCS#12 that includes the leaf and intermediates:
openssl pkcs12 -export \
-inkey privkey.pem \
-in cert.pem \
-certfile chain.pem \
-name tomcat \
-out keystore.p12
Tomcat server.xml:
<Connector
port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
SSLEnabled="true"
keystoreFile="/etc/ssl/example/keystore.p12"
keystorePass="changeit"
keystoreType="PKCS12"
/>
Spring Boot application.properties:
server.ssl.enabled=true
server.ssl.key-store=/etc/ssl/example/keystore.p12
server.ssl.key-store-type=PKCS12
server.ssl.key-store-password=changeit
Restart the app and verify with OpenSSL. If you still see chain issues, ensure your P12 actually includes the intermediates; re-create the P12 using -certfile chain.pem.
IIS/Windows
- Import a PFX (.pfx) that includes the private key, leaf, and intermediates.
- Use the Certificates MMC:
- Import intermediates into “Intermediate Certification Authorities”.
- Import the PFX for your site into “Personal”.
- In IIS Manager, bind the site to the correct certificate under “Bindings…”.
A quick way: build the PFX with OpenSSL:
openssl pkcs12 -export -inkey privkey.pem -in cert.pem -certfile chain.pem -out bundle.pfx
Then import bundle.pfx in Windows.
AWS and CDNs
- AWS ACM: If you request certificates via ACM, AWS manages the chain. If you upload an external cert, upload the certificate (leaf), the private key, and the full chain. ACM validates and serves a complete chain on ALB/ELB/CloudFront.
- ELB/ALB without ACM: Provide a proper PEM bundle (leaf + chain). Test with s_client.
- CloudFront: If you use ACM in us-east-1 for custom domains, CloudFront handles the chain automatically.
- Cloudflare: Cloudflare serves the edge cert chain. For origin pulls over HTTPS, ensure your origin also serves a complete chain (or use Cloudflare Origin CA certs with Cloudflare trust on the edge only).
Traefik and Caddy
- Traefik: Provide
certFilecontaining leaf + intermediates, andkeyFilefor private key. With Let’s Encrypt via Traefik, it typically manages the chain automatically. - Caddy: With automatic HTTPS, Caddy serves the proper chain. For custom certs, combine leaf + intermediates in the
certificatefile.
Advanced Scenarios and Gotchas
1) Let’s Encrypt cross-sign and legacy device quirks
Let’s Encrypt historically offered alternate chains, including ones cross-signed by IdenTrust’s DST Root CA X3 (which expired). The modern default is to chain to ISRG Root X1, trusted by current platforms. If your clients include very old Android devices, you may need special consideration, but do not serve an expired cross-chain.
- With Certbot, ensure you’re using the default modern chain. Check
fullchain.pemcontent and issuance chain with:openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout | sed -n '1,/$/p' - Avoid pinning to an old or incompatible chain. If you must target legacy platforms, consider a CA and chain compatible with your user base.
2) RSA and ECDSA dual-certificate setups
Offering only ECDSA can break very old clients. Offering only RSA can be slower. Nginx supports serving both:
ssl_certificate /etc/ssl/example-rsa/fullchain.pem;
ssl_certificate_key /etc/ssl/example-rsa/privkey.pem;
ssl_certificate /etc/ssl/example-ecdsa/fullchain.pem;
ssl_certificate_key /etc/ssl/example-ecdsa/privkey.pem;
Each fullchain must pair with the correct intermediates for that key type. Test both paths:
# Prefer ECDSA sigalgs
openssl s_client -connect example.com:443 -servername example.com -cipher ECDHE-ECDSA-AES128-GCM-SHA256
# Force RSA
openssl s_client -connect example.com:443 -servername example.com -cipher ECDHE-RSA-AES128-GCM-SHA256
3) SNI mismatches
If you host multiple domains, each SNI host must serve the correct chain. Verify each:
for host in example.com api.example.com; do
echo "Testing $host"
openssl s_client -connect yourserver:443 -servername "$host" -showcerts -verify_return_error </dev/null | grep -E "Verify return code|subject=|issuer="
done
4) Reverse proxies and origin mismatch
Your CDN or load balancer might be correct, but the origin is not—or vice versa. Check both the edge and the origin endpoints:
- DNS directly to origin (temporary etc/hosts entry or direct IP with SNI override).
- CDN domain separately.
5) Including the root certificate by mistake
Some admins append the root to the chain. While many clients ignore it, others might fail chain building or incur extra path-building work. Best practice: do not include the root in fullchain.pem served to clients.
6) PEM formatting problems
- Files must be PEM encoded with proper headers/footers.
- No UTF-8 BOM, no Windows CRLF line endings if your stack is picky.
- Order matters.
Practical End-to-End Example
Let’s say SSL Labs reports “Chain issues: Incomplete” for example.com, and cURL shows:
curl: (60) SSL certificate problem: unable to get local issuer certificate
We diagnose with OpenSSL:
openssl s_client -connect example.com:443 -servername example.com -showcerts -verify_return_error </dev/null
We see only one certificate (the leaf). No intermediates appear.
Action plan:
- Get the right intermediates from your CA. Suppose they provide
intermediate1.pemandintermediate2.pem. - Build
fullchain.pem:
cat cert.pem intermediate1.pem intermediate2.pem > fullchain.pem
- Configure Nginx:
ssl_certificate /etc/ssl/example/fullchain.pem;
ssl_certificate_key /etc/ssl/example/privkey.pem;
ssl_trusted_certificate /etc/ssl/example/intermediate-chain-only.pem; # optional for stapling
Where intermediate-chain-only.pem is:
cat intermediate1.pem intermediate2.pem > intermediate-chain-only.pem
- Reload Nginx and retest:
nginx -t && systemctl reload nginx
openssl s_client -connect example.com:443 -servername example.com -showcerts -verify_return_error </dev/null
curl -Iv https://example.com
Expected: OpenSSL shows the leaf + both intermediates; verify code 0. cURL handshake succeeds. SSL Labs shows a complete chain.
Validating Your Fix Across Clients
Use multiple vantage points:
- OpenSSL:
openssl s_client -connect example.com:443 -servername example.com -verify_return_error </dev/null - cURL:
curl -Iv https://example.com - SSL Labs: Confirm “Chain issues: None.”
- testssl.sh: Broad compatibility matrix
./testssl.sh --fast https://example.com - Real devices: At least one iOS and one Android device.
- Headless Java client (if you have consumers on JVM):
- Run a simple Java HTTPS GET to ensure no PKIX errors.
Automation, Monitoring, and Renewal Safety
Certificate renewals are when chains often regress. Put guardrails in place:
- Use your ACME client’s “fullchain” output for the server:
- Certbot: typically
fullchain.pem(leaf + intermediates) andprivkey.pem. - acme.sh: configure “Le_OrderConf.json” for the correct chain; use
--fullchain-file.
- Certbot: typically
- Renewal hooks to reload the web server gracefully and run a quick check:
#!/usr/bin/env bash systemctl reload nginx sleep 2 if ! openssl s_client -connect example.com:443 -servername example.com -verify_return_error </dev/null 2>&1 | grep -q "Verify return code: 0"; then echo "Chain verification failed after reload" | mail -s "TLS Alert" [email protected] fi - CI/CD checks:
- Pull the live chain in a pipeline step and fail the build if chain is incomplete (use OpenSSL verification or testssl.sh).
- Monitoring:
- Scheduled SSL Labs API checks or Hardenize.
- Alert if chain issues appear or if intermediates are near expiry.
Quick Troubleshooting Checklist
If you’re stuck, run through this list:
- Did you concatenate your leaf followed by all required intermediates, in the right order?
- Are you serving the concatenated file on the correct vhost (SNI) and port?
- Did you avoid including the root certificate?
- Are you using the intermediate(s) that your CA intended for your issuance path?
- If you have both RSA and ECDSA certificates, did you pair each with its correct chain?
- Did you reload/restart the correct service instance or container?
- Are intermediate files readable by the server process (permissions)?
- Are you testing the exact host users hit (e.g., www vs. apex domain)?
- Did you validate from multiple networks and devices (to rule out caching/proxies)?
- If behind a CDN/LB, did you update the certificate there as well?
Frequently Asked Questions
Do I need to include the root certificate?
No. Do not include the root in the chain you serve. Clients already have roots in their trust stores.
Why does it work in Chrome but fail in cURL or Java?
Some browsers fetch missing intermediates automatically using AIA fetching. Many CLI tools and languages do not. Always serve the complete chain from the server.
Can I use the same chain file across all servers?
Yes, as long as they serve the same leaf certificate and chain. If you have multiple certificates (e.g., RSA and ECDSA), each requires its matching chain.
How do I verify the order is correct?
The order should be:
- First: leaf (your domain cert)
- Then: intermediate(s)
- Never include the root
Use OpenSSL verify with -untrusted pointing to the intermediate chain and verify the leaf.
My PFX imports but IIS still shows errors. What now?
Ensure the intermediate(s) are installed in “Intermediate Certification Authorities” and the site binding references the correct cert. Rebuild the PFX with -certfile chain.pem if necessary.
Actionable Best Practices
- Always deploy the CA-provided full chain (leaf + intermediates), not just the leaf.
- Use separate files consistently:
- cert.pem (leaf)
- chain.pem (intermediates only)
- fullchain.pem (leaf + intermediates)
- privkey.pem (private key)
- Keep your ACME/CA client up to date to avoid deprecated chains.
- Automate post-deploy checks with OpenSSL or testssl.sh.
- Document the renewal process and chain sources in your repo/runbook.
- Prefer widely trusted chains; avoid expired or exotic cross-signs for “compatibility hacks”.
- For multi-tenant servers, verify every SNI host.
- For Java servers, use PKCS#12 exports that include the chain.
Closing Thoughts
An incomplete SSL chain is one of the most common, easily preventable TLS outages. The fix is straightforward once you verify what the server actually serves and align it with the CA’s intended chain. Make it part of your routine to deploy the correct chain, validate with OpenSSL and cURL, and automate checks around renewals.
With the steps and examples in this guide, you can diagnose and fix chain issues quickly—then keep them fixed through automation and good operational hygiene.