Fix DEPTH_ZERO_SELF_SIGNED_CERT Error in Node.js When Calling Internal HTTPS APIs

intermediate🌐 Networking2026-07-04| Node.js 14+ on Linux/macOS/Windows, using https module, fetch, or axios to call internal APIs with self-signed TLS certificates

Error Message

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

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_CERTS is 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: false to a shared codebase. A pre-commit hook catches it fast: grep -r "rejectUnauthorized.*false" src/.

Related Error Notes