self-signed certificate in certificate chain: whose root is that?

npm, pip, git and curl stop with this error while the browser on the same machine is perfectly happy. The chain ends in a root your tool does not trust. Usually that root belongs to something on your own network, not to the site you are calling.

Written by · Published 2026-09-30 · 6 min read

What it means

This is OpenSSL's error 19. The client built a chain from the server's certificate up to a self-signed certificate, which is what a root is, and that root is not in the client's trust store. It is a different error from its neighbours:

OpenSSL errorMeaning
18, self-signed certificateThe server's own certificate is self-signed. There is no chain at all.
19, self-signed certificate in certificate chainThere is a chain, and it ends in a root this client does not trust.
20, unable to get local issuer certificateThe chain stops before any root: usually a missing intermediate. See that guide.

The same condition in other tools: SELF_SIGNED_CERT_IN_CHAIN in Node and npm, CERTIFICATE_VERIFY_FAILED ... self-signed certificate in certificate chain in Python and pip, SSL certificate problem: self-signed certificate in certificate chain in curl and git, and PKIX path building failed in Java.

The one-minute test: is it the server or your network?

Check the same host from outside your network. The SSL certificate chain checker connects from this server and shows the chain exactly as served, with a verdict per client.

Then compare with what your machine sees:

openssl s_client -connect registry.npmjs.org:443 -servername registry.npmjs.org -showcerts </dev/null 2>/dev/null \
  | grep -E '^ *[0-9] s:|^ *i:'

An issuer with your employer's name, a security vendor (Zscaler, Netskope, Palo Alto, Fortinet) or an antivirus product in it settles the question.

Cause 1: something is inspecting your TLS

By far the most common reason on a work laptop. A corporate proxy or endpoint security product decrypts HTTPS, inspects it, and re-encrypts it with a certificate issued by its own root. That root was installed in the operating system's trust store, which is why browsers work. Tools with their own trust store never got it:

ToolPoint it at the corporate root
Node, npm, yarnNODE_EXTRA_CA_CERTS=/path/corp-root.pem, or npm config set cafile /path/corp-root.pem
Python, pip, requestspip config set global.cert /path/corp-root.pem, REQUESTS_CA_BUNDLE, SSL_CERT_FILE
gitgit config --global http.sslCAInfo /path/corp-root.pem
curl--cacert, or cacert= in ~/.curlrc
Javakeytool -importcert into the JDK's cacerts
Docker buildsCOPY the root into the image and run update-ca-certificates

Your IT team can give you the root as a PEM file; it is also exportable from the operating system's keychain. Note that REQUESTS_CA_BUNDLE and SSL_CERT_FILE replace the default bundle rather than adding to it, so the file you point them at must contain the public roots as well, or everything not intercepted breaks instead.

Cause 2: the server uses a private CA

Internal services, staging environments and appliances often use certificates from an organisation's own CA, and some servers send that CA's root in the chain. Clients that have been given the root trust it; everything else prints this error. For an internal service that is correct behaviour, and the fix is to distribute the root to the clients that need it, using the same settings as above. For anything the public reaches, use a certificate from a public CA. Let's Encrypt costs nothing and removes the problem for every client at once.

Cause 3: the server sends a root nobody trusts

Less often, a public site's chain file includes an old root or a cross-signed certificate that has been removed from trust stores, and some clients build their path through it. The chain checker's "root sent" and "untrusted root" findings show this. Sending a root is never necessary, since clients use their own copy, so rebuild the chain without it; how to build a correct fullchain.pem has the details.

The advice to ignore

npm config set strict-ssl false, NODE_TLS_REJECT_UNAUTHORIZED=0, pip --trusted-host, git config http.sslVerify false. Each one makes the tool accept any certificate from anyone, and they tend to outlive the afternoon they were added in. Adding the one root you actually need takes about as long and leaves nothing open.

All guides · All tools