Back to Blog
Ssl

Your SSL Works in Chrome But Breaks Everywhere Else? Missing Intermediates.

Why missing intermediate certificates cause silent failures across clients, and how to stop shipping broken chains to production.

CertGuard Team··7 min read

Chrome lies to you

Not maliciously. But Chrome, and most modern browsers really, will silently fetch missing intermediate certificates using a mechanism called AIA (Authority Information Access). Your certificate is technically misconfigured, the chain is incomplete, and Chrome just... fixes it for you. Behind the scenes. Without telling you.

So you test in Chrome. Everything looks fine. Green padlock. Ship it.

Then a customer using an older Android phone calls in. Or your backend service that talks to your API via curl starts throwing certificate verify errors. Or the webhook you send to a partner gets rejected because their Java client is stricter than yours. And you're sitting there thinking "but it works on my machine" while half your integrations are broken.

What's actually happening when you skip intermediates

SSL certificates form a chain of trust. Your server cert was signed by an intermediate CA, which was signed by another intermediate (sometimes), which was ultimately signed by a root CA. Root CAs live in trust stores, pre-installed on operating systems and browsers. The intermediates? Those are your responsibility to serve.

When you configure your web server with just your leaf certificate and skip the intermediates, you're basically telling clients "figure it out yourself." Some can. Most can't. And the ones that can't don't give you a nice error message about missing intermediates. They just say "certificate verify failed" or "unable to get local issuer certificate" and leave you guessing.

I once helped a fintech company debug why their payment processor integration broke every few months. Turned out their deployment script was pulling only the leaf cert from their CA portal, ignoring the bundle file entirely. Worked fine in all their browser tests. Failed silently in the Java-based payment gateway. They'd been manually "fixing" it by restarting services and re-downloading certs for over a year before anyone looked at the actual chain.

How to check if your chain is broken right now

Stop testing in Chrome. Seriously.

# this is your best friend for chain validation
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>&1 | grep -A2 "Certificate chain"

# if you see only "0 s:" with no "1 s:" or "2 s:", your chain is incomplete
# a healthy chain looks like:
# 0 s: CN = yourdomain.com
# 1 s: CN = R3 (or whatever your intermediate is)
# 2 s: CN = ISRG Root X1 (root, optional but nice)

The key thing: if depth 0 is the only entry, you're serving a naked leaf cert. That's broken. Maybe not visibly broken in your browser, but broken for everything else that matters.

There are also online tools. SSL Labs gives you a full chain analysis and will flag missing intermediates immediately. But openssl is faster when you're debugging at 2 AM and don't want to wait for a web UI to queue your scan.

Fixing it on Nginx

Nginx expects a single file that contains your leaf cert followed by the intermediate(s). Order matters. Leaf first, then intermediates, root last (or skip the root entirely, clients have it already).

# build the full chain file
cat your_domain.crt intermediate.crt > fullchain.pem

# or if your CA gave you a bundle file
cat your_domain.crt ca_bundle.crt > fullchain.pem

Then in your Nginx config:

server {
    listen 443 ssl;
    server_name yourdomain.com;

    # point to the FULL chain, not just the leaf cert
    ssl_certificate /etc/ssl/fullchain.pem;
    ssl_certificate_key /etc/ssl/private.key;

    # don't do this - this only has your leaf cert
    # ssl_certificate /etc/ssl/your_domain.crt;
}

Common mistake: people use ssl_certificate for the leaf and think ssl_trusted_certificate handles the chain. Nope. ssl_trusted_certificate is for OCSP stapling validation, not for serving the chain to clients. Seen this confusion in Stack Overflow answers that get hundreds of upvotes. Wrong answers with green checkmarks, a tradition as old as the internet.

Apache, because someone always runs Apache

Apache actually has a separate directive for the chain, which arguably makes it clearer but also means there's another place to forget:

<VirtualHost *:443>
    SSLEngine on
    SSLCertificateFile /etc/ssl/your_domain.crt
    SSLCertificateChainFile /etc/ssl/ca_bundle.crt
    SSLCertificateKeyFile /etc/ssl/private.key
</VirtualHost>

If you're on Apache 2.4.8 or later, you can also just concatenate everything into one file and use only SSLCertificateFile. But older configs floating around the internet still reference the pre-2.4.8 way with three separate directives, and if you're copying config snippets from blog posts written in 2014, well. You get what you get.

The Let's Encrypt case (and why certbot usually saves you)

If you use certbot, it generates a fullchain.pem file automatically. You should be using that one, not cert.pem. But I've seen plenty of setups where someone manually configured their web server to point at /etc/letsencrypt/live/domain/cert.pem instead of fullchain.pem because the filename seemed more intuitive.

Quick reference for the Let's Encrypt file structure:

