Programming languages
self-signed certificate in certificate chain: causes and fixes
SSL certificate problem: self-signed certificate in certificate chainself-signed certificate in certificate chain means the server's certificate chain ends in a self-signed root certificate that your client does not trust. The chain itself is intact; the missing piece is trust in its top. On work laptops this is almost always a TLS-inspecting proxy; elsewhere it is an internal CA that was never distributed to the client.
Quick fix
The root at the top of the chain is the one your client must trust.
Find out which root is at the top of the chain:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null \
| grep -E '^ *[0-9] s:|^ *i:'
Look at the last certificate. If its subject and issuer are identical and name your company, a security vendor or an internal CA, that is the root to trust. Get the PEM file from your IT team (or export it from the OS store), check its fingerprint with them, then add it to the tool that failed:
export NODE_EXTRA_CA_CERTS=/path/company-root.pem # Node.js
npm config set cafile /path/ca-bundle.pem # npm
git config --global http.sslCAInfo /path/ca-bundle.pem # Git
export REQUESTS_CA_BUNDLE=/path/ca-bundle.pem # Python requests
curl --cacert /path/ca-bundle.pem https://example.com # curl
For npm, Git, curl and Python, ca-bundle.pem should be the system bundle plus your root, because these options replace the default list rather than extend it. NODE_EXTRA_CA_CERTS adds to Node's built-in roots.
Causes
1. A TLS-inspecting proxy or antivirus
Corporate gateways and antivirus "HTTPS scanning" decrypt traffic and re-encrypt it with certificates issued by their own root.
- Confirm: every external site shows the same unusual root at the top of the chain, and the error disappears on a home network or phone hotspot.
- Fix: trust the proxy root per tool as shown above, or ask IT to exempt package registries such as registry.npmjs.org and pypi.org from inspection.
2. An internal CA that clients do not know
Internal services (GitLab, Nexus, Harbor, intranet APIs) often use a private CA. Only machines that received its root can verify them.
- Confirm: the error appears only for internal hostnames, and the root's subject is your organisation's CA.
- Fix: distribute the root through the OS trust store (
update-ca-certificatesorupdate-ca-trust) and through each runtime's own setting.
3. The server sends a wrong or obsolete root
Some servers append a root certificate to their chain. If that root is private, expired or a different cross-signed version than the client trusts, the client may stop at it.
- Confirm: the certificate decoder shows the server chain contains a root, and other clients succeed without it.
- Fix: serve the leaf and intermediates only. Rebuild the chain with the chain builder.
4. A development CA (mkcert, local Kubernetes, test environments)
Local CAs are trusted by browsers after installation, but not by runtimes that ship their own CA list.
- Fix: load the dev root explicitly, for example with
NODE_EXTRA_CA_CERTS, and keep it out of production images.
Fixes by environment
Every fix adds the proxy's or internal CA's root to the store each tool reads; tools with their own stores need it separately.
Linux system store (Debian, Ubuntu, Alpine):
sudo cp company-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
RHEL, Rocky, Fedora: copy to /etc/pki/ca-trust/source/anchors/ and run sudo update-ca-trust.
Windows: Git for Windows can use the Windows certificate store with git config --global http.sslBackend schannel, which picks up roots deployed by Group Policy. Node.js and Python requests still need the settings above.
macOS: add the root to the System keychain and mark it trusted; then configure Node, npm and Python separately.
Java: import the root into the JVM truststore; see PKIX path building failed.
Docker builds: copy the root into the image and run update-ca-certificates before any npm install or pip install step, and pass NODE_EXTRA_CA_CERTS where Node runs.
Don't do this
NODE_TLS_REJECT_UNAUTHORIZED=0. Node prints a warning because it disables verification for every TLS connection in the process.npm config set strict-ssl false. Packages are then downloaded without any server authentication, which is exactly the path supply-chain attackers use.git config --global http.sslVerify false. It applies to every remote, not only the one that failed.verify=Falseorcurl -k. Same problem: you lose all protection, not only the check that failed.
FAQ
What is the difference between "self-signed certificate" and "self-signed certificate in certificate chain"?
"self-signed certificate" (error 18, Node's DEPTH_ZERO_SELF_SIGNED_CERT) means the server's own certificate is self-signed. "in certificate chain" (error 19) means the server certificate was issued by a CA, but the chain ends in a self-signed root you do not trust. See DEPTH_ZERO_SELF_SIGNED_CERT for the first case.
Why do I only see this at work?
Office networks often route HTTPS through an inspection proxy that issues certificates on the fly from a company root. Browsers on managed machines trust that root through the OS store, but Node.js, npm, Git for Linux, Python requests and Java keep their own CA lists, so they reject it.
Is it safe to trust the corporate proxy root?
On a company-managed machine it is the intended setup: the proxy already sees your traffic. Trust only the exact root your IT team provides, verify its fingerprint with them, and do not install it on personal devices or in public container images.
How do I trust a mkcert development CA in Node.js?
mkcert -install adds its root to the system and browser stores, but Node.js uses its bundled CA list. Run Node with NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" so the extra root is loaded at startup.