Back to publishing & release Automate semantic versioning Manage release channels Harden the supply chain

Fixing SELF_SIGNED_CERT_IN_CHAIN on Internal Registries

Corporate networks often intercept TLS traffic with a proxy that re-signs certificates using the company's own certificate authority, and internal registries are frequently served with certificates from a private CA. Node.js ships with its own bundle of public root certificates and does not trust either, so installs fail with SELF_SIGNED_CERT_IN_CHAIN or UNABLE_TO_GET_ISSUER_CERT_LOCALLY. The tempting fix — strict-ssl=false — disables certificate verification for every package you install, which is exactly the protection that stops a tampered download. This guide shows how to make npm, pnpm, Yarn and Node.js trust your CA properly, on developer machines, in CI and in Docker.

Exact symptoms and error messages

npm error code SELF_SIGNED_CERT_IN_CHAIN
npm error errno SELF_SIGNED_CERT_IN_CHAIN
npm error request to https://registry.internal.example.com/@acme%2fui failed, reason: self-signed certificate in certificate chain
npm error code UNABLE_TO_GET_ISSUER_CERT_LOCALLY
npm error request to https://registry.npmjs.org/react failed, reason: unable to get local issuer certificate

pnpm and Yarn report the same underlying Node.js errors:

 ERR_PNPM_META_FETCH_FAIL  GET https://registry.internal.example.com/@acme%2Fui: request to https://registry.internal.example.com/@acme%2Fui failed, reason: self-signed certificate in certificate chain
➤ YN0001: │ RequestError: self-signed certificate in certificate chain

If the failure happens for public registry URLs as well, a TLS-intercepting proxy is in the path. If it happens only for the internal registry, its certificate comes from a private CA.

Root cause analysis

A TLS connection is trusted when the server's certificate chain ends at a root certificate the client trusts. Operating systems and browsers trust your company CA because IT installed it in the system store. Node.js, by default, does not use the system store; it uses the Mozilla root bundle compiled into the binary. So the same URL works in a browser and fails in npm. The registry access model is covered in Private Registries and Access Control.

Why the browser trusts the registry and npm does not The registry presents a chain ending at the corporate root CA; the browser checks the OS trust store, which contains it, while Node.js checks its bundled roots, which do not. registry certificate issued by Acme Internal CA browser / OS store contains Acme root: trusted Node.js bundled roots no Acme root: rejected add CA to Node NODE_EXTRA_CA_ CERTS or cafile
The fix is to add the corporate CA to what Node.js trusts, not to turn verification off.

Resolution: trust the CA, do not disable verification

Choosing how to trust the corporate CA Prefer NODE_EXTRA_CA_CERTS for all Node tools, the npm cafile setting for npm and pnpm only, or the system CA store on Node versions that support it; never strict-ssl=false. Should every Node.js tool trust it? NODE_EXTRA_CA_CERTS npm, pnpm, Yarn, scripts, SDKs yes Only package installs? cafile in .npmrc npm and pnpm read it yes no Node 22.15+ / 23.8+ available? --use-system-ca reuse the OS trust store yes no strict-ssl=false do not: disables verification entirely no
Each option keeps verification on; they differ only in which tools they cover.

1. Get the CA certificate in PEM format. Ask IT for the root (and any intermediate) certificate, or export it from the system store. Save it as a PEM file, for example ~/certs/acme-root-ca.pem. It should start with -----BEGIN CERTIFICATE-----.

2. Make Node.js trust it, for every tool:

export NODE_EXTRA_CA_CERTS="$HOME/certs/acme-root-ca.pem"
npm install

NODE_EXTRA_CA_CERTS adds the certificates to Node's bundled roots at startup, so it covers npm, pnpm, Yarn and any Node.js script that makes HTTPS requests. Put it in your shell profile, and in CI job environments.

3. Or configure npm and pnpm specifically:

# user ~/.npmrc (the path is machine-specific, so not in the project file)
cafile=/home/dev/certs/acme-root-ca.pem

cafile makes npm (and pnpm) trust only the certificates in that file for registry requests, replacing the default roots. If the file contains only your company CA, requests to the public registry through a non-intercepting network will then fail. Put the company CA and the public roots in the file, or prefer NODE_EXTRA_CA_CERTS, which adds rather than replaces.

4. Or use the system store (newer Node.js). Recent Node.js releases can read the operating system's trust store with the --use-system-ca flag (Node.js 23.8 and 22.15 onwards; platform coverage has grown across releases, so check your version's documentation):

export NODE_OPTIONS="--use-system-ca"

That reuses the CA your IT team already deployed, with no certificate files to manage.

5. Yarn Berry reads its own settings: set caFilePath in .yarnrc.yml, or rely on NODE_EXTRA_CA_CERTS.

Diagnosing the certificate chain

Before changing configuration, confirm which certificate is presented and who issued it. openssl shows the chain the server sends:

openssl s_client -connect registry.internal.example.com:443 -servername registry.internal.example.com -showcerts </dev/null 2>/dev/null   | grep -E "^ *[0-9] s:|^ *i:"

The output lists each certificate's subject (s:) and issuer (i:). If the last issuer is your company's CA, you need that CA's root certificate. If the chain is incomplete — the server sends only its own certificate without the intermediate — the fix belongs on the server: configure it to send the full chain, because some clients cannot build the path themselves.

