SSL Certificate Problem: Unable to Get Local Issuer Certificate

You open the site in a browser and the padlock is green. Then your app tries to call the same host and dies:

curl: (60) SSL certificate problem: unable to get local issuer certificate

Node says it a different way — Error: unable to get local issuer certificate, UNABLE_TO_GET_ISSUER_CERT_LOCALLY. Go says x509: certificate signed by unknown authority. Python says [SSL: CERTIFICATE_VERIFY_FAILED] unable to get local issuer certificate. Same root cause, four dialects. And the first instinct is almost always wrong: this is rarely your client's fault, and the fix is almost never curl -k or rejectUnauthorized: false. Reaching for those disables the check that just correctly told you something is broken on the server.

The confusing part is the disagreement. If the certificate were simply expired or issued for the wrong hostname, the browser would refuse it too. Here the browser is happy and everything else is unhappy, which feels like the strict clients are being pedantic. They aren't. The browser is doing extra work to cover for a misconfigured server, and your API client is showing you the server as it actually is.


Your Browser Is Not a Test of Your TLS Chain

Here is the reframing that makes the whole error make sense. A TLS server is supposed to send its leaf certificate plus every intermediate up to (but not including) a trusted root — the full chain, in order. The client already trusts the root; its job is to link the leaf back to that root through the intermediates the server provides.

When a server sends only the leaf and forgets the intermediates, strict clients have nothing to build the path with. curl, Node, Go, Python, Java, and every API SDK look at the leaf, ask "who signed this?", can't find that issuer in what was presented or in their trust store, and stop. That is unable to get local issuer certificate, verbatim: the local issuer — the intermediate — is missing.

Browsers refuse to fail on this, for two reasons that your server has no right to rely on:

  • They cache intermediates. Once any site has handed a browser a given intermediate, the browser remembers it and will happily reuse it for a different site that forgot to send it. So the site "works" on your machine because you visited a properly-configured sibling first, and breaks on a fresh machine, a colleague's laptop, or a CI runner that has never seen that intermediate.
  • They fetch the missing one. Most browsers implement AIA (Authority Information Access) fetching: if an intermediate is missing, they read a URL out of the leaf and go download it. curl and most server-side stacks deliberately do not — fetching an unknown URL mid-handshake is a footgun they'd rather not have.

So a green padlock proves the browser papered over your chain, not that your chain is correct. The only honest test is to look at exactly what bytes the server hands out, which we'll do below.

What Actually Causes It

In rough order of how often they bite:

  • A missing intermediate certificate. The overwhelmingly common case. The server was configured with the leaf (cert.pem) but not the CA bundle (chain.pem / fullchain.pem), or someone deployed privkey.pem + cert.pem and skipped fullchain.pem. The leaf is valid; the path to a trusted root just isn't reachable from what's presented. Let's Encrypt's fullchain.pem exists precisely to prevent this — using cert.pem where fullchain.pem was meant is the single most frequent trigger.

  • The chain is present but in the wrong order. TLS expects leaf first, then each issuer in sequence. Some clients tolerate a shuffled chain; strict ones treat a leaf that isn't in position zero, or an intermediate that doesn't link to the cert before it, as a broken path. The certificates are all there — they just don't form a link you can walk.

  • An expired intermediate. The leaf is fine and far from its own expiry, but an intermediate in the chain has lapsed. Path building reaches the dead link and stops. This is nastier than leaf expiry because your leaf's expiry date looks reassuringly distant, so monitoring that only watches the leaf's notAfter says everything is fine while clients are already failing.

  • A private or self-signed CA the client doesn't trust. Internal services, corporate proxies doing TLS interception, and staging environments often present a certificate chaining to a CA that isn't in the public trust store. This is "working as intended" if the client is supposed to trust that CA — the fix is to install the CA, not to disable verification — and a genuine misconfiguration if it isn't.

  • The client's own CA bundle is stale or wrong. Least common, but real: an ancient ca-certificates package, a container image with no root store at all, or a hardcoded SSL_CERT_FILE pointing at the wrong bundle. If one specific machine fails and every other client on earth succeeds, suspect this — but check the server first, because the server is wrong far more often than your trust store is.

