EasyDeploy
Back to blog
ssl tls certificates security pki troubleshooting

Why Is My Certificate Not Trusted? A Systematic Troubleshooting Checklist

Author: Łukasz Tomalczyk , founder of EasyDeploy ·
Why Is My Certificate Not Trusted? A Systematic Troubleshooting Checklist

“Not trusted,” “not secure,” “your connection is not private” — every client phrases it differently, but the underlying question is always the same: could the client build a valid chain of trust from the certificate it received back to a root it already trusts? When it can’t, that’s the only error you get. It doesn’t tell you why.

This is a checklist for finding the actual cause, roughly in order of how often each one turns out to be it.

TL;DR: the fastest way to find out why

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | grep -A2 "Verify return code"

Swap example.com for your host. The Verify return code line at the bottom tells you exactly which check failed. Jump to the quick reference below to decode the number, then go straight to the matching step.

Step 1: Check if the chain is complete

By far the most common cause. The server needs to send the leaf certificate and every intermediate CA certificate up to (but not including) a root the client already trusts. Miss an intermediate and you get verify code 20 (unable to get local issuer certificate) or 21 (unable to verify the first certificate).

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null

Count the certificates in the output. If you only see one (s: / i: pair), the server is only sending the leaf. Fix: configure your server (or load balancer, or reverse proxy) to serve the full chain, not just the end-entity certificate.

This is also why the same misconfigured server can look “fine” in a browser but fail in curl or a backend HTTP client: browsers cache intermediates they’ve seen from other sites and quietly complete the chain themselves. curl, most language HTTP libraries, and backend services with a minimal trust store won’t do that favor for you.

Step 2: Check the expiration dates — of the whole chain, not just your certificate

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -dates

That only checks the leaf. Your certificate can be valid while an intermediate above it has expired — check every certificate s_client -showcerts printed, not just the first one. This is exactly what caused several public incidents where a perfectly valid-looking certificate started failing overnight because the CA’s intermediate expired.

Step 3: Check the hostname / SAN match

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -ext subjectAltName

Since Chrome 58, only the Subject Alternative Name (SAN) counts for hostname validation — the Common Name is ignored. If the hostname you’re connecting to isn’t listed in subjectAltName (including www. vs. bare domain, which are different hostnames), validation fails regardless of everything else being correct.

Step 4: Check whether it’s self-signed or from a private CA the client doesn’t trust

Verify codes 18 (self-signed certificate) and 19 (self-signed certificate in the chain) mean the root of the chain isn’t in the client’s trust store — expected for local development, and expected for internal/private CAs until you explicitly distribute and install that CA’s certificate on every machine or container that needs to trust it. If this is an internal service, the fix isn’t a “real” certificate — it’s making sure your private CA is actually trusted where it needs to be.

Step 5: Check the system clock

A certificate is only valid inside its notBefore / notAfter window. A client with a clock that’s wrong — common on containers, VMs that lost NTP sync, or CI runners — will report a perfectly valid certificate as expired or “not yet valid.” If step 2 says the dates are fine but the error persists, check date (or w32tm /query /status on Windows) on the client, not the server.

Step 6: Check you’re actually serving the certificate you think you are

On a server hosting multiple domains (multiple vhosts, multiple ingress rules, a load balancer routing by SNI), it’s easy to end up serving the wrong certificate for a given hostname — correct certificate, wrong site. Confirm the subject and subjectAltName you see in step 1 actually match the certificate you intended to serve there, not just any valid certificate.

Step 7: Check revocation status

The least common cause, but worth ruling out last: if the certificate was revoked, some clients will actively reject it via CRL or OCSP rather than just erroring generically. Check whether the certificate has an OCSP responder and whether it’s reachable:

openssl x509 -in cert.pem -noout -ocsp_uri

If the client can’t reach that URL (firewalled OCSP responder, expired OCSP staple), some clients fail closed and treat the certificate as untrusted even though it’s technically still valid.

Quick reference: verify return codes

CodeMeaningGo to
10Certificate has expiredStep 2
18Self-signed certificateStep 4
19Self-signed certificate in chainStep 4
20Unable to get local issuer certificateStep 1
21Unable to verify the first certificateStep 1
24Invalid CA certificateStep 1 or Step 2

From reactive troubleshooting to prevention

Every step above is something you check after someone already hit the error — a user, a teammate, or a service that just failed a handshake. The chain, expiry, and hostname issues in particular are exactly the kind of thing that’s obvious in hindsight and invisible until it breaks something. That’s the gap Certifier is built to close: it tracks every certificate’s chain and expiry across environments and flags the problem — missing intermediate, expiring leaf, mismatched SAN — before a client ever has to tell you about it.

Frequently asked questions

What does "certificate not trusted" actually mean?

It means the client (browser, curl, an HTTP library) couldn't build a valid chain of trust from the certificate it received back to a root CA it already trusts. That can happen for several unrelated reasons — an incomplete chain, an expired certificate, a hostname mismatch, a certificate signed by a CA the client doesn't know about, or a clock that's wrong — the error message is usually the same regardless of the cause.

What's the fastest way to find out why a certificate isn't trusted?

Run openssl s_client -connect host:443 -servername host </dev/null and read the "Verify return code" at the bottom of the output. It tells you exactly which check failed — expired, self-signed, unable to get local issuer certificate, and so on — instead of you guessing.

What is a "verify return code" and what do the common ones mean?

It's OpenSSL's numeric result for the chain-of-trust check. The ones you'll see most often: 0 is success, 10 is an expired certificate, 18 is a self-signed certificate, 20 is unable to get local issuer certificate (a missing intermediate), and 21 is unable to verify the first certificate (usually the same root cause as 20).

Why does a valid, non-expired certificate still show as untrusted?

The most common reason is an incomplete chain — the server is sending the leaf certificate but not the intermediate CA certificate that links it to a trusted root. Browsers often paper over this by caching intermediates from other sites, which is why the same misconfigured server can look fine in a browser but fail in curl or a backend service.

Why is my certificate untrusted only in some browsers or clients but not others?

Different clients ship different trust stores and different tolerance for incomplete chains. A browser that has recently cached the missing intermediate from another site will complete the chain itself; curl, a mobile app, or a backend service with a minimal trust store won't. If it fails in one place and not another, suspect an incomplete chain first.

Can an expired root or intermediate CA cause this even if my own certificate is fine?

Yes. Your leaf certificate can be perfectly valid and still fail validation if the intermediate CA that signed it has expired or been distrusted — this is exactly what happened when browsers removed trust from Symantec-issued roots. Check the expiry of every certificate in the chain, not just your own.

Related articles