Why your certificate works in Chrome but fails in curl.

The site loads with a padlock. Then a webhook stops arriving, a cron job dies with a TLS error, the Android app cannot log in and a colleague's curl says the issuer is unknown. Nothing changed on the server, and that is the point: the server was always wrong, and browsers were covering for it.

Written by · Published 2026-09-23 · 7 min read

The short version

Your server is sending only the leaf certificate, not the intermediate that signed it. A client has to build a path from your leaf to a root it already trusts, and it cannot do that with a link missing. Browsers repair the gap on the fly: Chrome, Safari and Windows download the missing intermediate from a URL embedded in your certificate, Firefox has every public intermediate preloaded, and all of them cache intermediates they saw on other sites. Almost nothing else does. curl, wget, Python, Java, Go, Node, Ruby, PHP, Android apps and every embedded device take what the server sends, find it does not reach a root, and stop.

You can confirm it in ten seconds. Put the host into the SSL certificate chain checker: if the "Chain" tile says incomplete and the verdict table is green for browsers and red for everything else, this is your problem, and the corrected fullchain.pem is at the bottom of the page.

What the client is actually doing

A public certificate is not trusted on its own. It is trusted because it was signed by an intermediate CA certificate, which was signed by a root CA certificate, and the root is in the client's trust store. The client has the root. The server is responsible for sending the leaf and every intermediate between the leaf and the root. That has been the rule since TLS 1.0, and every certificate authority's installation instructions say it, usually on a page nobody reads because the browser already showed a padlock.

When the intermediate is missing, a strict client sees a leaf signed by "R11" or "Sectigo RSA Domain Validation Secure Server CA" or whatever the intermediate is called, looks for that name in its trust store, does not find it (intermediates are not roots and are not in there), and gives up. The error is some form of "I cannot find the issuer of this certificate":

ClientWhat it says
curl, wget, PHPSSL certificate problem: unable to get local issuer certificate
openssl s_clientverify error:num=20:unable to get local issuer certificate
Python requests[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
JavaPKIX path building failed: unable to find valid certification path to requested target
Node.jsUNABLE_TO_VERIFY_LEAF_SIGNATURE: unable to verify the first certificate
Gox509: certificate signed by unknown authority
AndroidTrust anchor for certification path not found
Chrome, Safari, Firefox, EdgeNothing. Padlock.

Same fault, seven vocabularies. Each of those strings has its own search results, its own Stack Overflow thread and its own wrong answers, most of which involve turning verification off. If you are here from one of them, what unable to get local issuer certificate actually means works through the diagnosis, because about half the time the fault is on the client and not the server.

How browsers get away with it

AIA fetching

Every public leaf certificate carries an Authority Information Access extension with a caIssuers URL, something like http://r11.i.lencr.org/. It is a plain HTTP link to the intermediate that signed the leaf. Chrome, Safari and Windows Schannel follow it when the served chain does not build. That download takes a round trip nobody notices, and it is why the padlock appears. It is also why the same page loads in Chrome on the machine where curl just failed: they are not using the same verifier.

Intermediate preloading

Firefox does not fetch, but since 2020 it ships every intermediate that CAs have disclosed to Mozilla's programme, which is all of them for a publicly trusted CA. So it has your intermediate before it has ever seen your site. Same result, different route.

Caching

All browsers keep intermediates they have seen. Let's Encrypt's intermediates sign a very large share of the web, so a browser that has been anywhere today has them cached. This is the one that fools people during testing: "it works on my machine" is true, and it works because the machine had been to another Let's Encrypt site an hour ago.

None of this is available to a Python script in a container that was built this morning, to a Java service with its own cacerts, to a payment provider's webhook sender or to the TLS stack in a smart TV. They get what your server sends.

How it happens

The CA sent you two or three files and only one went into the server config. That is nearly always the whole story. The variations:

How to see it for yourself

Do not use a browser to test a certificate deployment. Use something that shows what came over the wire. The chain checker does this from outside your network and lists every certificate the server sent, in order, with the missing one marked. From a shell:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -c 'BEGIN CERT'
# 1 means leaf only. 2 or 3 means leaf plus intermediates.

curl -vI https://example.com 2>&1 | grep -iE 'issuer|SSL certificate problem'

If you have several load balancers or a CDN, test each address separately: put the IP in the host field and the site name in SNI. The most common intermittent version of this fault is a chain that is right on three edges and wrong on the fourth.

The fix

Serve the leaf followed by every intermediate, in order, in one file, and no root. The chain checker builds that file for you from the correct path; how to build a correct fullchain.pem covers doing it by hand and the configuration line for each server. Then re-run the check and confirm the whole verdict column is green, not just the browser rows.

Two things not to do. Do not add the intermediate to the client's trust store; that fixes one machine and leaves every other client broken. And do not set verify=False, -k, NODE_TLS_REJECT_UNAUTHORIZED=0 or a trust-all TrustManager; that fixes the symptom by removing the security the certificate was for, and it will be found in production a year from now by someone who is not pleased.

Why this keeps coming back

Because the test that everyone runs, opening the site in a browser, is the one test that cannot detect it. Every other check in a deployment pipeline would catch a broken certificate. This one needs a non-browser client in the pipeline: a curl against the new endpoint before it goes live, or a monitor that uses a strict verifier. The domain health checker uses one, so an incomplete chain shows up in its TLS pillar rather than in an incident channel.

All guides · All tools