---
title: "Unable to Get Local Issuer Certificate: Causes and Fixes"
description: "Unable to get local issuer certificate means your tool cannot find the CA that signed the server's chain. The three causes and fixes for Python, npm, Git, curl."
url: https://proxynet.io/blog/unable-to-get-local-issuer-certificate
date: 2026-10-06
author: "Acar Diveroli"
category: "Tutorial, Web Scraping"
lang: en
---

# Unable to Get Local Issuer Certificate: Causes and Fixes

You run `npm install` on a work laptop and it stops with `npm error code UNABLE_TO_GET_ISSUER_CERT_LOCALLY`. The registry opens fine in Chrome on the same machine, and a colleague working from home installs the same packages without trouble. A Python script fails with `CERTIFICATE_VERIFY_FAILED`, and `git clone` reports an SSL certificate problem. Nothing is wrong with the registry: your tools and your browser trust different lists of certificate authorities.

This guide explains what the client checks, separates the three causes with errors we reproduced on local test servers, and gives the fix for Python, npm, Git and curl, plus the server-side fix.

> **Note: Short answer**
>
> Your client could not link the server's certificate chain to a root in its own list of trusted roots, the local trust store. Three causes: the server leaves out its intermediate certificate; a TLS-inspecting proxy or antivirus re-signs the traffic with a root your tool lacks; or your tool reads its own CA bundle instead of the system store. Add the right root where the tool looks (`truststore` or `REQUESTS_CA_BUNDLE`, `NODE_EXTRA_CA_CERTS`, Schannel or `http.sslCAInfo`, `--cacert`), or fix the server's chain. `verify=False` and `-k` only hide the error.

## What does "unable to get local issuer certificate" mean?

A TLS certificate names its owner (the subject) and the certificate authority that signed it (the issuer). A server sends its own certificate, called the leaf, plus one or more intermediate certificates that link it to a root certificate. Roots are not sent during the connection: every client keeps its own set of trusted roots, the trust store, and that set is the "local" in the message.

The text is OpenSSL's verify error 20, `X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY`. The client reached a certificate whose issuer it found neither among the certificates the server sent nor in its own store. The handshake stops before your request is sent.

## How does certificate chain verification work?