You can also test Node.js directly, independent of npm:

node -e "require('https').get('https://registry.internal.example.com/-/ping', r => console.log(r.statusCode)).on('error', e => console.error(e.code))"
NODE_EXTRA_CA_CERTS=~/certs/acme-root-ca.pem node -e "require('https').get('https://registry.internal.example.com/-/ping', r => console.log(r.statusCode)).on('error', e => console.error(e.code))"

If the first command prints SELF_SIGNED_CERT_IN_CHAIN and the second prints 200, the PEM file is the right one.

Proxies, environment variables and other tools

Corporate networks often combine TLS interception with an HTTP proxy that must be configured explicitly. npm and pnpm read proxy and https-proxy from .npmrc and the HTTPS_PROXY environment variable; Node.js's built-in fetch historically ignored proxy variables unless configured, and newer releases support them through NODE_USE_ENV_PROXY. When installs work but a postinstall script or a CLI that downloads binaries fails, it is usually because that tool makes its own HTTPS requests and needs both the proxy settings and the CA — which NODE_EXTRA_CA_CERTS provides, and a per-tool cafile setting does not. Tools that shell out to curl or git use the operating system's store, so they may work while Node-based tools fail, which is another reason to keep the Node.js and OS trust configurations aligned.

Why not strict-ssl=false?

strict-ssl=false   # do not do this

With verification off, any machine between you and the registry can serve modified packages, and npm will install them. On an intercepting corporate network that is the proxy itself — but the same setting follows the laptop onto café Wi-Fi, into CI runners on shared networks, and into Docker images. The integrity hashes in the lockfile provide some protection for already-locked packages, but new resolutions, metadata and anything without a lockfile entry are exposed. Every alternative above achieves the same result — installs work — while keeping verification on. The broader supply-chain picture is in Supply-Chain Security Hardening.

Options for trusting a private CA Compares NODE_EXTRA_CA_CERTS, npm cafile, --use-system-ca and strict-ssl=false on tool coverage, whether verification stays on, and maintenance. covers verification maintenance NODE_EXTRA_CA_C ERTS all Node.js tools on distribute PEM file npm cafile npm and pnpm on replaces default roots --use-system-ca all Node.js tools on uses OS store strict-ssl=false npm off security debt
Every option except disabling strict-ssl keeps downloads verified.

CI and Docker

In CI, add the certificate to the runner image or write it from a secret at job start:

- name: Trust corporate CA
  run: |
    echo "$CORP_ROOT_CA" > "$RUNNER_TEMP/corp-ca.pem"
    echo "NODE_EXTRA_CA_CERTS=$RUNNER_TEMP/corp-ca.pem" >> "$GITHUB_ENV"
  env:
    CORP_ROOT_CA: ${{ secrets.CORP_ROOT_CA_PEM }}
- run: pnpm install --frozen-lockfile

In Docker, install the CA into the image's system store and point Node.js at it:

FROM node:22-slim
COPY certs/acme-root-ca.pem /usr/local/share/ca-certificates/acme-root-ca.crt
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
    && update-ca-certificates && rm -rf /var/lib/apt/lists/*
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/acme-root-ca.crt

A CA certificate is public information, so committing the PEM file to the repository is acceptable; the private key never leaves the CA. Keep the file name and location stable across images and repositories, so documentation and scripts can refer to one path, and rotate it in one place when IT renews the root certificate.

Worked example: an install that worked in the browser

A new engineer on a corporate laptop can open https://registry.internal.example.com in the browser but pnpm install fails with SELF_SIGNED_CERT_IN_CHAIN. A colleague suggests strict-ssl=false, which works. A week later, a security review finds the setting in dozens of personal .npmrc files and in two Dockerfiles. The platform team publishes the corporate root CA in the internal documentation, adds NODE_EXTRA_CA_CERTS to the standard developer setup script and the base CI and Docker images, and adds a CI check that fails if any committed .npmrc contains strict-ssl=false. Installs work everywhere with verification on.

Prevention and guardrails

  • Distribute the corporate CA through the standard developer setup, CI images and Docker base images.
  • Prefer NODE_EXTRA_CA_CERTS or --use-system-ca over cafile.
  • Ban strict-ssl=false in committed configuration with a CI check.
  • Test from CI and a clean container, not only a configured laptop.

Frequently Asked Questions

Does NODE_EXTRA_CA_CERTS work if set inside .npmrc? No. It is read by Node.js at process start, before npm reads any configuration. Set it in the environment.

Why does it fail for registry.npmjs.org too? A TLS-intercepting proxy re-signs all traffic, including public registries. Trust the proxy's CA; do not add a bypass for public hosts.

Is ca= in .npmrc the same as cafile=? ca holds certificate contents inline, cafile a path. Both replace the default roots for npm's requests; NODE_EXTRA_CA_CERTS adds to them.

Should the CA file include intermediate certificates? Include the root certificate at minimum. Add intermediates only if the server does not send them in its chain; the better fix in that case is to configure the server to send the full chain.

What about NODE_TLS_REJECT_UNAUTHORIZED=0? It disables certificate verification for every HTTPS request in the process — installs, application code, SDKs — and Node.js prints a warning when it is set. It has the same problems as strict-ssl=false, more broadly. Never set it outside a throwaway debugging session.

Related

Private Registries and Access Control