编程语言

UNABLE_TO_GET_ISSUER_CERT_LOCALLY:Node.js 找不到根证书

作者: · 最后核实

报错信息UNABLE_TO_GET_ISSUER_CERT_LOCALLY

UNABLE_TO_GET_ISSUER_CERT_LOCALLY 的意思是:Node.js 拿到了服务器的证书链,但在它信任的根证书里,找不到签发这条链顶端证书的 CA。 实际情况往往是服务器没问题,只是 Node.js 不认识那张根证书:它只信任自己内置的一份 Mozilla CA 列表,不读取公司装在 Windows、macOS 或 Linux 系统里的根证书。正确做法是把这张根证书交给 Node.js,而不是关闭校验。

这条报错是什么意思

这条报错说明证书链的顶端连不到 Node.js 信任的任何根证书。它来自 OpenSSL 的证书链校验,到你的代码里是一个 Error 对象,code 为 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',消息是 unable to get local issuer certificate,例如:

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

这里的“local(本地)”指的是本地信任库。Node.js 先用服务器发来的证书拼出一条链,再到受信任的根证书里找最后一张证书的签发者。Node.js 文档说明,默认信任库是 Mozilla CA 列表的一份快照,在版本发布时固定,各平台完全相同。公司根证书、内网私有 CA、或者很新的公开根证书,只要不在这份列表里,Node.js 就不认识,除非你手动加进去。

常见原因(按出现频率排序)

最常见的是公司代理或杀毒软件解密了 HTTPS,其次是内网私有 CA 和代码里显式传了 ca 选项。

1. 公司代理或杀毒软件解密了 HTTPS

企业代理、上网行为管理、安全网关和杀毒软件的“HTTPS 扫描”会用自己的根证书重新签名流量。浏览器信任它,是因为 IT 已经把根证书装进了系统;Node.js 看不到系统证书库。

  • 确认: 查看你实际收到的证书是谁签发的:
openssl s_client -connect registry.npmjs.org:443 -servername registry.npmjs.org </dev/null 2>/dev/null | openssl x509 -noout -issuer

签发者是公司名称或安全厂商,而不是公开 CA,就可以确认。见企业 SSL 解密导致的报错。

  • 修复: 把这张根证书导出为 PEM,用 NODE_EXTRA_CA_CERTS 加入,或者使用 --use-system-ca。

2. 内网私有 CA

内部 API、自建 GitLab、Nexus、Harbor 等服务,证书常由公司自己的 CA 签发。证书链本身没问题,只是根证书是私有的。

  • 确认: 签发者是内网 CA;装了这张根证书的其他机器可以正常访问。
  • 修复: 通过 NODE_EXTRA_CA_CERTS 或系统证书库统一分发内网根证书。

3. 代码里显式传了 ca 选项

Node.js 文档写明:显式指定 ca 选项后,内置的根证书和额外证书都不再使用。为了访问某个内网服务而写了 ca: fs.readFileSync('internal-root.pem') 的代码,访问任何公网网站都会报这个错。

  • 确认: 搜索 HTTPS Agent、axios、got、undici 配置里的 ca:。
  • 修复: 在内置根证书的基础上追加,而不是替换:
const tls = require('node:tls');
const fs = require('node:fs');
const ca = [...tls.rootCertificates, fs.readFileSync('/etc/ssl/company-root.pem', 'utf8')];

4. 服务器发的证书链不完整或不常规

服务器只发了部分中间证书,或者发来的链最终指向一个不在 Node.js 快照里的根证书,也会是同样的报错。用证书链补全工具看看服务器发了什么、完整的链应该是什么样。如果服务器只发了站点证书,更常见的报错是 UNABLE_TO_VERIFY_LEAF_SIGNATURE。

  • 修复: 服务器必须发送完整证书链(Nginx 用 fullchain.pem;宝塔面板里粘贴证书时要包含中间证书)。

5. Node.js 版本太旧

内置列表只有升级 Node.js 才会更新。很老的版本可能缺少公开 CA 现在使用的根证书。用 node -p "process.version" 查看版本,升级到仍在维护的版本。

分平台修复命令

各平台的修法都是把缺的根证书交给 Node.js:用 NODE_EXTRA_CA_CERTS 追加,或者让 Node.js 读系统证书库。

任意平台,不改代码:

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

Windows 和 macOS,Node.js 22.15+ / 23.8+: 直接使用操作系统证书库,域策略或 MDM 下发的根证书都能生效:

