self signed certificate in certificate chain

The error "self signed certificate in certificate chain" (OpenSSL verify error 19) means the chain ended at a self-signed root that is not in your trust store. Every chain ends at a self-signed root — that is what a root is — so the error is not about the certificate being self-signed but about the root being *unknown*. The two common causes are a corporate TLS-inspecting proxy re-signing traffic with its own CA, and a private CA whose root has not been installed. The fix is to add that root to the relevant trust store, not to disable verification.

Updated August 19, 2026 Verified against OpenSSL 3.5.7

What it actually means #

Every certificate chain ends at a self-signed root — a root certificate signs itself, which is what makes it a root. So the error is not complaining that a certificate is self-signed. It is saying:

I followed the chain to its end, found a self-signed root there, and I do not have that root in my trust store.

The chain is structurally fine. The trust anchor is unknown.

Step 1 — see which root you are being offered #

Print the subject of every certificate the server sends
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| grep -E "^ *[0-9]+ s:"
Expected output
0 s:CN = example.com
1 s:CN = Contoso Security Appliance CA

The last line is the root. What it says tells you which of the two causes you have.

Cause A — a TLS-inspecting proxy #

If the root names your employer, a firewall vendor, or something like “Zscaler”, “Netskope”, “Fortinet” or “Blue Coat”, your traffic is being intercepted and re-signed by a corporate proxy. This is intentional on managed networks: the proxy terminates TLS, inspects the traffic, and re-encrypts it with a certificate from its own CA.

Managed machines have that CA root installed automatically. Containers, virtual machines and freshly-imaged laptops usually do not.

Save the root the proxy is presenting
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| awk '/BEGIN CERT/{n++} n==2' \
> corporate-root.crt

Cause B — your own private CA #

If the root names your own organisation’s CA, it simply has not been installed on this machine. See create a root CA for the distribution step.

Installing the root #

sudo cp corporate-root.crt /usr/local/share/ca-certificates/corporate-root.crt
sudo update-ca-certificates
sudo cp corporate-root.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
sudo apk add ca-certificates
sudo cp corporate-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain corporate-root.crt

Runtimes that ignore the system store #

Fixing the OS store frequently does not fix the error, because several runtimes bundle their own CA list:

Runtime Fix
Node.js export NODE_EXTRA_CA_CERTS=/path/to/corporate-root.crt
npm npm config set cafile /path/to/ca-bundle.crt
Python requests export REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crt
Python ssl, pip export SSL_CERT_FILE=/path/to/ca-bundle.crt
pip pip config set global.cert /path/to/ca-bundle.crt
Git git config --global http.sslCAInfo /path/to/ca-bundle.crt
Java keytool -importcert -trustcacerts -alias corp-root -file corporate-root.crt -keystore $JAVA_HOME/lib/security/cacerts
Go Uses the system store; SSL_CERT_FILE also works
curl curl --cacert /path/to/corporate-root.crt

In a Dockerfile #

COPY corporate-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt

What not to do #

Frequently asked questions

How is this different from "unable to get local issuer certificate"?
Error 20 means the chain was incomplete — a link was missing. Error 19 means the chain was complete but ended at a root you do not trust. These are different problems; the first is the server's fault, the second is a local trust configuration issue.
Why did this start happening on a new laptop or in a new container?
The trust store changed. A corporate root that was installed on the old machine by device management is absent on the new one, or a container image ships without it. The network did not change; your trust store did.
Is NODE_TLS_REJECT_UNAUTHORIZED=0 an acceptable fix?
No. It disables certificate verification for every connection the process makes, so the traffic is encrypted but unauthenticated. Use it to confirm a diagnosis and never in anything that ships.
How do I fix this for npm specifically?
Point npm at a CA bundle containing the root — npm config set cafile /path/to/ca-bundle.crt. Do not use npm config set strict-ssl false, which turns verification off for all package downloads.

References