Programming languages

Node.js UNABLE_TO_GET_ISSUER_CERT_LOCALLY: causes and fixes

By · Last verified

Error messageUNABLE_TO_GET_ISSUER_CERT_LOCALLY

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:

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:
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.

  • 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:
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 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.

  • 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:

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:

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:

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:

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

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.

FAQ

What is the difference between UNABLE_TO_GET_ISSUER_CERT_LOCALLY and UNABLE_TO_VERIFY_LEAF_SIGNATURE?

Both mean the chain could not be completed. UNABLE_TO_VERIFY_LEAF_SIGNATURE usually appears when the server sent only its leaf certificate, so Node.js cannot verify even the first link; see UNABLE_TO_VERIFY_LEAF_SIGNATURE. UNABLE_TO_GET_ISSUER_CERT_LOCALLY usually means the chain reached a CA, often a proxy or private root, that is not in Node's trust store.

I set NODE_EXTRA_CA_CERTS but nothing changed. Why?

Node.js reads it only once, when the process starts, so setting process.env.NODE_EXTRA_CA_CERTS in code has no effect. It is also ignored when an explicit ca option is passed, when node runs setuid root, and if the file is missing or malformed Node.js only emits a warning. Export it in the shell, service unit or container environment that launches node.

Does npm use the same settings?

npm runs on Node.js, so NODE_EXTRA_CA_CERTS works for it too, and npm also has its own cafile setting. See npm unable to get local issuer certificate for npm-, yarn- and pnpm-specific steps.

Why does the site open in Chrome on the same machine?

Chrome uses the operating system trust store, where your IT department installed the proxy or company root. Node.js uses its own bundled list unless you start it with --use-system-ca or set NODE_USE_SYSTEM_CA=1 (Node.js 24.6+ and 22.19+).

Sources

  1. Node.js docs: TLS — X509 certificate error codes
  2. Node.js docs: CLI — NODE_EXTRA_CA_CERTS, --use-system-ca, --use-openssl-ca
  3. npm docs: config (cafile, ca, strict-ssl)
  4. curl: CA certificates extracted from Mozilla