Only the last one is actually about the client. The other four are the server presenting a chain that a browser's safety net happens to catch and a strict client correctly does not.

How to See What Your Server Actually Serves

Stop guessing and read the handshake. One command shows you the exact chain the server presents:

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

Two things to read in the output:

  • Verify return code. 0 (ok) means openssl (using your local trust store) built a full path. 20 (unable to get local issuer certificate) or 21 (unable to verify the first certificate) means the chain the server sent doesn't reach a trusted root — that's your bug, on the server.
  • The -showcerts block. Count the certificates. A well-configured public site sends the leaf and at least one intermediate. If you see exactly one certificate and it isn't self-signed, the intermediate is missing — that's the missing-intermediate case, confirmed. Check that each certificate's i: (issuer) matches the next certificate's s: (subject); the first place that linkage breaks is where path building stops.

Cross-check against a public analyzer like SSL Labs, which explicitly reports "Chain issues: Incomplete." And the tell that confirms the whole diagnosis: the site works in your everyday browser and fails from a container you just built, a coworker's fresh machine, or CI. That split is the cached-intermediate effect — same server, different clients, and the strict ones are right.

Once you've confirmed it, the fix is on the server: deploy the full chain (fullchain.pem, or your platform's equivalent leaf-plus-intermediates bundle), in order, and re-run the openssl command until Verify return code reads 0 (ok) from a machine with a clean trust store. Don't validate the fix in the browser that cached the intermediate for you — it lied to you once and will do it again.

What This Means If You Operate the Server

  • Ship fullchain.pem, never cert.pem alone. If your renewal tooling writes both, wire the server at the full-chain file. The leaf-only file is a trap that passes in every browser and fails everywhere else.
  • Validate from a clean client, not your browser. After any certificate change, run the openssl check (or curl -v) from a machine or container that hasn't visited the site, so a cached intermediate can't fake a pass.
  • Watch the whole chain's expiry, not just the leaf's. An intermediate can expire while your leaf is nowhere near its own notAfter. If your monitoring only reads the leaf date, an expired-intermediate outage is invisible to it right up until customers hit it.
  • Test the deployed instance from outside. The chain your server presents depends on config the box actually loaded — a template that's correct in git can still deploy leaf-only if the wrong file got referenced.

What This Means If You Consume One

  • Don't reach for -k / rejectUnauthorized: false. The error is correct: the server can't prove its identity to a client that isn't papering over the gap. Disabling verification ships that blindness to production, where it becomes a real man-in-the-middle exposure.
  • If it's a private CA you're supposed to trust, install the CA — add it to the trust store or point SSL_CERT_FILE/NODE_EXTRA_CA_CERTS at it. That keeps verification on for everyone else.
  • If it's someone else's public endpoint, report it upstream. "Your server is serving an incomplete chain — openssl s_client returns verify code 20" is a precise, actionable bug report, and it's their fix to make, not yours to disable your way around.

An "unable to get local issuer certificate" error is not your client being difficult. It's the first client to tell you the truth after your browser spent weeks hiding it: the certificate is valid, but the path from it to a trusted root isn't reachable from what your server actually sends. The certificate that works in a browser and fails in curl is a certificate that will fail for a real fraction of your users and integrations — every fresh device, every server-to-server call, every SDK that doesn't cache intermediates or fetch them for you.

Merlonix reads the certificate your server actually serves — the whole presented chain, its expiry, and structural problems like a broken leaf-to-intermediate link, an expired intermediate, or a chain that doesn't reach a trusted root — from outside your stack, continuously, and tells you in plain language when it changes, before a customer's client is the thing that notices. You can check a domain's live certificate and DNS right now without signing up, watch a certificate's expiry with the free cert watcher, and the free tools hub has the rest. If you want the deeper picture on the failure this post's cousin covers, what happens when an SSL certificate expires and how to handle SSL expiry during a site migration pick up where a chain problem leaves off.

The padlock in your browser answers "did my browser accept this?" The question that actually matters is "will every client accept it?" — and the only way to answer that is to look at the chain your server hands out, not the one your browser quietly finished for it.