Fix Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE in Node.js HTTPS Requests

intermediate๐Ÿ”’ SSL/TLS2026-05-14| Node.js 14+ on Linux/macOS/Windows, any version making HTTPS requests via https, axios, node-fetch, got, or similar libraries

Error Message

Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE
#nodejs#https#ssl#certificate-chain#intermediate-cert

The Error

Your Node.js app hits an HTTPS endpoint and dies with:

Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE
    at TLSSocket.onConnectEnd (_tls_wrap.js:1495:19)
    at Object.onceWrapper (events.js:422:26)
    at TLSSocket.emit (events.js:327:22)

It's 2 AM. The endpoint loads fine in Chrome, curl on your laptop doesn't complain, but Node.js flat-out refuses to connect. The certificate is valid โ€” you can see it with your own eyes. So what gives?

What This Error Actually Means

This is not an expired cert. It's not self-signed either. The problem is that Node.js can't build a trusted chain from the server's certificate up to a root CA it recognizes.

Nine times out of ten, the cause is simple: the server sends its leaf certificate during the TLS handshake but omits the intermediate CA certificate(s). No intermediate, no chain. No chain, no connection.

Browsers handle this gracefully โ€” they fetch missing intermediates automatically using Authority Information Access (AIA) and cache them locally. Node.js doesn't. It needs the full chain handed to it up front, every single time.

Reproduce and Diagnose

Step 1 โ€” Check whether the server has a broken chain

Run OpenSSL directly against the server:

openssl s_client -connect yourdomain.com:443 -showcerts

A healthy chain shows at least two certificates:

Certificate chain
 0 s:CN=yourdomain.com
   i:CN=Some Intermediate CA
 1 s:CN=Some Intermediate CA
   i:CN=Root CA

A broken chain shows only one:

Certificate chain
 0 s:CN=yourdomain.com
   i:CN=Some Intermediate CA

Just the leaf. The intermediate is gone. That's your culprit.

Step 2 โ€” Confirm Node.js is failing

node -e "require('https').get('https://yourdomain.com', r => console.log(r.statusCode)).on('error', e => console.error(e.message))"

See UNABLE_TO_VERIFY_LEAF_SIGNATURE in the output? Confirmed.

Solutions

Solution 1 โ€” Fix the server (the right fix)

This is a server misconfiguration, not a Node.js bug. Fix it at the source: configure the server to send the full certificate chain.

For Nginx, bundle your certs into a single file and point ssl_certificate at it:

# Concatenate leaf + intermediate
cat your_domain.crt intermediate.crt > fullchain.pem

# In nginx.conf
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/your_domain.key;

For Apache:

SSLCertificateFile /etc/ssl/certs/your_domain.crt
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt

For a Node.js HTTPS server:

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

https.createServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
  ca: fs.readFileSync('intermediate.crt')  // add this line
}, app).listen(443);

Reload the server, then re-run openssl s_client. You should now see both the leaf and the intermediate in the chain output.

Solution 2 โ€” Supply the CA cert from the Node.js client

Don't control the server? Maybe you're calling a third-party API with a broken chain. In that case, tell Node.js exactly which CA cert to trust:

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

const agent = new https.Agent({
  ca: fs.readFileSync('/path/to/intermediate-or-root-ca.crt')
});

https.get({
  hostname: 'yourdomain.com',
  path: '/',
  agent: agent
}, (res) => {
  console.log(res.statusCode);
});

Using axios? Same idea, different option name:

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

const agent = new https.Agent({
  ca: fs.readFileSync('/path/to/ca-bundle.crt')
});

const response = await axios.get('https://yourdomain.com/api', {
  httpsAgent: agent
});

This is a workaround, not a permanent fix. Push the server team to resolve the chain properly.

Solution 3 โ€” Refresh the system CA bundle (Linux servers)

Fresh Linux boxes sometimes ship with an outdated CA bundle that's missing the issuing root. A quick update usually sorts it:

# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y ca-certificates
sudo update-ca-certificates

# RHEL/CentOS
sudo yum update ca-certificates
update-ca-trust

Node.js reads the system CA bundle at startup. No code changes needed after the update โ€” just restart your process.

What NOT to do

This suggestion is all over Stack Overflow. Resist it:

// DO NOT DO THIS IN PRODUCTION
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

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

Turning off certificate verification is not a fix. It disables TLS validation entirely, leaving your app wide open to man-in-the-middle attacks. Fine in a local throwaway script. A genuine security incident in production.

Verify the Fix

Three commands to confirm everything is working:

# 1. Check the server sends the full chain
openssl s_client -connect yourdomain.com:443 -showcerts 2>&1 | grep -E '(subject|issuer|Certificate chain)'

# 2. Verify the chain validates against the system CA store
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /dev/null  console.log('OK:', r.statusCode)).on('error', e => console.error('FAIL:', e.message))"

You should see OK: 200. Or whatever status code the endpoint normally returns โ€” the point is no more error.

For a deeper audit, run the server through SSL Labs. It will flag missing intermediates explicitly and give you a chain visualization worth bookmarking.

Lessons Learned

  • Browsers lie to you. Chrome and Firefox silently fetch missing intermediates and cache them. You can load a page just fine while Node.js, curl, and every server-to-server client reject the same endpoint. Always test TLS with openssl s_client โ€” not a browser tab.
  • Certificate renewals are the #1 trigger. Someone renewed the leaf cert, copied it into place, and forgot to re-bundle the intermediate. Add the openssl s_client chain check to your deployment runbook. It takes five seconds and catches this every time.
  • Client-side workarounds are a smell. If you're injecting ca overrides in every service that talks to an endpoint, that's technical debt accumulating. The chain belongs on the server. File the ticket.
  • Container base images go stale. A Node.js app in a Docker image built from node:16 a year ago may not trust newer root certificates. Add RUN apt-get install -y ca-certificates to your Dockerfile and rebuild โ€” don't chase this bug in production at 2 AM.

Related Error Notes