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_UNAUTHORIZEDvariable from your environment. - Step 2: Apply the
https.Agentfix using thecaproperty. - 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=0in a terminal session and forget about it. This can lead to shipping insecure code. - Organization matters: Store your internal certificates in a dedicated
/certsfolder. Just remember to add them to your.gitignore. - Be surgical: Always prefer
https.AgentorNODE_EXTRA_CA_CERTSover global flags. High security and developer productivity don't have to be mutually exclusive.

