In short. A green padlock does not prove your TLS setup is correct. Browsers repair several server-side mistakes silently - most importantly, they fetch missing intermediate certificates on their own. Command-line tools and application runtimes do not. When a site opens fine in Chrome but fails in curl, Java or Python, the server is almost always serving an incomplete certificate chain.
Why the browser is your most forgiving client
Browsers are built to keep users on the page. Over years they accumulated compensations for common server misconfigurations, and those compensations run without telling anyone. Every other TLS client - curl, a Java service, a Python script, a payment gateway calling your webhook - validates strictly and fails loudly.
This is why "it works in my browser" is not evidence, and why the failure usually surfaces first through an integration rather than through visitors. The partner's server tried to call your API, validation failed, and nobody looked at a browser at all.
If a site opens in a browser but fails everywhere else, do not start by suspecting the client. Start by inspecting what your server actually sends during the handshake. In the large majority of these reports the server is at fault and the browser was hiding it.

The four things browsers do that other clients do not
| Browser behaviour | Effect | What strict clients do instead |
|---|---|---|
| Fetches missing intermediates | An incomplete chain still validates | Fail with "unable to get local issuer certificate" |
| Ships a frequently updated root store | New CAs are trusted quickly | Use an OS or runtime store that may be years old |
| Always sends SNI | The right virtual host answers | Some libraries omit it and get the default host |
| Offers a broad protocol and cipher set | Almost any server negotiates | Narrow defaults; may share nothing with the server |
Reproduce the failure the way the strict client sees it
Before changing anything, get the server to show you its handshake. This command validates the way curl and most runtimes do, without a browser's assistance:
openssl s_client -connect example.com:443 -servername example.com \
-verify_return_error < /dev/null 2>&1 \
| grep -E 'Verify return code|Protocol|Cipher|subject=|issuer='
# How many certificates does the server actually send?
openssl s_client -connect example.com:443 -servername example.com -showcerts \
< /dev/null 2>&1 | grep -c 'BEGIN CERTIFICATE'
The count is the fastest diagnostic in this whole article. A publicly trusted certificate almost always needs at least two certificates in the handshake: your leaf and one or more intermediates. If the count is 1, you have found the problem and can stop reading the rest of the causes.
Cause 1: an incomplete certificate chain
Certificate authorities do not sign your certificate with their root key. They sign it with an intermediate, and the intermediate is signed by the root. Clients ship only roots, so the server must supply the intermediates that connect its leaf to one.
When the server omits them, browsers quietly fetch the missing certificate using the URL inside the leaf's Authority Information Access extension. Strict clients do not, and report that they cannot build a path to a trusted root.
# See the AIA URL browsers use to repair the gap - and the CN of the issuer
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -issuer -ext authorityInfoAccess
The fix is on the server, and it is usually a one-word mistake in the configuration - pointing at the leaf certificate rather than the full chain:
# nginx - ssl_certificate must be the FULL chain, not the leaf alone
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # correct
# ssl_certificate /etc/letsencrypt/live/example.com/cert.pem; # leaf only - breaks strict clients
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Apache - modern versions read the chain from this one file
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Reload after changing it, then re-run the certificate count. It should now be two or more. The SSL checker reports the chain exactly as served, and the chain guide covers rebuilding it when the intermediate is not in your issuer's bundle.
Cause 2: the client's root store is not yours
Every TLS client trusts a list of root certificates, and those lists are maintained separately. A browser updates its store with the browser. An operating system updates with system packages. A Java runtime carries its own cacerts file, and Python's requests library uses a bundle shipped inside a package rather than the OS store.
The practical consequence is that a container image or a long-lived server can be years behind, and a certificate from a newer CA will fail there while working everywhere else. The signature of this cause is that the failure is specific to one environment and the chain itself is complete.
# Which bundle is this curl using?
curl -sI https://example.com --cacert /etc/ssl/certs/ca-certificates.crt > /dev/null \
&& echo 'OS bundle: OK'
# Java: is the issuing root present in the runtime truststore?
keytool -list -cacerts -storepass changeit 2>/dev/null | grep -ci 'trustedCertEntry'
# Python: which CA file is actually in use?
python3 -c "import ssl; print(ssl.get_default_verify_paths())"
python3 -c "import certifi; print(certifi.where())"
Resist the temptation to disable verification to make the error disappear.
curl -k,verify=Falseand a trust-everythingTrustManagerturn a visible certificate problem into a silent interception risk, and they tend to survive in the codebase long after the real cause is forgotten.

