How to Fix "self signed certificate in certificate chain"

VigilDog Team · October 12, 2026 · 6 min read

The "self signed certificate in certificate chain" error stops npm installs, git clones, curl calls and Docker builds cold, usually with no obvious cause. The good news: it almost never means your website's certificate is broken. It means the machine you're running on doesn't trust one of the certificates presented during the TLS handshake. Here's how to find the real culprit and fix it without turning off certificate verification.

What "self signed certificate in certificate chain" actually means

During a TLS handshake, the server sends its leaf certificate plus any intermediate certificates. Your client then walks up that chain, leaf, intermediate, intermediate, until it reaches a root certificate it already trusts in its local trust store. If it reaches the top and finds a self-signed certificate (one where the subject and issuer are identical) that is NOT in its trust store, it gives up and throws "self signed certificate in certificate chain."

The key word is *your client*. This is a trust problem on the machine making the request, not a defect on the server you're connecting to. The same URL often loads fine in a browser and still fails in Node.js or curl, because each has its own idea of which roots to trust. Before changing anything, confirm the leaf itself is actually valid with a quick lookup in the SSL checker, if the public chain is fine there, the problem is local trust, not the certificate.

Root CAin the trust storeIntermediatemust be installedYour certificateleaf, for your domain
The TLS certificate chain of trust

The three causes, in order of likelihood

Almost every occurrence of this error traces back to one of three situations. Identify which one you're in before you touch a config file:

  • A corporate TLS-inspection proxy (Zscaler, Netskope, Palo Alto, Fortinet) is intercepting HTTPS and re-signing every site with its own private root CA. Your browser trusts that root because IT pushed it to the OS store; your CLI tools don't. This is by far the most common cause on managed laptops.
  • You're genuinely connecting to something signed by a private or self-signed CA, an internal registry, a staging box, a homelab service, whose root was never added to the system trust store.
  • The server is serving an incomplete chain (missing an intermediate) and your client can't build a path to a trusted root. Rarer, and often surfaces as 'unable to get local issuer certificate' rather than the self-signed variant.

Diagnose the chain before you change anything

Ask the server exactly what it's presenting with openssl: openssl s_client -showcerts -connect example.com:443 </dev/null. Read the certificates from bottom to top. The topmost certificate is the one to inspect, if its subject= and issuer= lines are identical, it's self-signed. If that root is your corporate proxy's CA, cause #1 is confirmed. If it's some internal CA name, cause #2.

Note *which tool* is failing, because each keeps its own trust store. Node.js bundles its own CA list; git on Linux usually uses the OS bundle but on Windows may use its own; Python's requests uses certifi, not the OS store. A fix that satisfies curl won't necessarily satisfy Node. That's why 'I added it to Keychain and it still fails' is such a common follow-up.

Fix it the right way: trust the CA

The correct fix is to make your client trust the extra root, not to stop checking. First, save the offending root certificate as a PEM file (copy the block from the openssl output, or export the proxy CA your IT team provides). Add it to the OS storeupdate-ca-certificates on Debian/Ubuntu after dropping it in /usr/local/share/ca-certificates/, security add-trusted-cert on macOS, or the Certificate Manager on Windows.

Then point the tools that use their own store at the same file. For Node.js set NODE_EXTRA_CA_CERTS=/path/to/ca.pem (it appends to the built-in list rather than replacing it). For npm, npm config set cafile /path/to/ca.pem. For git, git config --global http.sslCAInfo /path/to/ca.pem. For Python and pip, export REQUESTS_CA_BUNDLE / PIP_CERT or pass --cert. For curl, --cacert /path/to/ca.pem or the CURL_CA_BUNDLE env var. Once the right root is trusted, the handshake completes and the error disappears for good.

What not to do

It is tempting to reach for NODE_TLS_REJECT_UNAUTHORIZED=0, git config http.sslVerify false, or curl -k. Each one doesn't fix the trust problem, it removes trust checking entirely, so your client will now accept *any* certificate from *any* party, including an actual man-in-the-middle. That flag has a habit of surviving from a quick local workaround into CI and then into production, at which point every request the service makes is silently unauthenticated.

If you must ship something in the next five minutes, scope the bypass to the single command and delete it immediately after, never set it as an environment default or commit it. The few extra minutes to trust the correct CA is the difference between a fix and a lingering security hole.

Keep chains healthy in production

Once your own machine trusts the right roots, the flip side matters too: the servers *you* run must present complete, valid chains, because a missing intermediate produces exactly this class of error for your users and API clients, and browsers often paper over it with AIA fetching while stricter clients fail hard. Chains also expire, and an expired or soon-to-expire certificate breaks everything downstream.

That's the kind of thing worth watching continuously rather than discovering from a support ticket. VigilDog's certificate and domain monitoring validates the full chain, flags missing intermediates, and warns you well before anything expires, so 'self signed certificate in certificate chain' stays a local dev annoyance and never becomes a production outage.

Questions

Frequently asked

Is this error a problem with my server's SSL certificate?

Usually not. It almost always means the client machine doesn't trust one of the roots in the chain, most often because a corporate TLS-inspection proxy or a private internal CA signed the connection and that CA isn't in your CLI tool's trust store.

Why does the site work in my browser but fail in Node.js or curl?

Browsers ship their own root store (which often includes CAs your IT team pushed) and can fetch missing intermediates automatically via the AIA extension. Node.js, curl and openssl use separate, stricter trust stores and don't do that fetching, so they fail where the browser silently succeeds.

Is setting NODE_TLS_REJECT_UNAUTHORIZED=0 safe for CI?

No. It disables certificate verification for the whole process, so any certificate, including one from an attacker intercepting the connection, is accepted. Trust the specific CA with NODE_EXTRA_CA_CERTS instead; it's only marginally more effort and keeps verification intact.

Never get surprised by a broken chain

VigilDog validates full certificate chains and warns you before they expire or break, so trust errors stay in dev, not production.

Your first domain is free forever