What Happened
You're calling an internal HTTPS endpoint β a staging server, a microservice on localhost:8443, or a corporate tool sitting behind a proxy β and Node.js throws:
Error: self signed certificate (code: 'DEPTH_ZERO_SELF_SIGNED_CERT')
Three situations trigger this almost every time: migrating an internal service from HTTP to HTTPS, spinning up a local dev environment with a self-signed cert, or working behind a corporate proxy that terminates and re-signs TLS traffic.
There are clean fixes for each case. None require disabling SSL verification across the board.
Root Cause
Node.js validates the full certificate chain on every HTTPS request. A self-signed certificate has no chain β it signed itself. With no trusted Certificate Authority (CA) backing it, Node's TLS stack rejects it immediately.
The DEPTH_ZERO_SELF_SIGNED_CERT code is specific: the certificate at depth zero (the server cert itself) is self-signed and isn't in Node's built-in CA store. Worth distinguishing from UNABLE_TO_VERIFY_LEAF_SIGNATURE and SELF_SIGNED_CERT_IN_CHAIN β those involve intermediate certificates higher up the chain. Here, the server cert is the direct problem.
Reproduce the Error
Generate a self-signed cert to test with:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'
Start a quick HTTPS server:
const https = require('https');
const fs = require('fs');
https.createServer(
{ key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') },
(req, res) => res.end('ok')
).listen(8443);
Call it from a second Node.js process:
const https = require('https');
https.get('https://localhost:8443', res => console.log(res.statusCode));
// Error: self signed certificate (code: 'DEPTH_ZERO_SELF_SIGNED_CERT')
Fix 1: Trust the Specific Certificate (Recommended)
Tell Node.js to trust this one certificate explicitly. Every other request still goes through normal TLS validation β nothing else changes.
With the built-in https module
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
ca: fs.readFileSync('/path/to/cert.pem') // the server's self-signed cert
});
https.get('https://internal-api.company.local/health', { agent }, res => {
console.log('Status:', res.statusCode);
});
With axios
const axios = require('axios');
const https = require('https');
const fs = require('fs');
const httpsAgent = new https.Agent({
ca: fs.readFileSync('/path/to/cert.pem')
});
const client = axios.create({ httpsAgent });
await client.get('https://internal-api.company.local/health');
With native fetch (Node 18+)
const fs = require('fs');
const { fetch, Agent } = require('undici');
const dispatcher = new Agent({
connect: {
ca: fs.readFileSync('/path/to/cert.pem')
}
});
const res = await fetch('https://internal-api.company.local/health', { dispatcher });
console.log(res.status);
Fix 2: Add the Certificate to Node's CA Store via Environment Variable
When multiple services in the same process need the same cert, skip the per-call agent setup and reach for NODE_EXTRA_CA_CERTS. Zero code changes.
NODE_EXTRA_CA_CERTS=/path/to/cert.pem node app.js
Or drop it in your .env file (with dotenv):
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/internal-ca.pem
Particularly useful in shared environments: add the env var to your GitHub Actions workflow, Docker Compose file, or CI config, and every developer and pipeline run trusts the right cert automatically β no code changes required.
Fix 3: Export and Trust the Certificate System-Wide
When many tools need to trust the same cert β curl, Python scripts, not just Node.js β add it to the OS trust store.
Ubuntu/Debian
sudo cp cert.pem /usr/local/share/ca-certificates/internal-api.crt
sudo update-ca-certificates
RHEL/CentOS/Fedora
sudo cp cert.pem /etc/pki/ca-trust/source/anchors/internal-api.crt
sudo update-ca-trust
macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain cert.pem
One catch: Node.js doesn't read the OS trust store by default unless you're on a distro-patched build (some Debian/Ubuntu packages enable this). For Node specifically, NODE_EXTRA_CA_CERTS is more reliable and portable.
What NOT to Do
You'll find this suggestion everywhere online:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // NEVER do this in production
Or with axios:
const agent = new https.Agent({ rejectUnauthorized: false }); // also dangerous
Both options disable TLS certificate validation for the entire Node.js process β not just this one request. Your app will silently accept any certificate, including a forged one from a MITM attacker. Fine for a 5-minute local test. A real vulnerability anywhere else. Use an explicit cert fix above for anything that ships.
Verify the Fix
Re-run your HTTPS call β you should get a 200 (or whatever the endpoint returns) instead of the error. To inspect what certificate the server is actually presenting:
openssl s_client -connect internal-api.company.local:443 -showcerts 2>/dev/null | openssl x509 -noout -text | grep -E 'Issuer|Subject|Not After'
Same Issuer and Subject? Self-signed. Grab the cert directly with:
openssl s_client -connect internal-api.company.local:443 2>/dev/null | openssl x509 > server.pem
Then use server.pem as the ca value in any fix above. Confirm Node.js trusts it:
NODE_EXTRA_CA_CERTS=server.pem node -e "
const https = require('https');
https.get('https://internal-api.company.local/health', r => console.log('OK:', r.statusCode))
.on('error', e => console.error('FAIL:', e.message));
"
Lessons Learned
- Self-signed certs are fine for internal services β just distribute and trust the cert explicitly rather than bypassing the check entirely.
NODE_EXTRA_CA_CERTSis the least invasive fix for CI/CD and Docker environments β one env var, zero code changes.- If you control the internal service, consider switching to Let's Encrypt or an internal CA instead of per-service self-signed certs. An internal CA means trusting one root cert covers all your services, not one cert per service.
- Never commit
rejectUnauthorized: falseto a shared codebase. A pre-commit hook catches it fast:grep -r "rejectUnauthorized.*false" src/.

