Programming languages

Node.js DEPTH_ZERO_SELF_SIGNED_CERT: trust it the right way

By · Last verified

Error messageError: self-signed certificate (code: 'DEPTH_ZERO_SELF_SIGNED_CERT')

self-signed certificate (DEPTH_ZERO_SELF_SIGNED_CERT) means the server presented a self-signed certificate, and Node.js has no reason to trust it. That is expected for local HTTPS servers and many internal tools; the right fix is to tell Node.js to trust that specific certificate or the CA that issued it, not to turn verification off.

Quick fix

For development, create a local CA and issue certificates from it with mkcert, then point Node.js at the CA:

mkcert -install
mkcert localhost 127.0.0.1 ::1
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
node app.js

For a single internal service you do not control, fetch its certificate once, verify its fingerprint with the owner, and pass it as ca:

const fs = require("node:fs");
const https = require("node:https");
const tls = require("node:tls");

const agent = new https.Agent({
  ca: [...tls.rootCertificates, fs.readFileSync("/etc/ssl/extra/nas-example.pem")],
});
https.get("https://nas.example.com/", { agent }, (res) => console.log(res.statusCode));

Causes

1. A development or internal server with a self-signed certificate

Frameworks, test containers, routers, NAS devices and admin consoles often generate a self-signed certificate on first start.

  • Confirm: openssl s_client -connect host:443 -servername host </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer shows identical subject and issuer.
  • Fix: replace it with a certificate from an internal CA, or trust it explicitly as shown above. A self-signed certificate generator can create one with proper SANs.

2. The wrong virtual host answers

Web servers present a default (often self-signed "snakeoil") certificate when the requested name does not match any configured site, for example when you connect by IP or without SNI.

  • Confirm: the subject is a generic name such as localhost or the machine's hostname, not your domain.
  • Fix: connect with the correct hostname; in custom TLS code set servername for SNI. On the server, make the right site the default for that port.

3. Interception on the network

A captive portal or security device that cannot complete interception may present its own self-signed certificate.

  • Confirm: the issuer names a network device or vendor; the error only appears on one network.
  • Fix: talk to the network owner; for corporate proxies, trust their CA rather than a self-signed leaf.

4. Hostname mismatch after trusting

If you trusted the certificate but now see ERR_TLS_CERT_ALTNAME_INVALID, the certificate does not list the hostname in its SAN. Regenerate it with the right names.

Fixes by environment

In every environment, trust the CA that issued the certificate through NODE_EXTRA_CA_CERTS or the ca option. When the self-signed root sits higher in the chain, Node.js reports SELF_SIGNED_CERT_IN_CHAIN instead.

Local development with Vite, Next.js, webpack-dev-server: use mkcert certificates and set NODE_EXTRA_CA_CERTS for server-side code that calls the dev server.

Docker Compose: mount the CA file and set NODE_EXTRA_CA_CERTS in the service's environment.

Axios / node-fetch / undici: Axios accepts httpsAgent; undici-based fetch uses Node's default trust store, so NODE_EXTRA_CA_CERTS is the simplest path. With undici directly, pass connect: { ca } to an Agent.

Electron: for app code use the ca option or NODE_EXTRA_CA_CERTS; the Chromium side uses the operating system store.

Don't do this

  • NODE_TLS_REJECT_UNAUTHORIZED=0. It disables verification for every connection in the process and is often forgotten in a .env file that reaches production.
  • rejectUnauthorized: false as a default in shared HTTP clients. It hides the problem for public APIs too.
  • Accept any self-signed certificate "for internal hosts". Internal networks are not automatically trustworthy; pin the specific certificate or CA.

FAQ

What is the difference from SELF_SIGNED_CERT_IN_CHAIN?

DEPTH_ZERO_SELF_SIGNED_CERT means the server's own certificate is self-signed (a chain of one). SELF_SIGNED_CERT_IN_CHAIN means the server sent a chain whose root is self-signed but not trusted, typical for private CAs and TLS-inspecting proxies. See self-signed certificate in certificate chain.

Can I trust a self-signed certificate with NODE_EXTRA_CA_CERTS?

Yes, the exact certificate can be added as a trust anchor, but its subject alternative names must still match the hostname you connect to, and you must update the file whenever the certificate is regenerated. A local CA avoids that churn.

I get this error for a public site. What does it mean?

A public site should never present a self-signed certificate. Either you are being routed to a default or misconfigured virtual host (for example by connecting with an IP address), or something on your network is intercepting the connection. Check the site with the SSL checker.

Sources

  1. Node.js TLS (SSL) documentation
  2. Node.js command-line options (NODE_EXTRA_CA_CERTS)
  3. OpenSSL X509_STORE_CTX_get_error (verification error codes)