<!-- https://pkizerotohero.com/guides/trust-ca-python-node/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Make Python, pip, requests, Node.js and npm trust a private root CA with truststore, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS or --use-system-ca.

Source: https://pkizerotohero.com/guides/trust-ca-python-node/ · Last reviewed: 2026-10-20

How-to guide · Trust a CA

# How to make Python, Node.js and npm trust a private CA

**In one sentence:**

Python, pip, requests, Node.js and npm trust your private root CA, through the operating system's store or a bundle file, with verification still fully on.

Task

[Trust a CA](https://pkizerotohero.com/guides/#task-trust)

Platforms

- Linux
- macOS
- Windows
- Python
- Node.js

Who it is for

- [Developer](https://pkizerotohero.com/start/#developer)
- [IT ops](https://pkizerotohero.com/start/#itops)

Level

Practitioner · 7 steps

## Before you start

- The [root certificate](https://pkizerotohero.com/handbook/glossary/#root-certificate) of your private or corporate CA as a PEM file, for example `corp-root.crt`. Check its [fingerprint](https://pkizerotohero.com/handbook/glossary/#fingerprint) first ([Linux](https://pkizerotohero.com/guides/add-root-ca-linux/), [Windows](https://pkizerotohero.com/guides/add-root-ca-windows/), [macOS](https://pkizerotohero.com/guides/add-root-ca-macos/)).
- Ideally, the root already added to your operating system’s store, because then two of the options below need no file at all.
- Python 3.10 or later for `truststore`; Node.js 22.15 or later for `--use-system-ca`.

Why it matters: Python’s `requests` and Node.js ship their own lists of public roots, so a root your browser trusts can still fail in code with [`CERTIFICATE_VERIFY_FAILED`](https://pkizerotohero.com/errors/python-certificate-verify-failed/) or [`UNABLE_TO_GET_ISSUER_CERT_LOCALLY`](https://pkizerotohero.com/errors/npm-unable-to-get-issuer-cert-locally/).

## Steps

### 1. Know where each one looks

| Runtime or library | Default [trust store](https://pkizerotohero.com/handbook/glossary/#trust-store) | Change it with |
| --- | --- | --- |
| Python `ssl`, `urllib` | OpenSSL’s default file and folder; on Windows also the Windows store ([`load_default_certs`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_default_certs)) | `SSL_CERT_FILE`, `SSL_CERT_DIR` |
| `requests` (and libraries built on it) | certifi, a package that ships Mozilla's curated set of root certificates as one cacert.pem file | `truststore`, `REQUESTS_CA_BUNDLE`, `verify=` |
| pip 24.2 and later | The system store **plus** certifi ([pip](https://pip.pypa.io/en/stable/topics/https-certificates/)) | Add the root to the OS store, or `--cert` |
| Node.js | Mozilla’s roots, compiled into the `node` binary | `NODE_EXTRA_CA_CERTS`, `--use-system-ca` |
| npm, Yarn, pnpm | Node.js’s rules | The same, or npm’s `cafile` |

### 2. Python: use the operating system’s store with truststore

If the root is already in the Windows, macOS or Linux store, [truststore](https://truststore.readthedocs.io/) lets `requests`, `urllib3` and the standard library verify through the operating system:

```bash
python -m pip install truststore
```

```python
import truststore
truststore.inject_into_ssl()   # call once, at program start

import requests
print(requests.get("https://intranet.example.com/").status_code)
```

On our Ubuntu test machine, with the root in the system store, a pip-installed `requests` failed with `CERTIFICATE_VERIFY_FAILED` on its own, and printed `200` after `inject_into_ssl()`.

### 3. Python: or point it at a bundle file

`REQUESTS_CA_BUNDLE` (for `requests`) **replaces** the default list. A file with only your root fixes internal sites and breaks public ones. On Windows we tested exactly that:

```bash
REQUESTS_CA_BUNDLE=corp-root.crt python -c "import requests; print(requests.get('https://pypi.org/simple/').status_code)"
```

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

`SSL_CERT_FILE` (for `ssl` and `urllib`) replaces only OpenSSL’s default file. In our tests Python still trusted public sites through the Windows store on Windows, and through the default certificate folder on Ubuntu unless `SSL_CERT_DIR` pointed elsewhere too. Do not rely on either behaviour: use a bundle that holds the public roots **and** yours. On Linux, after [adding the root to the system store](https://pkizerotohero.com/guides/add-root-ca-linux/), that is the system bundle:

```bash
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt   # Debian, Ubuntu
export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt     # RHEL family
```

Elsewhere, build one from certifi plus your root, outside the certifi folder (an upgrade replaces that file):

```bash
cat "$(python -c 'import certifi; print(certifi.where())')" corp-root.crt > ca-bundle-plus-corp.pem
export REQUESTS_CA_BUNDLE="$PWD/ca-bundle-plus-corp.pem"
```

With that file, both the internal test server and `pypi.org` returned `200`. In code you can pass the same file explicitly: `requests.get(url, verify="/path/ca-bundle-plus-corp.pem")`. The [requests documentation](https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification) notes that prepared requests ignore `REQUESTS_CA_BUNDLE` unless you merge the environment settings.

### 4. pip

pip 24.2 and later already use the operating system’s store in addition to certifi, so adding the root to the OS store is usually enough. Against our test index, pip 26.2 failed with `There was a problem confirming the ssl certificate` before the root was in Ubuntu’s store, and reached the index after. For older pip, or a root that is not in the OS store:

```bash
python -m pip config set global.cert /path/ca-bundle-plus-corp.pem
```

`PIP_CERT` does the same for one shell or CI job.

### 5. Node.js: add your root, or use the system store

`NODE_EXTRA_CA_CERTS` adds the certificates in a PEM file to Node’s built-in list ([Node.js](https://nodejs.org/api/cli.html#node_extra_ca_certsfile)), so public sites keep working. On Windows with Node.js 23.11:

```bash
NODE_EXTRA_CA_CERTS=corp-root.crt node -e "require('https').get('https://localhost:18444/', r => { console.log(r.statusCode); process.exit(0) }).on('error', e => console.log(e.code))"
```

```text
200
```

Without the variable the same command printed `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`. Node reads the variable only when the process starts, and only warns if the file is missing:

```text
Warning: Ignoring extra certs from `nope.crt`, load failed: error:80000002:system library::No such file or directory
```

If the root is already in the operating system’s store, `--use-system-ca` adds that store instead ([Node.js](https://nodejs.org/api/cli.html#--use-system-ca)); the `NODE_USE_SYSTEM_CA` variable needs Node.js 22.19 or 24.6. With Node.js 24.22 on Ubuntu, these three forms all returned `200`:

```bash
node --use-system-ca app.js
NODE_USE_SYSTEM_CA=1 node app.js
NODE_OPTIONS=--use-system-ca npm install
```

If your code passes a `ca` option to `https` or `tls`, Node uses only that list and ignores both settings. Include the defaults: `ca: [...tls.rootCertificates, fs.readFileSync('corp-root.crt')]`.

### 6. npm, Yarn and pnpm

They run on Node.js, so `NODE_EXTRA_CA_CERTS` and `NODE_OPTIONS=--use-system-ca` work for them too. With npm 11.10:

```text
npm error code UNABLE_TO_GET_ISSUER_CERT_LOCALLY
npm error request to https://localhost:18444/-/ping failed, reason: unable to get local issuer certificate
```

```text
npm notice PING https://localhost:18444/
npm notice PONG 251ms
```

The first is `npm ping` without the variable, the second with it. npm’s own `cafile` setting [trusts only the CAs in that file](https://docs.npmjs.com/cli/v11/using-npm/config#ca) for the registry, so it must contain every root you need.

### 7. Make it stick

Put the variables where your programs start: your shell profile, an `Environment=` line in the systemd unit, `ENV` in the Dockerfile, or your CI’s variables.

## Check it worked

```bash
python -c "import requests; print(requests.get('https://intranet.example.com/').status_code)"
node -e "require('https').get('https://intranet.example.com/', r => { console.log(r.statusCode); process.exit(0) })"
```

Both should print a status code, not an SSL error. Then test one public site, to prove you did not replace the public roots.

## If something goes wrong

- **Still [`CERTIFICATE_VERIFY_FAILED`](https://pkizerotohero.com/errors/python-certificate-verify-failed/) or [`SELF_SIGNED_CERT_IN_CHAIN`](https://pkizerotohero.com/errors/node-self-signed-cert-in-chain/).** The variable is not set in the process that fails (a service, an IDE, a CI step), or the file is not the root that issued the server’s chain. Paste the chain into the chain checker below.
- **[`UNABLE_TO_VERIFY_LEAF_SIGNATURE`](https://pkizerotohero.com/errors/node-unable-to-verify-leaf-signature/) or “unable to get local issuer certificate” with the right root.** The server leaves out its intermediate. Fix the server: [build the full chain file](https://pkizerotohero.com/guides/build-fullchain/).
- **Public sites broke.** Your bundle replaced the defaults (step 3) or your code sets `ca` (step 5).
- **[`Hostname mismatch`](https://pkizerotohero.com/errors/python-hostname-mismatch/) or [`ERR_TLS_CERT_ALTNAME_INVALID`](https://pkizerotohero.com/errors/node-err-tls-cert-altname-invalid/).** Trust is fine; the server certificate lacks the name in its Subject Alternative Name.
- **Behind a TLS-inspecting proxy.** The root you need is the proxy’s: see [corporate proxy certificates](https://pkizerotohero.com/guides/corporate-proxy-certificates/).

*Interactive tool: Chain checker (runs in the browser at [/tools/chain/](https://pkizerotohero.com/tools/chain/))*

Warning

verify=False and NODE\_TLS\_REJECT\_UNAUTHORIZED=0 are traps

`verify=False`, `NODE_TLS_REJECT_UNAUTHORIZED=0` and `npm config set strict-ssl false` do not trust your CA; they trust everyone, including whoever intercepts the connection to your package registry. They also tend to stay in place long after the test. Add the one root you need instead.

## Why this works

Each runtime builds a [certificate chain](https://pkizerotohero.com/handbook/glossary/#certificate-chain) from the server’s certificate to one of the roots it loaded, and the libraries differ only in where those roots come from. The options above add your root to that list, or swap the list for the operating system’s, while verification stays fully on. Why so many stores exist is in [Who do we trust?](https://pkizerotohero.com/handbook/trust-stores/), and how the chain is checked is in [Certificate chains and path validation](https://pkizerotohero.com/handbook/certificate-chains/).

## Go deeper

- [Python `ssl`: load\_default\_certs](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_default_certs): what the standard library loads on each platform.
- [Requests: SSL certificate verification](https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification): `verify` and `REQUESTS_CA_BUNDLE`.
- [truststore](https://truststore.readthedocs.io/): using the operating system’s store from Python.
- [pip: HTTPS certificates](https://pip.pypa.io/en/stable/topics/https-certificates/): system certificates since pip 24.2, `--cert` and `PIP_CERT`.
- [Node.js command-line options](https://nodejs.org/api/cli.html#node_extra_ca_certsfile): `NODE_EXTRA_CA_CERTS`, `--use-system-ca` and `NODE_USE_SYSTEM_CA`.
- [npm config](https://docs.npmjs.com/cli/v11/using-npm/config#cafile): `ca`, `cafile` and `strict-ssl`.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/trust-ca-python-node/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 12: Who do we trust? CAs, trust stores and self-signed certificates](https://pkizerotohero.com/handbook/trust-stores/)
- [Chapter 13: Certificate chains and path validation](https://pkizerotohero.com/handbook/certificate-chains/)

### Error pages

- [\[SSL: CERTIFICATE\_VERIFY\_FAILED\] certificate verify failed: unable to get local issuer certificate](https://pkizerotohero.com/errors/python-certificate-verify-failed/)
- [SELF\_SIGNED\_CERT\_IN\_CHAIN](https://pkizerotohero.com/errors/node-self-signed-cert-in-chain/)
- [UNABLE\_TO\_GET\_ISSUER\_CERT\_LOCALLY](https://pkizerotohero.com/errors/npm-unable-to-get-issuer-cert-locally/)
- [UNABLE\_TO\_VERIFY\_LEAF\_SIGNATURE](https://pkizerotohero.com/errors/node-unable-to-verify-leaf-signature/)
- [certificate verify failed: Hostname mismatch, certificate is not valid for '…'](https://pkizerotohero.com/errors/python-hostname-mismatch/)
- [ERR\_TLS\_CERT\_ALTNAME\_INVALID](https://pkizerotohero.com/errors/node-err-tls-cert-altname-invalid/)

### Tools

- [Chain checker](https://pkizerotohero.com/tools/chain/): Paste a chain and see which link is missing, expired or out of order.
- [Paste your error](https://pkizerotohero.com/tools/error-finder/): Paste a certificate error from any tool and get the page that explains it.

### Topics

- [Chains and trust stores](https://pkizerotohero.com/topics/chains-and-trust/)

## More guides: Trust a CA

- [How to work behind a TLS-inspecting corporate proxy, safely](https://pkizerotohero.com/guides/corporate-proxy-certificates/)
- [How to trust a private root CA on Windows](https://pkizerotohero.com/guides/add-root-ca-windows/)
- [How to trust a private root CA on macOS](https://pkizerotohero.com/guides/add-root-ca-macos/)
- [How to trust a private root CA on Linux](https://pkizerotohero.com/guides/add-root-ca-linux/)
- [How to add a CA to a Java trust store (cacerts)](https://pkizerotohero.com/guides/add-root-ca-java/)
