Fixing DEPTH_ZERO_SELF_SIGNED_CERT in Node.js (SSL/TLS Guide)

intermediate๐Ÿ”’ SSL/TLS2026-05-01| Node.js (v14+), Linux/macOS/Windows, Axios, Node Fetch, or native HTTPS module

Error Message

Error: self signed certificate (DEPTH_ZERO_SELF_SIGNED_CERT)
#nodejs#https#ssl#self-signed#fetch#axios

Context

I recently ran into a hard stop while connecting a microservice to an internal inventory system. This legacy API lived on a local server at 10.0.0.50:8443 using a self-signed SSL certificate. While a browser lets you click "Advanced" and "Proceed anyway," Node.js is far less forgiving. It prioritizes security over convenience every time.

The moment my Axios request hit that internal endpoint, the application died. This error message was the only thing left behind:

Error: self signed certificate (DEPTH_ZERO_SELF_SIGNED_CERT)

Node.js relies on a hardcoded list of trusted Certificate Authorities (CAs). When you hit a server with a certificate not signed by a major player like Let's Encrypt, Node.js kills the connection. It assumes you are under a man-in-the-middle attack. This is vital for production but a massive pain for internal development.

Debug Process

The DEPTH_ZERO_SELF_SIGNED_CERT code is the smoking gun. It means the certificate at the very bottom of the chain (depth zero) is self-signed. There is no root CA to verify that the server is who it claims to be.

To rule out server-side misconfiguration, I tested the endpoint with curl:

curl -I https://internal-api.local/data

The output confirmed my suspicion. Curl threw a similar "untrusted certificate" warning. However, running curl -k worked perfectly. This proved the server was healthy; the issue was strictly how my Node.js client handled validation.

Solutions

You have three ways to handle this. They range from a quick-fix "nuclear option" to production-ready configurations.

1. The "Nuclear Option" (Development Only)

If you need to move fast and are working strictly in a sandboxed local environment, you can force Node.js to ignore all SSL errors. This is handled via an environment variable.

Option A: Setting it in code

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// Your Axios/Fetch code follows
axios.get('https://internal-api.local/data');

Option B: Setting it via terminal

export NODE_TLS_REJECT_UNAUTHORIZED=0
node app.js

WARNING: Use this with extreme caution. It disables SSL validation for every request. Your app will no longer verify connections to Stripe, AWS, or any other external API, leaving you wide open to data theft.

2. The Surgical Approach (Specific to the Client)

A better way is to tell your HTTP client to trust a specific certificate. You keep global security intact while making a single exception for your internal server.

Using Axios with a Certificate File:

const https = require('https');
const fs = require('fs');

const agent = new https.Agent({
  ca: fs.readFileSync('./certs/internal-server.pem')
});

axios.get('https://internal-api.local/data', { httpsAgent: agent });

If you don't have the .pem file but still want to limit the risk to a single client instance, you can disable authorization for just that agent:

const agent = new https.Agent({
  rejectUnauthorized: false
});

axios.get('https://internal-api.local/data', { httpsAgent: agent });

3. The Corporate Standard: NODE_EXTRA_CA_CERTS

In a company setting, you likely have an internal Root CA. Instead of modifying every https.Agent, you can tell Node.js to extend its list of trusted authorities globally.

export NODE_EXTRA_CA_CERTS="/path/to/company-root-ca.pem"
node app.js

This is the cleanest method for teams. It allows Node.js to trust your internal infrastructure while maintaining standard security checks for the rest of the internet.

Verification Steps

Don't assume it's fixed just because the error went away. Follow these steps to ensure you haven't punched a hole in your security:

  • Step 1: Clear the NODE_TLS_REJECT_UNAUTHORIZED variable from your environment.
  • Step 2: Apply the https.Agent fix using the ca property.
  • Step 3: Confirm your app can fetch data from the internal API.
  • Step 4 (The Acid Test): Try to connect to a different internal HTTPS site that you haven't added. It should still fail. This proves your security filter is still working.

Lessons Learned

This error isn't a bug; it's Node.js doing its job correctly. My main takeaways from this debug session:

  • Environment variables stick around: It's easy to set NODE_TLS_REJECT_UNAUTHORIZED=0 in a terminal session and forget about it. This can lead to shipping insecure code.
  • Organization matters: Store your internal certificates in a dedicated /certs folder. Just remember to add them to your .gitignore.
  • Be surgical: Always prefer https.Agent or NODE_EXTRA_CA_CERTS over global flags. High security and developer productivity don't have to be mutually exclusive.

Related Error Notes