1. **The server sends its chain.** The leaf comes first, each following certificate signs the one before it, and the root may be left out because clients already have it ([RFC 8446, section 4.4.2](https://www.rfc-editor.org/rfc/rfc8446.html#section-4.4.2)).
2. **The client checks the leaf:** the name must match the host in the URL, and the dates must be valid.
3. **The client finds each issuer** among the certificates sent, checks the signature, and repeats one level up.
4. **The top must end in the trust store,** signed by a root the client already trusts.
5. **A missing link stops the handshake,** and the wording depends on where the chain stopped.

The trust store depends on the tool, not the machine. Requests uses certifi, a Python package with Mozilla's root list; Node.js compiles a snapshot of the Mozilla store into each release; Git for Windows reads its own `ca-bundle.crt` or the Windows store. So one program can fail where another succeeds on the same laptop.

## Three error messages, one family

We created a root, an intermediate and a leaf with OpenSSL, ran three local HTTPS servers that send different parts of the chain, and called them on Windows 11 from Python 3.13.9 (Requests 2.34.2), Node.js 24.11.1 (npm 11.6.2), curl 8.21.0 and Git 2.55 with OpenSSL. No tool knew our root.

| What the server sent | Python (`ssl`, Requests) | Node.js and npm | curl and Git (OpenSSL) |
|---|---|---|---|
| Leaf + intermediate | `unable to get local issuer certificate` | `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` | `unable to get local issuer certificate (20)` |
| Leaf only | `unable to get local issuer certificate` | `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | `unable to get local issuer certificate (20)` |
| Leaf + intermediate + root | `self-signed certificate in certificate chain` | `SELF_SIGNED_CERT_IN_CHAIN` | `self-signed certificate in certificate chain (19)` |

Python and curl use the same words for a missing intermediate and an unknown root, so only the chain tells you which side to fix. The third row is the same problem with the root included: the server, or a proxy in the middle, sent a root your tool does not trust.

## The three causes behind the error

### 1. The server leaves out its intermediate certificate

The administrator installed only the leaf, so every client without the intermediate fails, on every network. Browsers often hide this: Mozilla explains that Firefox ships known intermediates in advance, while other browsers download a missing one in the background. Python, Node.js, curl and Git do neither, so "it works in Chrome" proves nothing.

### 2. A TLS-inspecting proxy or antivirus re-signs the traffic

Company web gateways, antivirus products with HTTPS scanning, and debugging tools such as mitmproxy, Charles and Fiddler open the encrypted connection and present a new certificate for the site, signed by their own root. IT installs that root in the system store of managed laptops, so browsers accept it; tools with their own list do not. Clues: the error appears on the office network or VPN, not at home, and hits almost every site. If you run such a tool yourself, its root certificate step is covered in [What Is a MITM Proxy?](/blog/mitm-proxy)

### 3. Your tool trusts a different list than your system

The bundles of Requests, Node.js and Git never see a root your company installed in Windows or macOS, or a private CA that signs internal services. Python from the python.org macOS installer needs its own certificate step for the same reason, and outdated systems and bundles can lack newer public roots.

## How do you find out which cause you have?

Look at the chain the server really sends. Git for Windows ships `openssl`, so this also runs in Git Bash:

```bash
openssl s_client -connect localhost:47444 -servername localhost -showcerts </dev/null
```

Our server on port 47444 sends only its leaf. The relevant lines:

```text
Certificate chain
 0 s:CN=localhost
   i:O=Example Corp, CN=Example Corp Issuing CA
Verify return code: 21 (unable to verify the first certificate)
```

`s:` is the subject and `i:` the issuer; a complete chain adds an entry `1 s:`. For the inspection case, we ran a local proxy that re-signs traffic like a company gateway, with a CA named Example Corp, and requested `example.com` through it (`-proxy 127.0.0.1:47461`):

```text
Certificate chain
 0 s:CN=example.com
   i:O=Example Corp, CN=Example Corp Issuing CA
 1 s:O=Example Corp, CN=Example Corp Issuing CA
   i:O=Example Corp, CN=Example Corp Root CA
Verify return code: 20 (unable to get local issuer certificate)
```

Through a plain tunnel, the same command showed the site's real chain, issued by `Cloudflare TLS Issuing ECC CA 3`, and `Verify return code: 0 (ok)`. Read your own output like this:

- **Only entry 0, public issuer:** the server leaves out its intermediate (cause 1).
- **Issuer is your company, a security product or a debugging tool:** something re-signs the traffic (cause 2).
- **Complete public chain and `0 (ok)`, but your tool fails:** the tool reads another trust store (cause 3).

## How do you fix it in Python?

Requests wraps the error in a longer line; [Max Retries Exceeded With URL](/blog/max-retries-exceeded-with-url) explains how to read its `Caused by` part. Our test produced:

```text
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=47443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1032)')))
```

If IT has installed the company root in the operating system, let Python use that store. The `truststore` package (Python 3.10 or later) makes Requests and the `ssl` module verify through the system store; its documentation limits `inject_into_ssl()` to applications and scripts, not libraries:

```python
import truststore
truststore.inject_into_ssl()  # call once, at the start of your script

import requests

print(requests.get("https://example.com/", timeout=10).status_code)
```

Otherwise, build a bundle with certifi's roots plus the company root and point `REQUESTS_CA_BUNDLE` at it:

```bash
cat "$(python -m certifi)" corp-root.pem > ca-bundle-plus-corp.pem
export REQUESTS_CA_BUNDLE="$PWD/ca-bundle-plus-corp.pem"
```

Both `example.com` and our test server then returned `200`. Never use the company root alone: the variable replaces certifi, and in our test `example.com` then failed with the same error. Rebuild the file after a certifi upgrade.

Unlike Requests, pip has also used the system certificates since version 24.2 ([pip documentation](https://pip.pypa.io/en/stable/topics/https-certificates/)), so `pip install` can work where Requests fails. Pass `--cert` or set `PIP_CERT` for a specific bundle. On macOS with the python.org installer, run `Install Certificates.command` once.

If a site you collect data from leaves out its intermediate, add it, downloaded from the issuing CA, to the combined file (that made our leaf-only server pass) and tell the owner.

## How do you fix it in Node.js and npm?

The npm output carries the Node.js code:

```text
npm error code UNABLE_TO_GET_ISSUER_CERT_LOCALLY
npm error errno UNABLE_TO_GET_ISSUER_CERT_LOCALLY
npm error request to https://localhost:47443/demo-pkg failed, reason: unable to get local issuer certificate
```

Node.js adds the roots in the file named by `NODE_EXTRA_CA_CERTS` to its built-in list ([Node.js documentation](https://nodejs.org/api/cli.html#node_extra_ca_certsfile)). npm runs on Node.js, so one variable fixes both:

```bash
export NODE_EXTRA_CA_CERTS="$PWD/corp-root.pem"
npm view demo-pkg version --registry https://localhost:47443/
```

The second command printed `1.0.0` (PowerShell: `$env:NODE_EXTRA_CA_CERTS = "C:\certs\corp-root.pem"`). Node.js reads the variable only at startup, not from inside a running script.

npm's own `cafile` setting replaces the trusted list instead. With only the company root in it, our request to the public registry failed:

```text
npm error request to https://registry.npmjs.org/left-pad failed, reason: unable to get local issuer certificate
```

If the root is already in the system store, Node.js can read that store: `--use-system-ca` arrived in Node.js 23.8.0 and `NODE_USE_SYSTEM_CA=1` in 24.6.0 and 22.19.0, and both worked with npm in our test (`NODE_OPTIONS=--use-system-ca`). AI coding assistants built on Node.js read the same variables; Claude Code's documentation lists `NODE_EXTRA_CA_CERTS` for company CAs.

## How do you fix it in Git?

With the OpenSSL backend, Git passes on curl's wording:

```text
fatal: unable to access 'https://localhost:47443/repo.git/': SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
```

On Windows, let Git use the Windows store. The Git for Windows installer offers "Use the OpenSSL library" and "Use the native Windows Secure Channel library"; you can switch later:

```bash
git config --global http.sslBackend schannel
```

If the root is missing there too, Schannel reports `SEC_E_UNTRUSTED_ROOT (0x80090325)`, "The certificate chain was issued by an authority that is not trusted", in the system language. It ignores `http.sslCAInfo` unless `http.schannelUseSSLCAInfo` is set.

On macOS, Linux or the OpenSSL backend, point Git at a file for one server only:

```bash
git config --global http.https://git.example.com/.sslCAInfo /path/to/corp-root.pem
```

In our test, the configured host got past TLS, another host still failed, and `github.com` kept working. If the gateway inspects every site, use Schannel or a combined file (Git's bundle plus the company root).

## How do you fix it in curl?

The curl error number is 60. Our curl 8.21.0 phrases it as below; other builds print `SSL certificate problem: unable to get local issuer certificate`:

```text
curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
More details here: https://curl.se/docs/sslcerts.html
```

- **`--cacert corp-root.pem`** verifies against that file and replaces the default bundle, so use a combined file for public sites too.
- **`--ca-native`** (curl 8.2.0+) adds the system store to OpenSSL builds on Windows, and on macOS when built with Apple's SecTrust.
- **Windows' own `curl.exe`** uses Schannel and the Windows store. With a private CA that publishes no revocation data, it stops at `schannel: the revocation status is unknown`; `--ssl-revoke-best-effort` accepts that and still checks the chain.
- **`--proxy-cacert`** verifies an HTTPS proxy, one reached with an `https://` proxy URL. A plain `http://` proxy has no certificate of its own; [cURL with Proxy](/blog/curl-proxy) covers both.

The curl project's page on [SSL certificate verification](https://curl.se/docs/sslcerts.html) strongly recommends never skipping verification in production.

## Why are verify=False, -k and NODE_TLS_REJECT_UNAUTHORIZED=0 not fixes?

They stop checking the chain instead of repairing it, so the client accepts any certificate for any name, including one from someone on the same network who wants to read your traffic; the Node.js documentation calls `NODE_TLS_REJECT_UNAUTHORIZED=0` insecure in so many words. In our Git test, `http.sslVerify=false` reached the server without a warning, so the broken setup simply stays. Such switches also spread: a variable in a shell profile reaches every Node.js program, and a test's `verify=False` ends up in production.

## How do you send a complete chain from your own server?

Configure the leaf followed by every intermediate, and leave the root out. In nginx, the `ssl_certificate` file holds the server certificate first and the intermediates after it ([nginx documentation](https://nginx.org/en/docs/http/configuring_https_servers.html#chains)); in the wrong order, nginx refuses to start with `key values mismatch`. Certbot writes `fullchain.pem` for this, while `cert.pem` holds the leaf alone; Apache 2.4.8 and later takes `fullchain.pem` in `SSLCertificateFile`. Check from outside with `openssl s_client -connect yourdomain:443 -servername yourdomain -showcerts`: you should see entries `0` and `1` and `Verify return code: 0 (ok)`.

Two 2026 changes make this check worth automating. Since 15 March 2026, the CA/Browser Forum's Baseline Requirements cap public certificates at 200 days (100 from March 2027, 47 from March 2029), so renewals come more often, and each is a chance to deploy the wrong file. On 27 May 2026, Let's Encrypt moved its default profile to Generation Y intermediates, which reach the familiar ISRG roots through cross-signs; Certify The Web, a Windows certificate client, documents Windows servers sending the shorter chain to the new roots, which older clients cannot verify.

## Does a proxy cause this error?

Only a proxy that decrypts traffic. For an `https://` request through an HTTP proxy, the client sends `CONNECT host:443` and the proxy relays encrypted bytes, so you verify the site's own certificate, as our plain tunnel showed. Proxynet's gateways are plain tunnels too: an [HTTPS Proxy](https://proxynet.io/https-proxy) connection needs no root certificate on your machine.

When you scrape through a [Residential Proxy](https://proxynet.io/residential-proxy) pool, the exit address changes but the site's certificate does not, so a chain error on one target follows you to every exit; rotating IPs will not fix it.

## Where you run into this error

- **Package installs on a company laptop:** npm, pip and yarn fail behind a TLS-inspecting gateway while the browser works.
- **CI runners and Docker builds:** a container has its own trust store, so the company root goes into the image.
- **AI coding assistants:** command-line tools built on Node.js call their APIs through the same gateway.
- **API clients:** in Postman, open **Settings**, then **Certificates**, turn on **CA certificates** and select the PEM file.
- **Scrapers:** a site with a missing intermediate fails in every script while it opens in a browser.
- **Internal services:** dashboards and Git servers signed by a company CA.

## Common mistakes

- **Pointing `REQUESTS_CA_BUNDLE`, npm's `cafile` or `--cacert` at the company root alone.** All three replace the default list.
- **Adding the wrong certificate.** The file must hold the root IT publishes, not the leaf of one site.
- **Saving the certificate as binary DER.** These settings expect PEM, the text form starting with `-----BEGIN CERTIFICATE-----`.
- **Setting `NODE_EXTRA_CA_CERTS` inside the script.** Node.js reads it only at startup.
- **Deploying `cert.pem` instead of `fullchain.pem`.** Browsers may still work, so nobody notices.

## Decision guide

| What you see | What to do |
|---|---|
| Only entry `0`, public issuer | Owner serves the full chain; meanwhile add the intermediate to your bundle |
| Issuer is your company, antivirus or debugging tool | Get that root from IT and add it per tool |
| Public chain and `0 (ok)`, one tool fails | Let the tool read the system store: truststore, `--use-system-ca`, Schannel |
| npm fails | `NODE_EXTRA_CA_CERTS`; `cafile` only with a full bundle |
| Git fails for one internal server | `http.<url>.sslCAInfo` for that host |
| curl fails | `--cacert` with a combined file, or `--ca-native` |

## Frequently asked questions

### Why does the site open in my browser but fail in Python, curl or Node.js?

The browser and the tool read different trust stores, and browsers repair incomplete chains themselves. A script reads its own bundle and does neither; compare the chain with `openssl s_client` to see which applies.

### Is it safe to use verify=False, -k or NODE_TLS_REJECT_UNAUTHORIZED=0?

Not as a fix. They turn off every certificate check, so anyone between you and the server can read or change the traffic. Use them at most for one command against your own test server.

### What is the difference between "unable to get local issuer certificate" and "self-signed certificate in certificate chain"?

Both mean the chain ends at a root your tool does not trust. In the first, the root was not sent and could not be found locally; in the second, the server or a proxy sent the root itself. The fix is the same.

### Where do I get my company's root certificate?

From your IT or security team; on a managed laptop it is already in the system store, so truststore, `--use-system-ca` and Schannel work without a file. Never take a root from an unofficial source: a trusted root can sign for any domain.

### Why does pip work when Requests fails on the same machine?

Since version 24.2, pip checks the system certificates as well as certifi, while Requests reads certifi only. The truststore package gives your script the same behaviour.

### Can a proxy cause "unable to get local issuer certificate"?

Only one that decrypts traffic: a company gateway, an antivirus with HTTPS scanning, or a debugging proxy. A tunnelling proxy that uses `CONNECT` passes the site's own certificate through unchanged.

## Summary

The error means your client could not connect the server's certificate to a root it trusts. `openssl s_client` tells you why: a lone leaf points to the server, a company issuer to TLS inspection, a clean public chain to your tool's own bundle. Add the right root where that tool looks and leave verification on. For proxies that tunnel traffic without touching certificates, see our [proxy services](/proxy).
