Two different problems behind one warning
When a certificate “doesn’t match”, one of two quite different things is going on:
- Hostname mismatch. The certificate is valid and trusted, but the name you connected to isn’t on it. Chrome says
NET::ERR_CERT_COMMON_NAME_INVALID, FirefoxSSL_ERROR_BAD_CERT_DOMAIN, and libraries say “hostname mismatch”, “certificate doesn’t match host” or “no alternative certificate subject name matches”. - Incomplete chain. The name is right, but the client can’t connect the certificate to a root it trusts, because the server didn’t send the intermediate certificate in between. Chrome says
NET::ERR_CERT_AUTHORITY_INVALID(when it shows anything at all), curl saysunable to get local issuer certificate, Java saysPKIX path building failed, and .NET saysPartialChain.
The fixes don’t overlap. Reissuing the certificate with more names does nothing for a missing intermediate, and installing the intermediate does nothing for a missing name. So find out which one you have before changing anything.
One command that answers both questions
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null
Always pass -servername with the exact name that fails. Without it the server may hand you its default certificate, and you’ll be debugging the wrong one.
Read three parts of the output.
The chain the server sent, near the top:
Certificate chain
0 s:CN = www.example.com
i:C = US, O = Let's Encrypt, CN = R11
1 s:C = US, O = Let's Encrypt, CN = R11
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
s: is the subject, i: is the issuer. A healthy chain is a staircase: each certificate’s issuer is the subject of the next line. If you only see line 0, the intermediate is missing.
The verification result, near the bottom:
Verify return code: 0 (ok)
With a missing intermediate you get this instead:
Verify return code: 21 (unable to verify the first certificate)
The names on the leaf, if you suspect a hostname mismatch:
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -ext subjectAltName
If the name you typed isn’t in the Subject Alternative Name list (wildcards cover exactly one label), it’s a hostname mismatch. That case has its own guide with the seven usual causes: NET::ERR_CERT_COMMON_NAME_INVALID. The rest of this page is about the chain.
The SSL check does the same thing from outside your network: it completes a real handshake from a server in Seoul, reads the certificate that’s actually served and verifies the chain against the standard Mozilla root list. It does not download missing intermediates, so it reports a broken chain the way curl or an Android app would see it, not the way Chrome papers over it. It doesn’t check revocation.
Why it works in Chrome and fails everywhere else
This is what makes an incomplete chain so confusing. You open the site in Chrome, padlock, everything fine. Then a payment provider’s webhook fails, an Android app can’t connect, a monitoring check goes red, or a colleague’s curl complains.
Every certificate carries an Authority Information Access (AIA) extension with a URL where its issuer’s certificate can be downloaded. When a server forgets to send the intermediate:
- Chrome, Edge and Safari notice the gap, fetch the intermediate from the AIA URL, and carry on. The user never knows.
- Firefox doesn’t fetch AIA URLs, but current desktop versions ship the intermediates of all publicly trusted CAs and also remember intermediates they’ve seen. So Firefox often works too, which makes testing with “another browser” useless here.
- curl, OpenSSL, Python, Node.js, Java, Go on Linux and Android’s TLS stack do none of this. They trust what the server sends plus their root store. A missing intermediate is a hard failure.
The result is a bug that’s invisible from a desktop browser and fatal for API clients and mobile apps. Never conclude “the certificate is fine” from a browser alone. Use openssl s_client or an external checker.
What the failure looks like in each client:
curl: (60) SSL certificate problem: unable to get local issuer certificate
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate
javax.net.ssl.SSLHandshakeException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
Error: unable to verify the first certificate (Node.js, UNABLE_TO_VERIFY_LEAF_SIGNATURE)
The remote certificate is invalid because of errors in the certificate chain: PartialChain (.NET)
One caveat on .NET: it can download missing intermediates itself on both Windows and Linux. So PartialChain there often means the download was blocked too (a container or server without outbound HTTP), or that the chain ends in a private CA root that isn’t installed in that container. Check openssl s_client first; if the server sends a full chain and .NET still says PartialChain, it’s the client’s trust store.
The same error messages also appear when the client is the problem: a slim Docker image without the ca-certificates package, or a corporate proxy re-signing traffic with its own root. If openssl s_client from a normal machine shows a full chain and Verify return code: 0, the server is fine.
The usual causes on the server
1. cert.pem configured instead of fullchain.pem
By far the most common. certbot writes four files into /etc/letsencrypt/live/example.com/:
| File | Contains |
|---|---|
cert.pem | the leaf certificate only |
chain.pem | the intermediate(s) only |
fullchain.pem | leaf followed by intermediate(s) |
privkey.pem | the private key |
nginx expects everything in one file:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
Find what nginx is actually loading, across all included files:
sudo nginx -T 2>/dev/null | grep -n 'ssl_certificate'
Apache 2.4.8 and later also takes the full chain in SSLCertificateFile; SSLCertificateChainFile is deprecated. On older Apache, SSLCertificateFile gets cert.pem and SSLCertificateChainFile gets chain.pem.
Certificates from a commercial CA usually come as a zip with your certificate plus a “CA bundle” or “intermediate” file. Build the full chain yourself, leaf first:
cat www_example_com.crt ca-bundle.crt > fullchain.pem
Other places the same mistake happens:
- HAProxy: the file in
crtmust contain the leaf, the intermediates and the private key. - Kubernetes: the
tls.crtkey of a TLS secret must hold the full chain, not just the leaf. - AWS ACM import, and most load balancer and CDN upload forms: there’s a separate “certificate chain” field. Leave it empty and you’ve uploaded a leaf-only chain.
- Node.js:
https.createServer({ cert, key })sends whatevercertcontains. Readfullchain.pemthere. - IIS and other Windows servers: they send the chain Windows can build from the local certificate stores. If the intermediate isn’t in the computer’s Intermediate Certification Authorities store, typically because a PFX was imported without it, the server sends the leaf alone.
2. The chain is in the wrong order
The leaf must come first, then the intermediate that signed it, then the next one up. If you concatenate the other way round (cat ca-bundle.crt www_example_com.crt), nginx pairs your private key with the first certificate in the file, which is now the intermediate, and refuses to load:
SSL_CTX_use_PrivateKey("/etc/nginx/ssl/example.key") failed
(SSL: error:05800074:x509 certificate routines::key values mismatch)
(Older OpenSSL versions print error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch.) When the leaf is first but the intermediates are shuffled, most modern clients sort it out, but older ones don’t, and there’s no reason to rely on it.
List what’s in a bundle, in order:
openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout
Check the staircase: the first entry’s issuer must be the second entry’s subject, and so on.
3. The certificate and private key don’t belong together
This happens after a renewal or reissue where the new certificate was copied over but the key from the old request stayed, or the other way round. The server usually refuses to start or reload with the key values mismatch error above.
Compare the public key in the certificate with the one derived from the private key. This works for RSA and ECDSA keys alike:
openssl x509 -in cert.pem -noout -pubkey | openssl sha256
openssl pkey -in privkey.pem -pubout | openssl sha256
The two hashes must be identical. The older modulus comparison (openssl x509 -noout -modulus against openssl rsa -noout -modulus) only works for RSA keys.
The dangerous part is what happens next. If systemctl reload nginx fails, nginx keeps running with the old configuration and the old certificate. Everything looks fine until the old certificate expires. Always run nginx -t (or apachectl configtest) and read the result before reloading, and check journalctl -u nginx after.
4. You fixed the files, but something else is serving the certificate
You edited the right file, reloaded, and openssl s_client still shows the old chain. Look at what is actually terminating TLS:
- A CDN, load balancer or reverse proxy in front of the server presents its own certificate. Your origin’s files don’t matter to visitors. Fix the chain where it’s uploaded.
- Several servers behind one name, and you only updated one. Test each IP with
openssl s_client -connect 203.0.113.10:443 -servername www.example.com. - The DNS name points somewhere else than you think. A DNS lookup shows where it resolves right now.
- A different server block answered because the
server_namedidn’t match the SNI, so a differentssl_certificatewas used.
5. The intermediate itself is outdated
CAs rotate intermediates. If you saved an intermediate years ago and keep concatenating it with new leaf certificates, the chain no longer lines up: the leaf’s issuer won’t match the intermediate’s subject. Always take the chain that came with the current certificate (certbot’s fullchain.pem is regenerated on every renewal).
If you only have the leaf, its AIA URL tells you where the issuer lives:
openssl x509 -in cert.pem -noout -ext authorityInfoAccess
curl -s http://r11.i.lencr.org/ | openssl x509 -inform DER -out intermediate.pem
The URL in the second line is only an example; use the one your certificate prints. These downloads are usually DER encoded, hence -inform DER.
Check the files before you deploy
openssl verify builds the chain the same way strict clients do:
openssl verify -untrusted chain.pem cert.pem
cert.pem: OK means the leaf chains to a root in your system store using only the intermediates you supplied. If it fails here, it will fail for curl and Android in production too.
After the fix
- Run
openssl s_client -showcertsagain and confirm you see at least two certificates andVerify return code: 0 (ok). - Test every hostname the certificate serves. Different server blocks often point at different files.
- Re-run the SSL check. The chain field should read OK rather than Untrusted.
- If a renewal hook or deployment script copies certificates around, fix the script too. Otherwise the next renewal brings the problem back.
Related errors
- NET::ERR_CERT_COMMON_NAME_INVALID: the hostname isn’t on the certificate.
- NET::ERR_CERT_AUTHORITY_INVALID: the chain doesn’t reach a trusted root, from a missing intermediate, a self-signed certificate, a private CA or an intercepting proxy.
- Cloudflare error 526: in Full (strict) mode, Cloudflare rejects an origin certificate it can’t validate.
Check your own domain now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.