# Node.js DEPTH_ZERO_SELF_SIGNED_CERT: trust it the right way

> Node.js says "self-signed certificate" (DEPTH_ZERO_SELF_SIGNED_CERT) when the server's own certificate is self-signed. Trust a local CA; keep checks on.

Source: https://ssl.sh/errors/node-depth-zero-self-signed-cert/
Last updated: 2026-11-10

**`self-signed certificate` (`DEPTH_ZERO_SELF_SIGNED_CERT`) means the server presented a [self-signed certificate](https://ssl.sh/glossary/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](https://github.com/FiloSottile/mkcert), then point Node.js at the CA:

```bash
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`:

```js
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](https://ssl.sh/tools/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](https://ssl.sh/glossary/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](https://ssl.sh/errors/self-signed-certificate-in-certificate-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.
