<!-- https://pkizerotohero.com/errors/node-self-signed-cert-in-chain/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Fix Node.js and npm SELF_SIGNED_CERT_IN_CHAIN: trust your proxy or company root with NODE_EXTRA_CA_CERTS or the system store, not by disabling checks.

Source: https://pkizerotohero.com/errors/node-self-signed-cert-in-chain/ · Last reviewed: 2026-10-20

- Node.js
- Exact text checked with the real tool

# SELF\_SIGNED\_CERT\_IN\_CHAIN

Also seen in: npm, yarn, axios, fetch (undici), Electron

**In one sentence:**

The server sent a complete chain that ends in a root Node.js does not trust, almost always a TLS-inspection proxy's root or a company CA.

[Check yours: Chain checker](https://pkizerotohero.com/tools/chain/)

## Where you see it

In Node.js HTTPS clients and, very often, in `npm install` on a company network. The code comes from OpenSSL’s verify error 19; Node.js lists these codes in its [TLS documentation](https://nodejs.org/api/tls.html#x509-certificate-error-codes).

```text
node:events:485
      throw er; // Unhandled 'error' event
      ^

Error: self-signed certificate in certificate chain
    at TLSSocket.onConnectSecure (node:_tls_wrap:1679:34)
    at TLSSocket.emit (node:events:507:28)
    at TLSSocket._finishInit (node:_tls_wrap:1078:8)
    at ssl.onhandshakedone (node:_tls_wrap:864:12)
...
  code: 'SELF_SIGNED_CERT_IN_CHAIN'
}
```

```text
$ npm install
npm error code SELF_SIGNED_CERT_IN_CHAIN
npm error errno SELF_SIGNED_CERT_IN_CHAIN
npm error request to https://registry.npmjs.org/express failed, reason: self-signed certificate in certificate chain
```

Older npm versions print `npm ERR!` instead of `npm error`.

## What it means

Every chain ends in a [root certificate](https://pkizerotohero.com/handbook/glossary/#root-certificate), which signs itself. A root is trusted only because it is already in the client’s [trust store](https://pkizerotohero.com/handbook/glossary/#trust-store). Here the server sent the whole chain including a self-signed root, and that root is not among the roots Node.js trusts. Node.js uses its own bundled list of public roots by default, not the operating system’s store, so a root that IT installed on your laptop may be missing for Node.js only.

## Likely causes

### A TLS-inspection proxy re-signs your traffic

If this happens for `registry.npmjs.org` or other public sites on a company network, a [TLS-inspection](https://pkizerotohero.com/handbook/glossary/#tls-inspection) proxy or antivirus product is decrypting HTTPS and re-signing it. Check the issuer:

```bash
openssl s_client -connect registry.npmjs.org:443 -servername registry.npmjs.org </dev/null 2>/dev/null | openssl x509 -noout -issuer
```

If it names your company or a security product instead of a public CA, get that root certificate from IT in PEM format and trust it:

```bash
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/corp-root.pem
npm install
```

`NODE_EXTRA_CA_CERTS` adds certificates to Node.js’s built-in roots when the process starts, so public sites keep working ([Node.js CLI documentation](https://nodejs.org/api/cli.html#node_extra_ca_certsfile)). Set it in your shell profile, CI variables or Dockerfile:

```dockerfile
COPY corp-root.pem /usr/local/share/ca-certificates/corp-root.crt
RUN update-ca-certificates
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root.crt
```

Node.js can also trust the operating system’s store, where IT usually deploys the proxy root: start it with [`--use-system-ca`](https://nodejs.org/api/cli.html#--use-system-ca) (Node.js 22.15 and 23.8 on Windows and macOS, 23.9 on other platforms), or set `NODE_USE_SYSTEM_CA=1` (Node.js 22.19 and 24.6).

### The server uses a private CA

Internal registries, Git servers and APIs often use a company CA. List what the server sends:

```bash
openssl s_client -connect npm.example.com:443 -servername npm.example.com -showcerts </dev/null 2>/dev/null | grep -E '^ [0-9]+ s:|^   i:'
```

The last entry has the same subject (`s:`) and issuer (`i:`): that is the root. Get it from your PKI team through a trusted channel, compare its SHA-256 fingerprint (`openssl x509 -in corp-root.pem -noout -fingerprint -sha256`), and add it with `NODE_EXTRA_CA_CERTS` as above.

For npm only, `npm config set cafile /path/to/bundle.pem` also works, but the file **replaces** npm’s built-in roots ([npm config documentation](https://docs.npmjs.com/cli/v11/using-npm/config#cafile)), so it must contain the public roots as well, or the public registry fails with `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`. When a `ca` is set, Node.js also ignores `NODE_EXTRA_CA_CERTS` for those connections.

### Your Node.js is old

Each Node.js release bundles a snapshot of public roots. A very old version may lack a newer public root. Upgrade Node.js to a supported release.

### Related: DEPTH\_ZERO\_SELF\_SIGNED\_CERT

If the code is `DEPTH_ZERO_SELF_SIGNED_CERT` (message `self-signed certificate`), the server sent a single [self-signed certificate](https://pkizerotohero.com/handbook/glossary/#self-signed-certificate) with no CA at all, typical of a development server. Replace it with a certificate from your company CA or a public CA. For a local test, trust exactly that certificate with `NODE_EXTRA_CA_CERTS` or the `ca` option.

Warning

Why strict-ssl false and NODE\_TLS\_REJECT\_UNAUTHORIZED=0 are traps

`npm config set strict-ssl false` and `NODE_TLS_REJECT_UNAUTHORIZED=0` switch off certificate checking. Packages you install then come from whoever answers, and a tampered package runs code on your machine and in your build. Trust the one proxy or company root you need with `NODE_EXTRA_CA_CERTS` and keep checking everything else.

## Check yours

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

Paste every certificate from `openssl s_client -showcerts`. The checker marks the self-signed root, explains when a client would trust it, and points out root names that belong to known HTTPS-inspection products.

## Learn why

- [Who do we trust? CAs, trust stores and self-signed certificates](https://pkizerotohero.com/handbook/trust-stores/): why each runtime has its own store.
- Related: [npm UNABLE\_TO\_GET\_ISSUER\_CERT\_LOCALLY](https://pkizerotohero.com/errors/npm-unable-to-get-issuer-cert-locally/), [curl: self-signed certificate in certificate chain](https://pkizerotohero.com/errors/curl-self-signed-certificate-in-certificate-chain/).

**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=/errors/node-self-signed-cert-in-chain/).

Last reviewed 20 October 2026Edition 0.9

## Learn why it happens

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

## Fix it step by step

- [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 Linux](https://pkizerotohero.com/guides/add-root-ca-linux/)
- [How to make Python, Node.js and npm trust a private CA](https://pkizerotohero.com/guides/trust-ca-python-node/)

## Everything on this topic

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