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.
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 #
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.
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 usenpm config set strict-ssl false, which turns verification off for all package downloads.
References
- openssl-verify(1) — the verify error this maps to
- RFC 4158: certification path building — why a verifier can fail to find a path that exists