Cause 3: the client is not sending SNI
One address can host many certificates, so the client must announce which hostname it wants during the handshake. Browsers always do. Some older libraries, and openssl s_client when you forget -servername, do not - and the server answers with its default certificate, which is for a different name.
The resulting error is a hostname mismatch, which sends people to check DNS. The test is a direct comparison:
# With SNI - the correct virtual host answers
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -subject
# Without SNI - which certificate does the server fall back to?
openssl s_client -connect example.com:443 < /dev/null 2>/dev/null \
| openssl x509 -noout -subject
Different subjects mean the server depends on SNI. That is normal and correct, but it means any client that omits it will fail, and the fix belongs in the client.
Cause 4: no shared protocol or cipher
Correctly hardening a server by disabling old TLS versions breaks clients that only speak those versions. This is the one cause where the server change was right and the client is genuinely outdated.
# Which versions does the server accept?
for v in tls1_2 tls1_3; do
printf '%-8s ' "$v"
openssl s_client -connect example.com:443 -servername example.com -$v \
< /dev/null 2>&1 | grep -m1 'Cipher is' || echo 'not accepted'
done
The signature here is different from a chain problem: the handshake fails before any certificate is discussed, so the error mentions protocol or cipher rather than trust. Old Java releases defaulting to TLS 1.0, and runtimes linked against very old OpenSSL builds, are the usual sources.
Reading the error message: which cause it names
| Client | Message | Cause |
|---|---|---|
| curl | unable to get local issuer certificate | Incomplete chain, or an outdated bundle |
| curl | self-signed certificate in certificate chain | An interception proxy, or a private CA |
| Java | PKIX path building failed | Incomplete chain, or root missing from cacerts |
| Python | certificate verify failed: unable to get local issuer certificate | Incomplete chain, or a stale bundle |
| Go | x509: certificate signed by unknown authority | Incomplete chain, or root not in the system store |
| Any | hostname mismatch / does not match certificate | Missing SNI, or the wrong certificate installed |
| Any | no cipher / protocol version alert | No shared TLS version or cipher |
| Any | certificate has expired | Genuine expiry - renewal did not run or did not reload |
Note how many rows point at the same cause. An incomplete chain accounts for most of the distinct-looking messages in this table, which is why the certificate count is the first thing to measure.

Notes for specific runtimes
curl and libcurl
Uses the OS bundle by default. curl -v prints the chain it built and where verification stopped, which makes it the quickest reproduction tool. Never leave -k in a script that reached production.
Java
Has its own truststore rather than the OS one, so a root trusted by the system may still be unknown to the JVM. -Djavax.net.debug=ssl:handshake prints the full negotiation, including exactly which certificate broke path building.
Python
The standard library uses the OS store, while requests uses the bundle from certifi. The two can disagree, so a script can fail while curl on the same machine succeeds. Upgrading certifi resolves the stale-bundle case.
Go and .NET
Both use the system store on most platforms and validate strictly. Minimal container images frequently ship without any CA bundle at all, which produces an unknown-authority error for every host until the certificate package is installed.
How to check your own server
Run the SSL checker against your hostname and read three things: the number of certificates in the served chain, the negotiated protocol, and the validation result. A chain of one is the defect described above, regardless of how the site looks in a browser. If you also need to confirm the handshake itself completes, the handshake guide covers failures that happen before validation.
Because the failure mode here is invisible to browsers, it is also invisible to manual checking - a certificate renewal that reinstalls only the leaf breaks every integration while the site keeps looking perfect. SSL monitoring validates the chain on a schedule from outside your network and alerts when it changes, which catches a bad renewal on the day it happens rather than when a partner reports their webhook stopped working.

Frequently asked questions
The padlock is green. How can the certificate be wrong?
The padlock reports that the browser built a valid chain, not that your server supplied one. If an intermediate was missing, the browser fetched it and showed success. The padlock is a statement about the browser's result, not about your configuration.
Why does it fail only from one server?
Almost always an outdated or absent CA bundle in that environment - a long-lived VM, or a minimal container image with no certificate package. Check which bundle that runtime uses before concluding the remote server is at fault.
Is disabling verification an acceptable temporary fix?
It removes the only protection TLS gives you against an impostor, and temporary flags are unusually persistent. If you must ship before the server is fixed, pin the specific expected certificate rather than trusting everything.
How do I know whether the chain or the root store is the problem?
Count the certificates the server sends. If it sends one, the chain is incomplete and the server is at fault. If it sends the full chain and only one client fails, that client's trust store is the problem.
Does Let's Encrypt need special handling?
No, but its file layout invites the classic mistake: cert.pem is the leaf alone and fullchain.pem includes the intermediates. Pointing a web server at the first one produces exactly this failure, and it is the single most common instance of it.
Can a corporate proxy cause this?
Yes. Interception proxies re-sign traffic with their own CA, which employee browsers trust because it was installed centrally. A build agent or service on the same network usually does not trust it, so the same request fails there with a self-signed-in-chain error.
Checklist
- Count the certificates in the served chain before investigating anything else.
- Point the web server at the full chain file, not the leaf certificate.
- Reload the server after changing certificate paths, and re-measure.
- Verify with
-verify_return_error; requireVerify return code: 0. - Check the failing client's trust store separately from the server's chain.
- Compare handshakes with and without SNI when the error mentions hostname mismatch.
- Confirm a shared TLS version before assuming a certificate problem.
- Never leave verification disabled as a fix.
- Re-verify the chain after every renewal - a bad renewal is invisible in browsers.
- Monitor the chain from outside so a silent regression is caught on the day it lands.