Writing

Self signed certificate in certificate chain

Published 15 September 2026

The message is misleading in a specific way: most of the time nobody has self-signed anything. Node is telling you the chain ended at a certificate that signed itself, and the usual reason is that something on your network re-issued the certificate on its way to you.

You will see it from Node.js as SELF_SIGNED_CERT_IN_CHAIN, and from npm, yarn, Electron builds and anything else that uses Node's TLS stack underneath.

The wording changed along the way. OpenSSL 3 added a hyphen, so anything built against it says self-signed certificate in certificate chain while older builds say the same thing without one: Node 18 is where most people met the change, since that is the release that moved to OpenSSL 3. The code is the same in both, which makes it the safer thing to search your logs for.

What the message actually says

Every chain ends at a root certificate, and every root is self-signed: that is what a root is. The question is only whether that particular root is in the trust store doing the checking.

So self signed certificate in certificate chain does not mean "somebody used a self-signed certificate". It means "this chain terminates in a self-signed certificate I do not recognise". The interesting question is whose it is.

The check: whose root is it?

openssl s_client -showcerts -connect registry.npmjs.org:443 \
  -servername registry.npmjs.org 2>/dev/null | grep -E "^(s|i):"

That prints the subject and issuer of each certificate the server sent. Look at the last issuer line:

1. A proxy is re-signing your traffic

Corporate TLS inspection works by terminating your connection, reading the plaintext, and issuing a fresh certificate from its own root. Browsers on a managed machine accept it because the root was installed by policy.

Node does not use the system trust store. It ships its own bundled list of root certificates and ignores the operating system's. So the proxy root that the whole rest of the machine trusts is invisible to Node, and everything Node does over HTTPS fails while browsers work perfectly. That single design decision accounts for most occurrences of this error.

The fix is to tell Node about the root, which is what NODE_EXTRA_CA_CERTS exists for:

export NODE_EXTRA_CA_CERTS=/path/to/corporate-root.pem

It adds to the bundled roots rather than replacing them, so normal public certificates keep working. Point it at the PEM your IT team publishes; exporting it from your browser's certificate viewer works too.

For npm specifically, npm config set cafile /path/to/root.pem does the same job for npm's own requests.

2. The chain is genuine and Node is out of date

Node's root bundle is baked in at build time. A very old Node has an old list, and roots do get added and retired: the expiry of the DST Root CA X3 cross-sign in 2021 broke a great many old clients against Let's Encrypt certificates in exactly this way.

If the issuer is a public CA you recognise and the same host works fine in curl, updating Node is the fix. It is also worth confirming the server is not sending a chain that only looks complete: see unable to get local issuer certificate for the two opposite situations that produces.

3. It really is self-signed

Internal registries, staging environments, a local development server with a certificate somebody generated once. Here the label is accurate and the answer depends on whether you trust that certificate.

If you do, add it explicitly: NODE_EXTRA_CA_CERTS again, or ca: in the request options for a single client. Adding one certificate you have verified is a completely different act from disabling verification, even though they both make the error stop.

See what the server really sends. The Kodura SSL checker connects from outside your network, so no local proxy sits in between. If it reports a clean chain from a public CA while your machine sees a self-signed root, you have proved interception in one step, and you know the fix is NODE_EXTRA_CA_CERTS, not the server.

Open the SSL checker

Not NODE_TLS_REJECT_UNAUTHORIZED=0

It is the first result for this error and it is the wrong answer. Setting it disables certificate verification for every TLS connection the process makes, to every host, for the life of the process. Node itself prints a warning saying this is dangerous.

Consider what the error is usually telling you: something is intercepting and re-signing your traffic. Turning off verification does not remove the interceptor: it removes your ability to notice it. If it reaches a build server or a container image, every dependency you pull is fetched over a connection nobody is checking.

NODE_EXTRA_CA_CERTS takes about the same effort and keeps verification switched on for everything else.

The same error in other languages

The pattern across all of them is the same: each runtime keeps its own idea of who to trust, and an intercepting proxy only gets installed into some of them.