Symptoms: What Does an Incomplete Certificate Chain Look Like?
Last Friday afternoon, one of our client’s Android apps started throwing errors everywhere — javax.net.ssl.SSLHandshakeException: Handshake failed. iOS and desktop browsers were fine. Android-only meltdown. If you’ve been in ops long enough, you know exactly what this means: incomplete certificate chain.
Run your site through Qualys SSL Labs and see this?
Chain issues: Incomplete This server’s certificate chain is incomplete. Grade capped to B.
You’ve hit the wall.
Common symptoms include:
- Mobile apps (especially Android < 7.1 or older WebViews) throwing SSL warnings
curlworks with--cacertbut fails with plaincurl https://yoursite.com- Browser padlock icon shows yellow warning triangle
- Monitoring alerts suddenly lighting up with
SSL_ERROR_BAD_CERT_DOMAINorcertificate unknown
Root Cause: Who Dropped the Ball?
Here’s the thing — it’s dead simple: your server didn’t send the intermediate certificate during the TLS handshake.
The SSL certificate chain has three layers:
Root CA (trusted by OS/browser)
└── Intermediate CA (must be served by server)
└── Leaf Certificate (your domain cert)
The problem lives in the middle layer. Your server only sent the leaf certificate. The client doesn’t have the intermediate in its trust store, so the chain breaks. Boom — “Incomplete.”
Common screw-ups:
- Wrong concatenation order — someone put the intermediate before the leaf, or included the root
- Only configured the server cert, forgot the intermediate entirely — most common, especially with half-baked deployment scripts
- Expired intermediate certificate — yes, intermediates have expiration dates too
- CDN or reverse proxy not passing the full chain — NGINX, Apache, Cloudflare, AWS ALB all have their own quirks
Fix Steps: From Diagnosis to Resolution
Step 1: Diagnose — Confirm the Problem
Check what your server is actually sending:
# Method 1: openssl s_client (most reliable)
openssl s_client -connect yoursite.com:443 -showcerts </dev/null 2>/dev/null | grep "subject="
# You should see at least two certificates:
# subject=CN = yoursite.com ← Leaf certificate
# subject=CN = R3, O = Let's Encrypt ← Intermediate certificate
# If only one line shows up, your chain is broken
# Method 2: curl verification (simulates Android behavior)
curl -v --tls-max 1.2 https://yoursite.com/ 2>&1 | grep "SSL certificate verify"
# Method 3: Qualys online scan (most visual)
# https://www.ssllabs.com/ssltest/analyze.html?d=yoursite.com
I always start with openssl s_client. If you see verify error:num=20:unable to get local issuer certificate, that’s your smoking gun.
Step 2: Get the Correct Intermediate Certificate
Download it from your CA’s website or grab it from your certificate provider. For Let’s Encrypt:
# Download Let's Encrypt intermediate
wget https://letsencrypt.org/certs/lets-encrypt-r3.pem
Critical: You need the Intermediate CA certificate, not the Root. The root is already in client trust stores. Don’t put it in your server config.
Step 3: Concatenate the Full Chain
This is where most people mess up. Order matters — strictly:
[Leaf Certificate]
[Intermediate Certificate]
Rules:
- Don’t include the root certificate
- Don’t put intermediate before leaf
- Don’t leave extra whitespace or garbage
# Correct: leaf first, intermediate second
cat yoursite.com.pem lets-encrypt-r3.pem > fullchain.pem
# Verify the merged file
openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -text | grep "Subject:"
# Should show two subjects: your domain + the intermediate CA
Step 4: Configure Your Web Server
NGINX config:
server {
listen 443 ssl;
server_name yoursite.com;
# Key: point to the merged fullchain file
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/yoursite.com.key;
# Other SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}
Apache config:
<VirtualHost *:443>
ServerName yoursite.com
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/yoursite.com.pem
SSLCertificateKeyFile /etc/apache2/ssl/yoursite.com.key
# Apache needs a separate chain file
SSLCertificateChainFile /etc/apache2/ssl/lets-encrypt-r3.pem
</VirtualHost>
Note: Apache 2.4.8+ can read the chain from SSLCertificateFile if you concatenate, but SSLCertificateChainFile is more explicit and less error-prone.
AWS ALB / CloudFront: When uploading via AWS Console, fill in the “Certificate chain” field. AWS’s UI is borderline hostile here — many people paste only the leaf cert and forget the chain.
Step 5: Verify the Fix
# 1. Restart your web server
sudo systemctl restart nginx # or httpd/apache2
# 2. Re-check with openssl
openssl s_client -connect yoursite.com:443 -showcerts </dev/null 2>/dev/null | grep "subject="
# 3. Full curl verification
curl -v https://yoursite.com/ 2>&1 | grep "SSL certificate verify"
# 4. Run Qualys scan
# Visit https://www.ssllabs.com/ssltest/analyze.html?d=yoursite.com
# Confirm "Chain issues: None" and "Grade A"
Certificate Chain Configuration by Platform
| Platform | Chain Config Method | Common Pitfall | Best Practice |
|---|---|---|---|
| NGINX | ssl_certificate points to merged file | Wrong concatenation order | cat server.crt intermediate.crt > fullchain.crt |
| Apache | SSLCertificateChainFile separate directive | Forgetting this directive entirely | 2.4.8+ can merge into SSLCertificateFile |
| AWS ALB | Chain field in Console upload | Field easily overlooked in UI | Use AWS CLI: aws acm import-certificate |
| CloudFront | Via ACM upload | Must use US East (N. Virginia) region | Cross-region certs will fail |
| HAProxy | crt points to merged PEM | Thinking private key must be included | PEM should contain cert chain only |
| IIS | Manual import via MMC | Windows auto-trusts root, but intermediate needs manual import | Import into “Intermediate Certification Authorities” store |
FAQ
Why does iOS work but Android throws errors?
Android < 7.1 doesn’t bundle intermediate certificates for many CAs like Let’s Encrypt. iOS and desktop browsers have their own certificate stores that automatically download missing intermediates. Android 7.1+ improved this, but many custom ROMs still have issues. Solution: Your server must serve the full chain.
Should I include the root certificate when concatenating?
No. Root certificates are already in client trust stores. Including them is not only unnecessary but can cause problems — some older OpenSSL implementations throw “chain too long” errors.
How do I check if my certificate chain is complete?
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | grep -E "subject=|issuer="
If you see only one subject= line, it’s incomplete. You should see at least two (leaf + intermediate).
Does Let’s Encrypt’s automatic renewal change the chain?
Yes. Let’s Encrypt migrated intermediate CAs in 2024 (from R3 to E1 and others). If your manual concatenation script hardcodes the old intermediate, the chain will break after renewal. Use Certbot for full lifecycle management — it handles chain updates automatically.
A Real-World Case
Last month I helped an e-commerce client whose SSL cert showed Grade B on Qualys (penalized for Incomplete Chain). Their ops guy said “but I configured the certificate!” I asked him to run openssl s_client — one line, leaf only.
Turns out their CI/CD pipeline had a script that copied only the server certificate to the deployment directory. The intermediate never made it. Fixed the script by adding one cat command. Grade jumped from B to A+. Android SSL errors dropped to zero.
This kind of problem — it’s almost always a missing line in automation.
Prevention Tips
- Include chain concatenation in your deployment scripts — don’t rely on manual steps
- Set up monitoring — periodically run
openssl s_clientto check chain integrity - Use Certbot or similar mature tools — they handle the full lifecycle, including intermediate updates
- Don’t assume CDN-to-origin is fine with just one cert — if you use a CDN, configure the full chain on the origin too
Here’s the rule to remember: Leaf first, intermediate second, root never.
References & Community Insights
The following authoritative resources were referenced for architectural best practices and specifications: