编程语言
unable to verify the first certificate:Node.js 缺中间证书
Error: unable to verify the first certificate (code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')unable to verify the first certificate 的意思是:Node.js 收到了服务器证书,却找不到签发它的那张 CA 证书,验证在第一步就失败了。 最常见的原因是服务器没有发送中间证书。
快速修复
修法是让服务器发送完整证书链;服务器不归你管时,用 NODE_EXTRA_CA_CERTS 补上缺失的证书。先看服务器发出的证书链:
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null | grep -E "^ *[0-9] s:|Verify return"
如果只有 0 s:,并且显示 Verify return code: 21 (unable to verify the first certificate),就去修服务器:发送站点证书加中间证书(Nginx 用 ssl_certificate fullchain.pem;)。用证书链补全工具可以生成正确的文件。
如果服务器不归你管(比如对接第三方接口),在启动时给 Node.js 补上缺失的中间证书或私有根证书:
export NODE_EXTRA_CA_CERTS=/etc/ssl/extra/example-intermediate.pem
node app.js
原因
最常见的是服务器没发中间证书,其余 3 种是链的顺序错、代码自带 ca 选项和企业代理。
1. 服务器没发中间证书
- 确认: 上面的
openssl s_client只列出一张证书;SSL 检测工具提示证书链不完整。 - 修复: 部署完整证书链。Let's Encrypt 用
fullchain.pem;商业证书把站点证书和 CA 提供的中间证书包按“站点证书在前”拼在一起。阿里云、腾讯云下载的 Nginx 格式证书一般已包含中间证书,宝塔里“证书(PEM格式)”一栏要粘贴完整内容。见缺少中间证书。
2. 中间证书顺序错或者发错了
有的服务器发的是旧链路的中间证书,或者顺序颠倒。
- 确认: 第 0 张证书的签发者(issuer)和第 1 张证书的主体(subject)对不上。
- 修复: 从 CA 官网取当前的中间证书重新拼接。
3. 代码里自己传了 ca
在 https.Agent 或 tls.connect 里设置 ca 选项后,Node.js 只信任这个列表,不再使用内置的根证书。
- 确认: 在代码里搜
ca:或自定义的Agent。 - 修复: 把需要的根证书都放进这个列表,或者改用
NODE_EXTRA_CA_CERTS,保留默认根证书。
4. 企业代理只发了站点证书
部分上网行为管理设备生成的证书由一张它从不发送的中间证书签发。
- 修复: 向 IT 要代理的完整 CA 链,用
NODE_EXTRA_CA_CERTS加进来。
各环境修法
不管在代码、Docker 还是云函数里,都是把缺的证书加进信任列表,同时保留内置根证书。
单个请求(你自己的代码):
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));
使用操作系统证书库: node --use-openssl-ca 让 Node.js 改用 OpenSSL 的默认 CA 库(大多数 Linux 发行版就是系统证书包),而不是内置列表。较新的 Node.js 还提供 --use-system-ca,以 node --help 的输出为准。
Docker: 把证书打进镜像,再设置环境变量:
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、云函数(阿里云函数计算、腾讯云 SCF 等): 在启动环境或函数配置里设置 NODE_EXTRA_CA_CERTS,程序启动后再改不会生效。
npm、yarn、pnpm 都运行在 Node.js 上,会报同一类错误,见 npm unable to get local issuer certificate。
不要这样做
关闭校验和固定站点证书都不是修法:前者去掉了保护,后者在下次续期时失效。
NODE_TLS_REJECT_UNAUTHORIZED=0。 它会关掉进程里所有 TLS 连接的证书校验,包括调用支付、登录接口的请求。Node.js 设置后会打印警告。- 在公共 HTTP 客户端里写
rejectUnauthorized: false。 所有复用这个客户端的调用都会悄悄失去保护。 - 把站点证书本身当成“CA”固定下来。 下次证书续期就失效;证书有效期还在不断缩短,续期只会越来越频繁。
常见问题
为什么浏览器能打开,Node.js 请求却报错?
因为浏览器会自动补全缺失的中间证书,Node.js 不会。Chrome 会按证书里“颁发机构信息访问(AIA)”扩展的地址下载缺失的中间证书,还会缓存见过的中间证书。Node.js 用 OpenSSL 验证,不做这种修补,所以证书链不完整的服务器只在 Node.js、curl、Python 这类客户端里报错。
它和 UNABLE_TO_GET_ISSUER_CERT_LOCALLY 有什么区别?
UNABLE_TO_VERIFY_LEAF_SIGNATURE(unable to verify the first certificate)是证书链在站点证书之后就断了;UNABLE_TO_GET_ISSUER_CERT_LOCALLY 是链条构建得更长,但最顶上那张不在本地信任库里,更多是代理或私有 CA 导致的。
NODE_EXTRA_CA_CERTS 会替换掉内置的 CA 吗?
不会。它把文件里的证书追加到 Node.js 内置的 Mozilla CA 列表里。它只在进程启动时读取一次,所以要在启动 Node.js 之前设置,在代码里用 process.env 改是无效的。
只对某一个请求设 rejectUnauthorized: false 可以吗?
不建议。它比全局环境变量好一点,但这个请求依然可以被中间人冒充。给这个请求传正确的 ca 也只是一行代码,没必要关校验。