The #1 SSL Mistake: Wrong Certificate Chain Order

Your certificate chain order is probably wrong. Learn why leaf-intermediate-root order matters, how to diagnose chain issues with openssl, and how to fix them in nginx, Apache, and Node.js.


The #1 SSL Mistake: Wrong Certificate Chain Order

You just installed a brand-new SSL certificate. Chrome shows the padlock. You ship it. Then the alerts roll in: your API integration is returning unable to get local issuer certificate, Android users see security warnings, and curl refuses to connect.

The certificate is valid. The problem is the order you served it in.

What "certificate chain" actually means

When a browser connects to your server over TLS, your server doesn't just send its own certificate. It sends a chain of certificates that links your leaf certificate back to a trusted root CA. The chain typically looks like this:

  1. Leaf certificate -- your domain's certificate
  2. Intermediate certificate(s) -- issued by the root CA, used to sign your leaf
  3. Root certificate -- pre-installed in the client's trust store

Your server needs to send the leaf and intermediates. The root is already on the client, so sending it is optional (and slightly wasteful, but harmless).

Why the order matters

The TLS spec (RFC 5246, section 7.4.2) is explicit: the sender's certificate MUST come first, and each following certificate MUST directly certify the one preceding it. In other words: leaf first, then intermediates, with each cert signing the one above it.

Correct order:

1. yoursite.com          (leaf -- signed by intermediate)
2. Intermediate CA       (signed by root)
3. Root CA               (optional -- already in trust store)

Wrong order (and the most common mistake):

1. Intermediate CA
2. yoursite.com

Or just the leaf with no intermediates at all.

Why it "works" in Chrome but fails everywhere else

Modern browsers are forgiving. Chrome and Firefox implement AIA fetching -- if an intermediate is missing, they follow the Authority Information Access URL in the certificate to download it. So Chrome fills in the gap silently.

These clients do not do AIA fetching:

  • curl and wget
  • Java's HttpsURLConnection and most JVM HTTP clients
  • Python's requests / urllib3
  • Go's net/http
  • OpenSSL-based tools
  • Older Android browsers (pre-10)
  • Many IoT devices and embedded systems

If your chain is wrong, every automated system hitting your API will fail while your browser looks fine. This is exactly why it's the number one SSL mistake -- you won't catch it by clicking around in Chrome.

How to diagnose it

Check with openssl s_client

openssl s_client -connect yoursite.com:443 -servername yoursite.com </dev/null 2>&1

Look at the certificate chain output:

Certificate chain
 0 s:CN = yoursite.com
   i:C = US, O = Let's Encrypt, CN = R3
 1 s:C = US, O = Let's Encrypt, CN = R3
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1

That's correct: certificate 0 is the leaf, certificate 1 is the intermediate, and each i: (issuer) matches the next s: (subject).

If you see this instead:

Certificate chain
 0 s:CN = yoursite.com
   i:C = US, O = Let's Encrypt, CN = R3
---
Verify return code: 21 (unable to verify the first certificate)

You're missing the intermediate. The server only sent the leaf.

Check the verify return code

  • Verify return code: 0 (ok) -- chain is complete and valid
  • Verify return code: 2 (unable to get issuer certificate) -- missing intermediate
  • Verify return code: 21 (unable to verify the first certificate) -- missing intermediate

Quick test with curl

curl -vI https://yoursite.com 2>&1 | grep -E "SSL|certificate"

If curl fails with SSL certificate problem: unable to get local issuer certificate, your chain is broken.

How to fix it

The fix is the same everywhere: concatenate your certificates in the right order into one file.

Build the correct chain file

cat yoursite.crt intermediate.crt > fullchain.pem

If you have multiple intermediates (rare but possible):

cat yoursite.crt intermediate1.crt intermediate2.crt > fullchain.pem

Verify the result:

openssl crl2pkcs7 -nocrl -certfile fullchain.pem | \
  openssl pkcs7 -print_certs -noout

This prints each certificate's subject and issuer. Confirm the chain links up correctly.

nginx

server {
    listen 443 ssl;
    server_name yoursite.com;

    ssl_certificate     /etc/nginx/ssl/fullchain.pem;  # leaf + intermediates
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
}

nginx uses a single ssl_certificate directive that must point to the concatenated chain file. There is no separate directive for intermediates.

Reload:

nginx -t && systemctl reload nginx

Apache

<VirtualHost *:443>
    ServerName yoursite.com

    SSLCertificateFile    /etc/ssl/certs/yoursite.crt
    SSLCertificateKeyFile /etc/ssl/private/yoursite.key
    SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
</VirtualHost>

Apache 2.4.8+ also supports putting the full chain in SSLCertificateFile (like nginx), but the explicit SSLCertificateChainFile directive makes the setup clearer.

Reload:

apachectl configtest && systemctl reload apache2

Node.js

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('/etc/ssl/private/yoursite.key'),
  cert: fs.readFileSync('/etc/ssl/certs/fullchain.pem'),
  // Or specify the chain separately:
  // cert: fs.readFileSync('/etc/ssl/certs/yoursite.crt'),
  // ca: [fs.readFileSync('/etc/ssl/certs/intermediate.crt')],
};

https.createServer(options, app).listen(443);

The cert option accepts a full chain PEM. Alternatively, put only the leaf in cert and intermediates in ca.

Verify the fix

After reloading your server config, confirm the chain is correct:

# Full chain check
openssl s_client -connect yoursite.com:443 -servername yoursite.com </dev/null 2>&1 | \
  grep -E "Certificate chain|Verify return"

# Should show: Verify return code: 0 (ok)

# Also test with curl
curl -I https://yoursite.com

Quick reference

Symptom Cause Fix
Works in Chrome, fails in curl Missing intermediate Add intermediate to chain file
Verify return code: 21 Incomplete chain Concatenate leaf + intermediates
Chain shows wrong order Certs concatenated backwards Reorder: leaf first, then intermediates
Extra root cert in chain Harmless but wastes bytes Optionally remove, not required

Getting the chain order right is a one-time fix. Get it right, verify with openssl s_client, and you'll never get another 3 AM alert about SSL failures in your API clients.


More in Gotchas