# what certbot gives you:
/etc/letsencrypt/live/yourdomain.com/
    cert.pem        # leaf only - DON'T use this for ssl_certificate
    chain.pem       # intermediates only
    fullchain.pem   # leaf + intermediates - USE THIS ONE
    privkey.pem     # private key

Straightforward enough, but the naming trips people up. cert.pem sounds like the right file. It's not. Or rather, it's not the complete one.

Cross-signed roots make everything worse

Remember when Let's Encrypt switched from the IdenTrust cross-signed root to their own ISRG Root X1? That broke a bunch of older Android devices (pre-7.1.1) and some embedded systems that hadn't updated their trust stores in years. The certificate chain was technically valid, the intermediates were correctly served, but the root wasn't trusted by these old clients.

Let's Encrypt worked around it with a creative cross-signing trick where an expired root still validated the chain on older Android versions because Android didn't check root expiry. Clever. Also terrifying if you think about it too hard.

Point is: even when you do everything right with your intermediates, root trust store fragmentation across devices can still bite you. The only real defense is testing against multiple clients, not just your laptop's browser.

Automating chain validation in CI

You can catch missing intermediates before they hit production. Add something like this to your deployment pipeline:

#!/bin/bash
# verify-chain.sh - run this before deploying cert changes

CERT_FILE="${1:-fullchain.pem}"
KEY_FILE="${2:-privkey.pem}"

# check that the chain actually has more than one cert
CERT_COUNT=$(grep -c "BEGIN CERTIFICATE" "$CERT_FILE")
if [ "$CERT_COUNT" -lt 2 ]; then
    echo "ERROR: only $CERT_COUNT certificate(s) in chain file"
    echo "You're probably missing intermediate certificates"
    exit 1
fi

# verify the chain validates
openssl verify -untrusted "$CERT_FILE" "$CERT_FILE"
if [ $? -ne 0 ]; then
    echo "ERROR: certificate chain validation failed"
    exit 1
fi

# verify key matches cert
CERT_MD5=$(openssl x509 -noout -modulus -in "$CERT_FILE" | openssl md5)
KEY_MD5=$(openssl rsa -noout -modulus -in "$KEY_FILE" | openssl md5)
if [ "$CERT_MD5" != "$KEY_MD5" ]; then
    echo "ERROR: certificate and key don't match"
    exit 1
fi

echo "Chain looks good: $CERT_COUNT certificates, key matches"

It's not exhaustive. But catching the "you only have one cert in your chain file" case prevents probably 80% of intermediate-related outages. Fifteen lines of bash that could save you a 3 AM page.

When intermediates rotate and nobody tells you

CAs rotate their intermediates. Sometimes with plenty of notice. Sometimes not. If you've hardcoded intermediate certificates or you're doing manual cert management, a rotation can break your setup even if your leaf cert is still valid.

Let's Encrypt rotated from R3 to R10 and R11 intermediates in 2024. If your automation pulled the leaf cert fresh but used a cached copy of the R3 intermediate, you'd end up serving a chain where the leaf was signed by R10 but you're presenting R3 as the intermediate. The signatures don't match. Everything breaks.

The fix is simple in theory: always pull the full chain from your CA when you renew, never cache intermediates separately. In practice, I've seen custom scripts that download the cert from one endpoint and the intermediate from a completely different hardcoded URL that hasn't been updated since the original setup. Fragile automation is worse than no automation because at least with manual processes, someone is looking at what they're downloading.

Testing against real clients, not just openssl

If you want to be thorough:

# curl uses the system trust store, good baseline test
curl -vI https://yourdomain.com 2>&1 | grep "SSL certificate verify"

# test with a specific CA bundle to simulate minimal trust stores
curl --cacert /etc/ssl/certs/ca-certificates.crt -vI https://yourdomain.com

# python requests - uses certifi bundle, different from system store
python3 -c "import requests; r = requests.get('https://yourdomain.com'); print(r.status_code)"

# java keytool for checking what java clients see
keytool -printcert -sslserver yourdomain.com:443

Each of these tools uses a different trust store and has different behavior around AIA fetching. If all four work, your chain is solid. If curl works but Java fails, you've probably got a trust store issue. If only Chrome works, you're almost certainly missing intermediates.

Stop relying on browsers to cover your mistakes

The core problem is that browsers are too forgiving. AIA fetching was designed as a fallback mechanism, not a crutch for misconfigured servers. But because Chrome handles it so seamlessly, most people never realize their chain is broken until a non-browser client hits their endpoint and fails.

Serve the full chain. Validate it in CI. Test with curl, not Chrome. And when your CA sends you a zip file with six different .crt files and no clear instructions on which ones to concatenate in what order, take the ten minutes to read their docs instead of guessing. Your future self, and your on-call rotation, will thank you.