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
- Level
- Practitioner · 7 steps
On this page
- Before you start
- Steps
- 1. Know where each one looks
- 2. Python: use the operating system’s store with truststore
- 3. Python: or point it at a bundle file
- 4. pip
- 5. Node.js: add your root, or use the system store
- 6. npm, Yarn and pnpm
- 7. Make it stick
- Check it worked
- If something goes wrong
- Why this works
- 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
- Still
CERTIFICATE_VERIFY_FAILEDorSELF_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_SIGNATUREor “unable to get local issuer certificate” with the right root. The server leaves out its intermediate. Fix the server: build the full chain file.- Public sites broke. Your bundle replaced the defaults (step 3) or your code sets
ca(step 5). Hostname mismatchorERR_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.
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
- Python
ssl: load_default_certs: what the standard library loads on each platform. - Requests: SSL certificate verification:
verifyandREQUESTS_CA_BUNDLE. - truststore: using the operating system’s store from Python.
- pip: HTTPS certificates: system certificates since pip 24.2,
--certandPIP_CERT. - Node.js command-line options:
NODE_EXTRA_CA_CERTS,--use-system-caandNODE_USE_SYSTEM_CA. - npm config:
ca,cafileandstrict-ssl.
Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.