UNABLE_TO_VERIFY_LEAF_SIGNATURE

Node.js raises "unable to verify the first certificate", error code UNABLE_TO_VERIFY_LEAF_SIGNATURE, when the server presented its own certificate but not the intermediate CA certificates needed to link it to a trusted root. Node ships its own copy of the Mozilla root store and, unlike a browser, never fetches a missing intermediate over the Authority Information Access extension, so a chain a browser silently repairs fails outright under Node. The correct fix is on the server, which must serve the full chain leaf-first; where that server is not yours, supply the missing issuer to Node with `NODE_EXTRA_CA_CERTS=/path/to/intermediate.pem`, which adds to the bundled roots rather than replacing them.

Updated August 19, 2026 Verified against OpenSSL 3.5.7

What the error actually means #

“The first certificate” is the one at the bottom of the chain — the server’s own. Node could not verify its signature, because it does not have the certificate of whoever signed it.

That is almost always because the server did not send it. A TLS server is required to present its own certificate and every intermediate needed to reach a trusted root; sending only the leaf is the single most common TLS misconfiguration on the internet.

Which Node TLS error do you have? #

Node’s codes are precise, and each points at a different problem. Read yours out of the error object before doing anything else:

Code Meaning Where to go
UNABLE_TO_VERIFY_LEAF_SIGNATURE Server sent no intermediates This page
UNABLE_TO_GET_ISSUER_CERT_LOCALLY Issuer missing from Node’s store unable to get local issuer certificate
SELF_SIGNED_CERT_IN_CHAIN Chain ends in an untrusted self-signed CA self signed certificate in chain
DEPTH_ZERO_SELF_SIGNED_CERT The server certificate signed itself self-signed vs CA-signed
CERT_HAS_EXPIRED Validity dates have passed certificate has expired
ERR_TLS_CERT_ALTNAME_INVALID Hostname not in the SAN list SAN explained

Step 1 — confirm the chain is short #

Count the certificates the server offers
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"
Expected output
1

A single certificate from a publicly trusted CA confirms the diagnosis. Two or more, and the chain is complete — in which case you have a different error code, and the table above is the right place to look.

Step 2 — fix the server, if it is yours #

Concatenate the certificate and its intermediates into one file, leaf first, walking upward:

Build a correctly ordered chain
cat certificate.crt chain.crt > fullchain.crt

Prove it validates before deploying it. -untrusted supplies the intermediates that the chain needs but that are not themselves trust anchors:

Verify the repaired chain
openssl verify -CAfile root-ca.crt -untrusted chain.crt certificate.crt
Expected output
certificate.crt: OK

Then point the server at fullchain.crt rather than at the bare certificate — nginx in particular has no separate chain directive, so ssl_certificate must name the combined file. The per-server configuration is on install an SSL certificate on nginx.

Step 3 — where the server is not yours #

Supply the missing issuer to Node directly. Save the intermediate as PEM and name it:

export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/intermediate.pem
node app.js

For a single request, pass the certificate in code instead of changing the environment:

Per-request trust, verification intact
import { readFileSync } from 'node:fs';
import https from 'node:https';

const agent = new https.Agent({
ca: readFileSync('/etc/ssl/certs/intermediate.pem'),
});

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

Or take the OpenSSL system store instead of Node’s bundled list, which is the right choice on a machine where a corporate root is already installed:

node --use-openssl-ca app.js

npm, yarn and pnpm #

The package managers use the same TLS stack and surface the same code. Point them at a bundle containing the missing issuer:

Tool Setting
npm npm config set cafile /path/to/ca-bundle.pem
yarn yarn config set cafile /path/to/ca-bundle.pem
pnpm pnpm config set cafile /path/to/ca-bundle.pem
All three NODE_EXTRA_CA_CERTS=/path/to/ca-bundle.pem

What not to do #

Frequently asked questions

What is the difference between this and SELF_SIGNED_CERT_IN_CHAIN?
This error means a link is missing — Node found no issuer for the certificate it was given. SELF_SIGNED_CERT_IN_CHAIN means the chain was complete but terminated in a self-signed certificate Node does not trust, which is what a corporate TLS-inspecting proxy looks like.
Does NODE_EXTRA_CA_CERTS replace the built-in roots?
No, it appends to them, which is the opposite of Java's javax.net.ssl.trustStore. Public sites keep working after you set it. Node reads the file once during startup, so the process must be restarted, and a path that does not exist is ignored with only a warning.
Why does the same URL work in curl and the browser?
They have different tolerances. A browser caches intermediates from earlier visits and fetches missing ones over the Authority Information Access extension; curl uses the system bundle, which on many machines already contains the intermediate. Node does neither, so it is often the first thing to notice a genuinely broken chain.
How do I fix this for npm specifically?
Point npm at a bundle containing the missing issuer with npm config set cafile /path/to/ca-bundle.pem. Do not use npm config set strict-ssl false, which disables verification for every package you install.
Can I use the system trust store instead of Node's bundled one?
Yes. Start Node with --use-openssl-ca, and it reads the OpenSSL default store rather than its compiled-in Mozilla list. This is useful on a machine where a corporate root is already installed system-wide.

References