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 error | Meaning |
|---|---|
18, self-signed certificate | The server's own certificate is self-signed. There is no chain at all. |
19, self-signed certificate in certificate chain | There is a chain, and it ends in a root this client does not trust. |
20, unable to get local issuer certificate | The 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.
- The checker shows a normal public chain and every client passes: the server is fine. Something between you and it is presenting a different chain. Go to the first cause.
- The checker also reports an untrusted root: the server really is using a private or unknown CA. Go to the second.
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:
| Tool | Point it at the corporate root |
|---|---|
| Node, npm, yarn | NODE_EXTRA_CA_CERTS=/path/corp-root.pem, or npm config set cafile /path/corp-root.pem |
| Python, pip, requests | pip config set global.cert /path/corp-root.pem, REQUESTS_CA_BUNDLE, SSL_CERT_FILE |
| git | git config --global http.sslCAInfo /path/corp-root.pem |
| curl | --cacert, or cacert= in ~/.curlrc |
| Java | keytool -importcert into the JDK's cacerts |
| Docker builds | COPY 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.