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:
- A security vendor or your employer: Zscaler, Netskope, Palo Alto, Fortinet, Sophos, Kaspersky, or your company's name. Your traffic is being intercepted and re-signed. This is cause 1, and it is by far the most common.
- A real public CA: ISRG, DigiCert, Sectigo. Then the chain is genuine and the problem is in Node's trust store. Cause 2.
- The server's own name: someone really did self-sign. Cause 3, and the only one where the label is literally accurate.
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.
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
- Python:
CERTIFICATE_VERIFY_FAILED: self signed certificate in certificate chain. Fix withREQUESTS_CA_BUNDLEorSSL_CERT_FILE. - Git:
SSL certificate problem: self signed certificate in certificate chain.git config --global http.sslCAInfo. - Java:
PKIX path building failed, because Java also keeps its own trust store. Import withkeytool. - Browsers:
ERR_CERT_AUTHORITY_INVALID, though these usually succeed where Node fails, since they do read the system store.
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.