# Node.js UNABLE_TO_GET_ISSUER_CERT_LOCALLY: causes and fixes

> Node.js cannot find the root that issued the server's chain in its bundled CA store. Add it with NODE_EXTRA_CA_CERTS or --use-system-ca.

Source: https://ssl.sh/errors/node-unable-to-get-issuer-cert-locally/
Last updated: 2026-11-10

**`UNABLE_TO_GET_ISSUER_CERT_LOCALLY` means Node.js got the server's chain but could not find the CA that issued its top certificate among the roots it trusts.** The server is usually fine. Node.js trusts only its bundled copy of the Mozilla CA list, not the roots your company installed in the operating system. Add that root to Node.js; keep verification on.

## What the error means

OpenSSL could not find the issuing root in the Node.js trust store. The error comes from OpenSSL's chain verification and reaches your code as an `Error` with `code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'` and the message `unable to get local issuer certificate`, for example:

```text
Error: unable to get local issuer certificate
    at TLSSocket.onConnectSecure (node:_tls_wrap:...)
  code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
```

"Locally" refers to the local trust store. Node.js builds a chain from the certificates the server sent and then looks for the issuer of the last one among trusted roots. The Node.js documentation describes the default store as a snapshot of the Mozilla CA store that is fixed at release time and identical on all platforms. Anything outside that list, such as a corporate root, a private CA or a new public root, is unknown unless you add it.

## Causes, most common first

### 1. A TLS-inspecting proxy or antivirus

Corporate proxies, secure web gateways and antivirus "HTTPS scanning" re-sign traffic with their own root. Browsers trust it because IT installed it in the operating system store; Node.js does not see that store.

- **Confirm:** check who issued the certificate you receive:

```bash
openssl s_client -connect registry.npmjs.org:443 -servername registry.npmjs.org </dev/null 2>/dev/null | openssl x509 -noout -issuer
```

An issuer naming your company or a security vendor instead of a public CA confirms it. See [corporate SSL inspection](https://ssl.sh/errors/corporate-ssl-inspection/).

- **Fix:** export that root as PEM and add it with `NODE_EXTRA_CA_CERTS`, or use `--use-system-ca`.

### 2. A private or internal CA

Internal APIs, GitLab, Nexus or Harbor servers often use certificates from a company CA. The chain is valid, but its root is private.

- **Confirm:** the issuer is your internal CA, and other machines that have the root installed connect fine.
- **Fix:** distribute the internal root through `NODE_EXTRA_CA_CERTS` or the system store.

### 3. Your code passes an explicit `ca` option

The Node.js docs state that neither the well-known nor the extra certificates are used when the `ca` option is explicitly specified. Code that sets `ca: fs.readFileSync('internal-root.pem')` to reach one internal service will then fail against every public site.

- **Confirm:** search for `ca:` in HTTPS agents, axios, got or undici options.
- **Fix:** append to the defaults instead of replacing them:

```javascript
const tls = require('node:tls');
const fs = require('node:fs');
const ca = [...tls.rootCertificates, fs.readFileSync('/etc/ssl/company-root.pem', 'utf8')];
```

### 4. The server sends an incomplete or unusual chain

If the server sends some intermediates but not all of them, or a chain that ends at a root absent from Node's snapshot, the result is the same error. Run the host through the [chain builder](https://ssl.sh/tools/chain-builder/) to see what it sends and what a complete chain looks like. If the server sends only the leaf, you will more often see [UNABLE_TO_VERIFY_LEAF_SIGNATURE](https://ssl.sh/errors/node-unable-to-verify-leaf-signature/).

- **Fix:** the server must send the full chain (for Nginx, `fullchain.pem`).

### 5. An outdated Node.js release

The bundled store only changes when Node.js is upgraded. An old Node.js may lack a root that public CAs now use. `node -p "process.version"` tells you what you run; upgrade to a supported release.

## Fix commands by platform

Every fix adds the missing root to what Node.js trusts, through `NODE_EXTRA_CA_CERTS` or the operating system store.

**Any platform, no code change:**

```bash
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/company-root.pem
node app.js
```

**Windows and macOS, Node.js 22.15+ / 23.8+:** trust the operating system store, including roots pushed by Group Policy or MDM:

```bash
node --use-system-ca app.js
# or, on Node.js 24.6+ / 22.19+
NODE_USE_SYSTEM_CA=1 node app.js
```

On Windows PowerShell use `$env:NODE_USE_SYSTEM_CA = "1"` before starting node.

**Debian / Ubuntu:** install the root into the system store, then let Node.js read it:

```bash
cp company-root.crt /usr/local/share/ca-certificates/ && update-ca-certificates
node --use-openssl-ca app.js      # or --use-system-ca on newer releases
```

**Docker:** copy the root into the image and set the variable in the Dockerfile:

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

## Don't disable certificate verification

Disabling verification hides the error and exposes every TLS connection in the process. `NODE_TLS_REJECT_UNAUTHORIZED=0`, `rejectUnauthorized: false` and `npm config set strict-ssl false` make the error disappear by accepting any certificate. The Node.js documentation calls the environment variable strongly discouraged, and Node.js itself prints a warning that it makes TLS connections and HTTPS requests insecure. With it set, a malicious Wi-Fi network or compromised proxy can impersonate your API, package registry or identity provider and capture tokens, or serve tampered packages to `npm install`.

The safer alternatives are precise: `NODE_EXTRA_CA_CERTS` for one extra root, `--use-system-ca` to reuse what IT already deployed, `ca: [...tls.rootCertificates, extraRoot]` in code, and a full chain on servers you control.

## Verify the fix

```bash
node -e "require('https').get('https://example.com', r => { console.log(r.statusCode, r.socket.authorized); r.resume(); }).on('error', e => console.error(e.code, e.message))"
```

It should print a status code and `true` (the socket is authorized), with no `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` and no NODE_TLS_REJECT_UNAUTHORIZED warning. Make sure `NODE_TLS_REJECT_UNAUTHORIZED` is not set anywhere (`env | grep NODE_TLS`). For the theory of how the chain is built, see [certificate chains explained](https://ssl.sh/learn/certificate-chains/).