node --use-system-ca app.js
# Node.js 24.6+ / 22.19+ 也可以用环境变量
NODE_USE_SYSTEM_CA=1 node app.js

Windows PowerShell 里先执行 $env:NODE_USE_SYSTEM_CA = "1" 再启动 node。

Debian / Ubuntu: 把根证书装进系统,再让 Node.js 读取:

cp company-root.crt /usr/local/share/ca-certificates/ && update-ca-certificates
node --use-openssl-ca app.js      # 新版本也可用 --use-system-ca

Docker: 在镜像里复制根证书,并在 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

国内开发环境: 下载依赖慢可以把 registry 换成 npmmirror:npm config set registry https://registry.npmmirror.com。但换镜像只解决速度问题;如果公司网络在解密 HTTPS,访问镜像站同样会报这个错,仍然要按上面的方法加根证书。

从公司电脑导出代理根证书

如果确认是公司代理在解密 HTTPS,最省事的办法是直接向 IT 部门要 PEM 格式的根证书。拿不到时,也可以自己从系统里导出:Windows 上运行 certmgr.msc,在“受信任的根证书颁发机构”里找到代理厂商或公司名称的证书,右键导出为“Base64 编码 X.509(.CER)”,这种文件本身就是 PEM 格式;macOS 上打开“钥匙串访问”,在“系统”钥匙串里找到对应证书,导出为 .pem。导出后用 openssl x509 -in company-root.pem -noout -subject -issuer 检查一下,主题和签发者相同,说明拿到的确实是根证书。

不推荐:关闭证书校验

NODE_TLS_REJECT_UNAUTHORIZED=0、rejectUnauthorized: false、npm config set strict-ssl false 都靠“接受任何证书”让报错消失,代价是失去对服务器身份的验证。Node.js 文档明确表示强烈不建议使用这个环境变量,Node.js 运行时也会打印警告,说明它会禁用证书校验、让 TLS 连接和 HTTPS 请求变得不安全。设置之后,恶意 Wi-Fi 或被入侵的代理可以冒充你的 API、包仓库或登录服务,截获 token,甚至在 npm install 时塞进被篡改的包。

更安全的替代办法:多一张根证书用 NODE_EXTRA_CA_CERTS;复用 IT 已部署的根证书用 --use-system-ca;代码里用 ca: [...tls.rootCertificates, extraRoot];自己的服务器则配置完整证书链。

怎么确认已经修好

修好的标志是下面的命令输出状态码和 true,并且没有 NODE_TLS_REJECT_UNAUTHORIZED 警告。

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))"

应当输出状态码和 true(连接已通过校验),没有 UNABLE_TO_GET_ISSUER_CERT_LOCALLY,也没有 NODE_TLS_REJECT_UNAUTHORIZED 的警告。再用 env | grep NODE_TLS 确认没有任何地方残留这个变量。证书链是怎么拼起来的,见证书链原理。

常见问题

它和 UNABLE_TO_VERIFY_LEAF_SIGNATURE 有什么区别?

区别在于链断在哪一环,两者都表示证书链没能补全。UNABLE_TO_VERIFY_LEAF_SIGNATURE 通常出现在服务器只发了站点证书、Node.js 连第一环都无法验证的时候,见 UNABLE_TO_VERIFY_LEAF_SIGNATURE;UNABLE_TO_GET_ISSUER_CERT_LOCALLY 通常表示链条已经到了某个 CA,但这个 CA(多半是代理或内网根证书)不在 Node.js 的信任列表里。

设置了 NODE_EXTRA_CA_CERTS 没效果,为什么?

Node.js 只在进程启动时读取一次,在代码里改 process.env.NODE_EXTRA_CA_CERTS 没有用。显式传了 ca 选项、以 setuid root 运行时它也会被忽略;文件不存在或格式不对时 Node.js 只发一条警告。要在启动 node 的 shell、systemd 服务或容器环境变量里设置。

npm、yarn、pnpm 也适用吗?

它们都跑在 Node.js 上,所以 NODE_EXTRA_CA_CERTS 同样有效;npm 另外还有自己的 cafile 配置。具体做法见 npm unable to get local issuer certificate。

换成 npmmirror 镜像能绕过这个错误吗?

如果报错来自公司代理解密 HTTPS,换镜像没用,因为访问镜像站的流量同样会被代理重新签名。镜像只解决下载慢的问题,证书问题仍然要靠把代理根证书加给 Node.js 来解决。

资料来源

  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. npmmirror 镜像站