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
Platforms
  • Linux
  • macOS
  • Windows
  • Python
  • Node.js
Who it is for
Level
Practitioner · 7 steps
On this page
  1. Before you start
  2. Steps
  3. 1. Know where each one looks
  4. 2. Python: use the operating system’s store with truststore
  5. 3. Python: or point it at a bundle file
  6. 4. pip
  7. 5. Node.js: add your root, or use the system store
  8. 6. npm, Yarn and pnpm
  9. 7. Make it stick
  10. Check it worked
  11. If something goes wrong
  12. Why this works
  13. Go deeper

Before you start

  • The root certificate of your private or corporate CA as a PEM file, for example corp-root.crt. Check its fingerprint first (Linux, Windows, 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 or UNABLE_TO_GET_ISSUER_CERT_LOCALLY.

Steps

1. Know where each one looks

Runtime or library Default trust store Change it with
Python ssl, urllib OpenSSL’s default file and folder; on Windows also the Windows store (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) 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 lets requests, urllib3 and the standard library verify through the operating system:

python -m pip install truststore
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:

REQUESTS_CA_BUNDLE=corp-root.crt python -c "import requests; print(requests.get('https://pypi.org/simple/').status_code)"
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, that is the system bundle:

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):

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 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:

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), so public sites keep working. On Windows with Node.js 23.11:

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))"
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:

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); 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:

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:

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
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 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

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

Try it: Chain checkerOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere.

Every certificate the server sends, leaf first. The output of openssl s_client -showcerts -connect example.com:443 works as it is: we ignore the text around the certificates.

Why this works

Each runtime builds a 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?, and how the chain is checked is in Certificate chains and path validation.

Go deeper

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9