Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 3 min read

Why your site works in Chrome but fails everywhere else: missing intermediate certificates

Your site loads fine in Chrome. The padlock is there. Then: a customer on an older Android phone gets a security warning your Stripe/PayPal webhook stops arriving curl fails with unable to get local issuer certificate

Your site loads fine in Chrome. The padlock is there. Then:

  • a customer on an older Android phone gets a security warning
  • your Stripe/PayPal webhook stops arriving
  • curl fails with unable to get local issuer certificate
  • a Python script throws SSLCertVerificationError

The certificate is valid. It hasn't expired. So what's going on?

Almost always: your server isn't sending the intermediate certificate.

How certificate chains work (in 30 seconds)

Browsers don't trust your certificate directly. They trust a small set of root certificates built into the operating system. A root signs an intermediate, and the intermediate signs your certificate:

ISRG Root X1          ← on the visitor's device already
  └─ R11              ← intermediate: YOUR SERVER MUST SEND THIS
       └─ example.com ← your certificate

Your server has to send your certificate and the intermediate(s). The root is already on the device, so it doesn't need to be sent.

Why it works on your machine

Desktop browsers are good at hiding this mistake:

  • Chrome on Windows and macOS uses the OS verifier, which downloads a missing intermediate from the URL inside your certificate (this is called AIA fetching).
  • Firefox ships its own copy of the intermediates from the CA database.
  • Any browser may have cached the intermediate from another site that sent it correctly.

But curl, OpenSSL, Python, Node.js, Java, many mobile apps and most webhook senders do none of that. They only use what your server sends, so they fail.

How to check

With openssl:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | grep -E "^ *[0-9] s:|Verify return code"

A healthy chain shows at least two certificates and Verify return code: 0 (ok):

 0 s:CN=example.com
 1 s:C=US, O=Let's Encrypt, CN=R11
Verify return code: 0 (ok)

A missing intermediate shows only one certificate and error 21:

 0 s:CN=example.com
Verify return code: 21 (unable to verify the first certificate)

Try it against incomplete-chain.badssl.com to see the broken version.

How to fix it

Point your server at the full chain file, not just your certificate. With Let's Encrypt (certbot) that's fullchain.pem. Other providers call it a "CA bundle".

Nginx

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;  # not cert.pem
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

Apache 2.4.8+

SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

SSLCertificateChainFile isn't needed on 2.4.8 and newer.

Bought a certificate elsewhere? Your provider gave you your certificate plus a bundle. Combine them, your certificate first:

cat your_domain.crt ca-bundle.crt > fullchain.pem

Then reload the server (sudo nginx -t && sudo systemctl reload nginx) and check again.

Caddy, Traefik and most hosting panels send the full chain automatically.

Two related mistakes

  • Wrong order: each certificate must be followed by the one that signed it. Browsers usually cope with a wrong order; strict clients don't.
  • Sending the root: harmless, but unnecessary. It adds bytes to every handshake.

A quicker way

If you don't want to parse openssl output, I built a free certificate chain checker. It lists every certificate your server sends and the path browsers build to a trusted root, and flags a missing intermediate even when your own machine silently fixes it.

Certificate lifetimes are shrinking: 200 days now, 100 in 2027, 47 in 2029. Every renewal is another chance to deploy cert.pem instead of fullchain.pem. Worth checking after each one.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.