# Node.js UNABLE_TO_VERIFY_LEAF_SIGNATURE: fix the chain

> "unable to verify the first certificate" in Node.js usually means the server sends no intermediate certificate. Fix the server chain or use NODE_EXTRA_CA_CERTS.

Source: https://ssl.sh/errors/node-unable-to-verify-leaf-signature/
Last updated: 2026-11-10

**`unable to verify the first certificate` means Node.js received the server's certificate but could not find the CA certificate that signed it, so verification fails at the first step.** Usually the server is not sending its intermediate certificate.

## Quick fix

A single certificate in the output confirms the missing intermediate.

Check the chain the server sends:

```bash
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null | grep -E "^ *[0-9] s:|Verify return"
```

If you see only `0 s:` and `Verify return code: 21 (unable to verify the first certificate)`, fix the server: serve the leaf plus intermediates (Nginx `ssl_certificate fullchain.pem;`). The [chain builder](https://ssl.sh/tools/chain-builder/) produces the right file.

If you cannot change the server, give Node.js the missing intermediate or private root at startup:

```bash
export NODE_EXTRA_CA_CERTS=/etc/ssl/extra/example-intermediate.pem
node app.js
```

## Causes

A server that omits its intermediate certificate is the usual cause.

### 1. The server omits the intermediate certificate

- **Confirm:** the `openssl s_client` output above lists a single certificate; the [SSL checker](https://ssl.sh/tools/ssl-checker/) reports an incomplete chain.
- **Fix:** install the full chain. For Let's Encrypt use `fullchain.pem`; for commercial CAs concatenate the leaf and the CA bundle the CA provides (leaf first). See [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/).

### 2. Wrong order or wrong intermediate

Some servers send intermediates from an older chain, or in the wrong order.

- **Confirm:** the issuer of certificate 0 does not match the subject of certificate 1.
- **Fix:** rebuild the bundle from the CA's current intermediate.

### 3. Your code passes its own `ca` list

When you set the `ca` option on `https.Agent` or `tls.connect`, Node.js uses only that list instead of its default root store.

- **Confirm:** search the code for `ca:` or a custom `Agent`.
- **Fix:** include every needed root in that list, or use `NODE_EXTRA_CA_CERTS` so that the defaults stay in place.

### 4. TLS-inspecting proxy that sends only its leaf

Some proxies generate a leaf certificate signed by an intermediate they never send.

- **Fix:** get the proxy's full CA chain from IT and add it with `NODE_EXTRA_CA_CERTS`.

## Fixes by environment

Fix the server chain where you can; on clients, add the issuing CA through the `ca` option or `NODE_EXTRA_CA_CERTS`.

**Per request (code you own):**

```js
const fs = require("node:fs");
const https = require("node:https");
const tls = require("node:tls");

const agent = new https.Agent({
  ca: [...tls.rootCertificates, fs.readFileSync("/etc/ssl/extra/company-root.pem")],
});
https.get("https://api.example.com/", { agent }, (res) => console.log(res.statusCode));
```

**Using the operating system store:** `node --use-openssl-ca` makes Node.js use OpenSSL's default CA store (the system bundle on most Linux distributions) instead of its bundled list. Recent Node.js releases also offer `--use-system-ca`; check `node --help` for your version.

**Docker:** add the certificate to the image and point the variable at it:

```dockerfile
COPY company-root.pem /usr/local/share/ca-certificates/company-root.crt
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/company-root.crt
```

**Electron and serverless:** set `NODE_EXTRA_CA_CERTS` in the launch environment or function configuration; it cannot be set from application code after start-up.

**npm and yarn** run on Node.js and show the same family of errors; see [npm unable to get local issuer certificate](https://ssl.sh/errors/npm-unable-to-get-local-issuer-certificate/).

## Don't do this

- **`NODE_TLS_REJECT_UNAUTHORIZED=0`.** It disables certificate verification for every TLS connection in the process, including calls to payment and auth APIs. Node.js prints a warning for a reason.
- **`rejectUnauthorized: false` in a shared HTTP client.** It quietly spreads to every caller of that client.
- **Pinning the leaf certificate as a "CA".** It works until the next renewal, which with short certificate lifetimes is weeks away.
