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

> How trust stores work in browsers, operating systems, Java, Node.js and Python, why private roots must be installed, and why -k is a trap.

Source: https://pkizerotohero.com/handbook/trust-stores/ · Last reviewed: 2026-10-20

Part IV: Trust and HTTPS · Chapter 12

# Who do we trust? CAs, trust stores and self-signed certificates

**In one sentence:**

Every client trusts only the root certificates in its own trust store, so a certificate is accepted only if its chain ends at one of them; being self-signed earns nothing.

## Who is this for

- [Developer](https://pkizerotohero.com/start/#developer)
- [IT ops](https://pkizerotohero.com/start/#itops)
- [Expert](https://pkizerotohero.com/start/#expert)
- [Beginner](https://pkizerotohero.com/start/#beginner)
- [Leader](https://pkizerotohero.com/start/#leader)

Level: **Practitioner**

- [New to this? Start with The idea](https://pkizerotohero.com/handbook/trust-stores/#the-idea)
- [Developer? Jump to How it really works](https://pkizerotohero.com/handbook/trust-stores/#how-it-really-works)
- [IT ops? At work](https://pkizerotohero.com/handbook/trust-stores/#at-work)
- [Leader? Why it matters](https://pkizerotohero.com/handbook/trust-stores/#why-it-matters-to-the-business)

## The idea

- Beginner

A certificate is signed by a CA. That CA’s certificate is signed by another CA, and so on, until you reach a certificate that signed itself: a [root certificate](https://pkizerotohero.com/handbook/glossary/#root-certificate). So why trust the root?

Not because it signed itself. Anyone can make a self-signed certificate in one command, with any name they like, including “Very Trustworthy Root CA”. A root is trusted for one reason only: **it is already on a list inside your device or your software**. That list is called a [trust store](https://pkizerotohero.com/handbook/glossary/#trust-store).

Who decides what goes on the list?

- **Browser and operating-system makers** run root programs. Mozilla, Google (for Chrome), Apple and Microsoft each decide which public CAs to include. A CA must pass yearly audits and follow strict rules to stay in.
- **Your organisation’s IT team** can add its own roots to company devices, for example for internal websites.
- **You**, when you install a root on your own computer.

Here is the catch: there is no single list. Your phone, your laptop, the Java application on a server and the Docker container next to it can each have a different trust store. The same website can be trusted by one of them and rejected by another, and both are behaving correctly.

A [self-signed certificate](https://pkizerotohero.com/handbook/glossary/#self-signed-certificate) on a server, one that no CA vouched for, is rejected everywhere it has not been installed. The warning does not mean the encryption is weak. It means nobody your device trusts has confirmed that the key belongs to that name, so you could be talking to anyone, including someone in the middle.

In the game

In the game: the Registry of Roots

In Level 12, Ken decides which roots go into the kingdom’s trust store, and finds that two gatehouses with different lists disagree about the same visitor. Mallory runs a fake castle gate with a root she made herself, and nobody’s list includes it.

Where the analogy stops being true: in the game, Ken weighs each root himself. In real life, most people never see their trust store. Vendors and IT teams choose it for them, and one root added to it, even by accident, is trusted for every website.

## How it really works

- Developer
- Expert

### What is a trust anchor?

Path validation in [RFC 5280 section 6.1](https://www.rfc-editor.org/rfc/rfc5280#section-6.1) starts from a **trust anchor**: a CA name and public key that the client accepts without further proof. Trust anchors are usually distributed as self-signed root certificates, but the self-signature adds no trust. It shows only that the file is intact and was made by whoever holds the root’s private key.

Everything else follows from the anchor. A client accepts a server certificate only if it can build a chain of valid signatures from that certificate up to one of its anchors ([chapter 13](https://pkizerotohero.com/handbook/certificate-chains/)).

### Who decides which roots are public?

Four root programs cover almost every device:

- the [Mozilla Root Store Policy](https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/policy/), used by Firefox and, through the NSS library, by many Linux distributions;
- the [Chrome Root Program](https://googlechrome.github.io/chromerootprogram/), whose root store Chrome uses on Windows, macOS, ChromeOS, Linux and Android (Apple’s rules prevent it on iOS);
- the [Apple Root Certificate Program](https://www.apple.com/certificateauthority/ca_program.html), for macOS, iOS and Safari;
- the [Microsoft Trusted Root Program](https://learn.microsoft.com/en-us/security/trusted-root/program-requirements), for Windows.

Each program requires the CA to follow the CA/Browser Forum’s [Baseline Requirements](https://cabforum.org/working-groups/server/baseline-requirements/requirements/), to pass independent audits, and to report incidents. When a CA breaks the rules badly, the programs remove it ([chapter 18](https://pkizerotohero.com/handbook/famous-failures/)).

### Which trust store does my software use?

This is where most “it works on my machine” problems come from:

| Client | Default trust store | How to add a private root |
| --- | --- | --- |
| Chrome | Chrome Root Store, plus roots that users or administrators added to the operating system | Add it to the OS store (Windows, macOS) |
| Firefox | Mozilla’s built-in list; on Windows, macOS and Android it also reads roots added to the OS by default | OS store, or Firefox settings or policy |
| Windows apps (.NET, PowerShell, IIS, curl with Schannel) | Windows certificate store (Trusted Root Certification Authorities) | `certutil -addstore`, Group Policy, Intune |
| macOS and iOS apps | Apple’s trust store and the keychains | `security add-trusted-cert`, MDM profile |
| Linux tools (curl, wget, git) built with OpenSSL | The distribution’s CA bundle file | `update-ca-certificates` or `update-ca-trust` |
| Java | `cacerts` in `$JAVA_HOME/lib/security`, shipped with the JDK | `keytool -importcert -cacerts` |
| Node.js | A snapshot of Mozilla’s store, built into Node and “fixed at release time” | `NODE_EXTRA_CA_CERTS`, or `--use-system-ca` |
| Python `ssl` module | OpenSSL’s default paths; on Windows also the system’s CA and ROOT stores | `SSL_CERT_FILE`, or `cafile=` in code |
| Python `requests` | The `certifi` package, a copy of Mozilla’s list | `REQUESTS_CA_BUNDLE`, or `verify=` with a path |
| Go | The system’s roots; on Linux, the usual bundle files | `SSL_CERT_FILE`, `SSL_CERT_DIR` |

Three details catch people out:

- **Node.js** reads `NODE_EXTRA_CA_CERTS` “only when the Node.js process is first launched”, and ignores it completely when code passes its own `ca` option ([Node.js CLI docs](https://nodejs.org/api/cli.html#node_extra_ca_certsfile)). Since Node 23.8, `--use-system-ca` adds the operating system’s roots.
- **Python `requests` does not use the operating system’s store.** It uses `certifi`, so a root installed on Windows or in `/etc/ssl` is invisible to it ([Requests docs](https://requests.readthedocs.io/en/latest/user/advanced/#ca-certificates)).
- **Minimal container images** often contain no CA bundle at all, so every HTTPS call fails with `x509: certificate signed by unknown authority` until you install the `ca-certificates` package.

*Diagram: On the left, a company server sends a chain that ends at the private Corp Root CA. On the right, four clients on the same laptop. The browser uses the operating system store, which contains Corp Root CA: trusted, marked with a tick. The Node.js app uses Node's bundled Mozilla list: rejected, marked with a cross. Python requests uses certifi: rejected, cross. The Java app uses its cacerts file: rejected, cross. A note says each client needs the root added to its own store.*

One server, one chain, four answers. Each client checks against its own trust store.

### What do self-signed errors look like?

There are two cases, with different messages:

- The server sends **only a self-signed certificate**. OpenSSL reports `verify error:num=18:self-signed certificate`, and Node.js reports `DEPTH_ZERO_SELF_SIGNED_CERT`.
- The server sends a chain that **ends in a root the client does not trust**. OpenSSL reports `verify error:num=19:self-signed certificate in certificate chain`, curl says [`SSL certificate problem: self-signed certificate in certificate chain`](https://pkizerotohero.com/errors/curl-self-signed-certificate-in-certificate-chain/), and Node.js says [`SELF_SIGNED_CERT_IN_CHAIN`](https://pkizerotohero.com/errors/node-self-signed-cert-in-chain/).

When the root is not sent at all, the same root cause shows up as “unable to get local issuer certificate” instead. In every case, the fix is to make the client trust the right root, not to switch checking off.

### Why do private roots exist?

Organisations run their own CAs for internal names, devices and staff ([chapter 22](https://pkizerotohero.com/handbook/private-ca/)). A private root works exactly like a public one, but only inside trust stores where someone installed it. That is the point: an internal CA should not be able to issue certificates that the whole internet trusts. The cost is distribution. Every device, runtime and container that talks to internal services needs that root.

### What is a TLS-inspection proxy?

Many companies route web traffic through a proxy that decrypts and inspects it. The proxy holds a private root that is installed on every company laptop. When you open `https://example.com`, the proxy connects to the real site, checks its certificate, then gives your laptop a new certificate for `example.com`, issued on the spot by the proxy’s root. Your browser accepts it, because the OS store contains that root.

Tools with their own trust store do not. Node.js, Python `requests`, Java, git and containers on the same laptop fail with “unable to get local issuer certificate” or “self-signed certificate in certificate chain”, while the browser works. The fix is to add the company root to those tools’ stores. Keep two facts in mind: anyone who controls a root in your store can read your TLS traffic, so install only roots your organisation controls; and the proxy now makes the trust decisions for you. Chrome, for example, honours locally added roots for TLS over TCP but not for QUIC, the transport of HTTP/3 ([Chrome Root Store FAQ](https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/faq.md)).

Warning

Why -k, --insecure and verify=False are a trap

`curl -k`, `wget --no-check-certificate`, Python’s `verify=False` and `NODE_TLS_REJECT_UNAUTHORIZED=0` all turn off the check that you are talking to the real server. curl’s manual says it plainly: “WARNING: using this option makes the transfer insecure.” The Requests documentation warns that `verify=False` makes your application “vulnerable to man-in-the-middle (MitM) attacks”, and Node.js calls the variable “strongly discouraged”. The connection is still encrypted, but possibly to an attacker. These switches also tend to survive into production. Instead, give the client the right root: `curl --cacert`, `REQUESTS_CA_BUNDLE`, `NODE_EXTRA_CA_CERTS`, or the system store.

## At work

- Developer
- IT ops

### Which store is my client using?

Ask each runtime. These commands ran on a Windows laptop in Git Bash; on Linux the paths differ:

```bash
openssl version -d
python -c "import ssl; print(ssl.get_default_verify_paths())"
python -c "import certifi; print(certifi.where())"
node -p "require('tls').rootCertificates.length"
```

```text
OPENSSLDIR: "/mingw64/etc/ssl"
DefaultVerifyPaths(cafile=None, capath=None, openssl_cafile_env='SSL_CERT_FILE', openssl_cafile='C:\\Program Files\\Common Files\\SSL/cert.pem', openssl_capath_env='SSL_CERT_DIR', openssl_capath='C:\\Program Files\\Common Files\\SSL/certs')
C:\Users\…\Python313\Lib\site-packages\certifi\cacert.pem
150
```

Four answers, four different places: OpenSSL’s own folder, Python’s OpenSSL defaults (plus the Windows store, which `ssl` loads on Windows), certifi’s bundle for `requests`, and 150 roots compiled into this Node.js 23 binary. For Java, `keytool -list -cacerts` lists the JDK’s roots.

### What happens with a private root?

We ran a test server whose chain ends in a private root, “Keystone Demo Root CA”, and sent the whole chain, root included. Nothing on the laptop trusts that root yet:

```bash
openssl s_client -connect 127.0.0.1:8443 -servername www.example.com </dev/null
```

```text
depth=2 C=BE, O=Keystone Demo, CN=Keystone Demo Root CA
verify error:num=19:self-signed certificate in certificate chain
...
Verify return code: 19 (self-signed certificate in certificate chain)
```

Node.js and Python give the same verdict in their own words:

```text
SELF_SIGNED_CERT_IN_CHAIN: self-signed certificate in certificate chain
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1028)
```

A server that presents only a self-signed certificate, with no chain, fails slightly differently:

```text
verify error:num=18:self-signed certificate
DEPTH_ZERO_SELF_SIGNED_CERT: self-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca
```

Now give Node.js the root, and only the root, as an extra trusted certificate:

```bash
NODE_EXTRA_CA_CERTS=root.crt node -e "const s=require('tls').connect({host:'127.0.0.1',port:8443,servername:'www.example.com'},()=>{console.log('OK, authorized:',s.authorized);s.end()});s.on('error',e=>console.log(e.code+':',e.message))"
```

```text
OK, authorized: true
```

Verification is still fully on. The client now simply knows one more anchor.

### How do I add a private root?

First, check that the file is the root you expect. Compare its fingerprint with the value your IT team publishes through a channel you trust, as the [keytool documentation](https://docs.oracle.com/en/java/javase/21/docs/specs/man/keytool.html) also advises:

```bash
openssl x509 -in corp-root.crt -noout -subject -fingerprint -sha256
```

Then add it to every store your clients use. The file must be the PEM root certificate, never a private key:

```bash
# Debian and Ubuntu: the file name must end in .crt
sudo cp corp-root.crt /usr/local/share/ca-certificates/corp-root.crt
sudo update-ca-certificates

# Red Hat, Fedora and similar
sudo cp corp-root.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

# macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain corp-root.crt

# Java (JDK 17 and older, and Debian/Ubuntu packages, ask for the cacerts password "changeit"; newer JDK builds such as Temurin 18+ have none)
keytool -importcert -cacerts -alias corp-root -file corp-root.crt

# Node.js, for processes started from this shell
export NODE_EXTRA_CA_CERTS=/path/to/corp-root.crt

# Python requests: point it at a bundle that contains the root
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
```

```powershell
# Windows, as administrator: the computer's Trusted Root store
Import-Certificate -FilePath .\corp-root.crt -CertStoreLocation Cert:\LocalMachine\Root
```

On managed fleets, distribute roots with Group Policy, Intune or an MDM profile rather than by hand, and remember to bake them into container images.

### Which errors point here?

An untrusted or missing root produces most of the classic messages:

- browsers: [`NET::ERR_CERT_AUTHORITY_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-authority-invalid/) and [`SEC_ERROR_UNKNOWN_ISSUER`](https://pkizerotohero.com/errors/firefox-sec-error-unknown-issuer/);
- tools: [curl’s self-signed chain error](https://pkizerotohero.com/errors/curl-self-signed-certificate-in-certificate-chain/), [`SELF_SIGNED_CERT_IN_CHAIN`](https://pkizerotohero.com/errors/node-self-signed-cert-in-chain/), [npm’s `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`](https://pkizerotohero.com/errors/npm-unable-to-get-issuer-cert-locally/) and [Go’s `x509: certificate signed by unknown authority`](https://pkizerotohero.com/errors/go-x509-unknown-authority/);
- runtimes: [Java’s `PKIX path building failed`](https://pkizerotohero.com/errors/java-pkix-path-building-failed/), [Python’s `CERTIFICATE_VERIFY_FAILED`](https://pkizerotohero.com/errors/python-certificate-verify-failed/) and [.NET’s `The remote certificate is invalid`](https://pkizerotohero.com/errors/dotnet-remote-certificate-invalid/).

If the root is trusted but the error persists, the server is probably not sending its intermediate. That is the next chapter.

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

Paste the chain your server sends, from `openssl s_client -showcerts`. The checker shows where the chain ends and whether that root is publicly trusted.

## Common mistakes

- **“A root is trusted because it is self-signed.”** A root is trusted only because it is already in the trust store. Anyone can make a self-signed certificate.
- **“Self-signed means weak encryption.”** The encryption can be just as strong. Self-signed means that nobody your device trusts has vouched for the name.
- **“Every device trusts the same roots.”** Browsers, operating systems, Java, Node.js, Python and containers each have their own stores, with different contents and update cycles.
- **“I installed the root in Windows, so all my tools trust it.”** Node.js, Python `requests`, Java and many containers ignore the Windows store unless you configure them.
- **“`-k` is fine for internal services.”** Internal networks have attackers too, and the setting tends to reach production. Trust the private root instead.
- **“A private root only affects our internal sites.”** A root in your store can vouch for any name, including your bank’s. Install only roots you control, and prefer CAs with name constraints ([chapter 22](https://pkizerotohero.com/handbook/private-ca/)).

## Why it matters to the business

- Leader

Why it matters to the business

Your trust store is a security boundary

Every root in a device’s trust store can vouch for any website, so whoever controls a root can read or fake that device’s encrypted traffic. Public roots are vetted by browser and OS root programs. Private roots, including those of TLS-inspection proxies, are your organisation’s own responsibility. Decide which private roots exist, who holds their keys, and how they reach every device and container. Make disabled certificate checking in code a policy violation, because it quietly removes this protection.

## Check yourself

Check yourself

Why does your laptop trust a public root CA certificate?

1. Because the root certificate is self-signed
2. Because it ships in your operating system's or browser's trust store
3. Because its key is longer than other keys

Show answer

Answer B Because it ships in your operating system's or browser's trust store

What happened

It is trusted because it is in the trust store.

Why

A self-signature proves nothing about trust: anyone can make one. Root programs decide which roots ship in the store.

The rule

A root is trusted only because it is already in the trust store.

Check yourself

Your company inspects TLS with a proxy. Chrome works, but npm install fails with UNABLE\_TO\_GET\_ISSUER\_CERT\_LOCALLY. What is the best fix?

1. Turn off strict SSL checking in npm
2. Add the company's root certificate to Node's trust, for example with NODE\_EXTRA\_CA\_CERTS
3. Reinstall Chrome

Show answer

Answer B Add the company's root certificate to Node's trust, for example with NODE\_EXTRA\_CA\_CERTS

What happened

Give Node.js the company root, so it can verify the proxy's certificates.

Why

Chrome trusts the root through the operating system store. Node.js uses its own bundled list, which lacks the company root.

The rule

Fix trust errors by adding the right root to the client's store, never by turning verification off.

Check yourself

A developer adds verify=False to fix CERTIFICATE\_VERIFY\_FAILED when calling an internal API. What is the risk?

1. None, because the traffic is still encrypted
2. Anyone who can intercept the traffic can pose as the API and read or change the data
3. The API will refuse the connection

Show answer

Answer B Anyone who can intercept the traffic can pose as the API and read or change the data

What happened

The client now accepts any certificate, including an attacker's.

Why

Encryption without verification protects you only from passive listeners. A man-in-the-middle simply presents its own certificate.

The rule

Keep verification on and trust the right root instead.

## Go deeper

- [RFC 5280, section 6.1](https://www.rfc-editor.org/rfc/rfc5280#section-6.1): path validation and the role of trust anchors.
- [Mozilla Root Store Policy](https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/policy/) and [Chrome Root Program](https://googlechrome.github.io/chromerootprogram/): the rules public CAs must follow to be included.
- [Chrome Root Store FAQ](https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/faq.md): where Chrome’s roots come from and how locally added roots are treated.
- [Apple Root Certificate Program](https://www.apple.com/certificateauthority/ca_program.html) and [Microsoft Trusted Root Program](https://learn.microsoft.com/en-us/security/trusted-root/program-requirements): the other two major root programs.
- [Node.js: `NODE_EXTRA_CA_CERTS`](https://nodejs.org/api/cli.html#node_extra_ca_certsfile) and [`--use-system-ca`](https://nodejs.org/api/cli.html#--use-system-ca): adding roots to Node.js.
- [Python `ssl`](https://docs.python.org/3/library/ssl.html) and [Requests: CA certificates](https://requests.readthedocs.io/en/latest/user/advanced/#ca-certificates): where Python finds its roots.
- [Java `keytool`](https://docs.oracle.com/en/java/javase/21/docs/specs/man/keytool.html): the `cacerts` keystore and importing trusted certificates.
- [curl: `--cacert` and `--insecure`](https://curl.se/docs/manpage.html): giving curl a CA bundle, and why skipping verification is unsafe.

The crash course covers trust stores in [lesson 6, Chains and trust stores](https://pkizerotohero.com/course/chains-and-trust-stores/).

**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=/handbook/trust-stores/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

Crash course: [Lesson 6: Chains and trust stores](https://pkizerotohero.com/course/chains-and-trust-stores/)

### Error pages for this chapter

- [SSL certificate problem: self-signed certificate in certificate chain](https://pkizerotohero.com/errors/curl-self-signed-certificate-in-certificate-chain/)
- [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/)
- [x509: certificate signed by unknown authority](https://pkizerotohero.com/errors/go-x509-unknown-authority/)
- [NET::ERR\_CERT\_AUTHORITY\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-authority-invalid/)
- [SEC\_ERROR\_UNKNOWN\_ISSUER](https://pkizerotohero.com/errors/firefox-sec-error-unknown-issuer/)
- [\[SSL: CERTIFICATE\_VERIFY\_FAILED\] certificate verify failed: unable to get local issuer certificate](https://pkizerotohero.com/errors/python-certificate-verify-failed/)
- [PKIX path building failed … unable to find valid certification path to requested target](https://pkizerotohero.com/errors/java-pkix-path-building-failed/)
- [The remote certificate is invalid according to the validation procedure](https://pkizerotohero.com/errors/dotnet-remote-certificate-invalid/)

### How-to guides

- [How to check the certificate chain a server sends](https://pkizerotohero.com/guides/check-remote-certificate-chain/)
- [How to create a CSR with Java keytool](https://pkizerotohero.com/guides/create-csr-java-keytool/)
- [How to convert a Java keystore (JKS) to PKCS #12](https://pkizerotohero.com/guides/convert-jks-pkcs12/)
- [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/)
- [How to make Python, Node.js and npm trust a private CA](https://pkizerotohero.com/guides/trust-ca-python-node/)

### Tools

- [Chain checker](https://pkizerotohero.com/tools/chain/): Paste a chain and see which link is missing, expired or out of order.
- [Certificate decoder](https://pkizerotohero.com/tools/decoder/): Paste a PEM certificate and read every field in plain words.

### Topics

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