编程语言

DEPTH_ZERO_SELF_SIGNED_CERT:Node.js 正确信任自签名证书

作者: · 最后核实

报错信息Error: self-signed certificate (code: 'DEPTH_ZERO_SELF_SIGNED_CERT')

self-signed certificate(DEPTH_ZERO_SELF_SIGNED_CERT)的意思是:服务器出示的证书是自己签自己的,Node.js 没有任何理由信任它。 本地 HTTPS 服务和很多内部工具都会这样,正确做法是告诉 Node.js 信任这张证书或签发它的 CA,而不是关掉校验。

快速修复

开发环境最省事的做法是用 mkcert 创建本地 CA,再用它签发证书,然后让 Node.js 信任这个 CA:

mkcert -install
mkcert localhost 127.0.0.1 ::1
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
node app.js

对于不归你管的单个内部服务(比如公司的 NAS、测试服务器),先取下它的证书,和负责人核对指纹,再作为 ca 传入:

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/nas-example.pem")],
});
https.get("https://nas.example.com/", { agent }, (res) => console.log(res.statusCode));

原因

最常见的是开发或内部服务器本来就用自签名证书;其次是服务器返回了默认证书,或者网络上有设备拦截。

1. 开发或内部服务器用的是自签名证书

各种框架、测试容器、路由器、NAS、管理后台,首次启动时经常自动生成一张自签名证书。

  • 确认: openssl s_client -connect host:443 -servername host </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer 显示 subject 和 issuer 完全相同。
  • 修复: 换成内部 CA 签发的证书,或者按上面的方法显式信任。用自签名证书生成器可以生成带正确 SAN 的证书。

2. 响应的是错误的虚拟主机

请求的域名和服务器上配置的站点都对不上时(比如用 IP 访问、没带 SNI),Web 服务器会出示默认证书,常常是自签名的 “snakeoil” 证书。

  • 确认: subject 是 localhost 或机器名这类通用名字,不是你的域名。
  • 修复: 用正确的主机名连接;自己写 TLS 连接代码时设置 servername 发送 SNI。在服务器端把正确的站点设为该端口的默认站点。

3. 网络上有拦截

公共 Wi-Fi 的认证页、某些安全设备无法完成解密时,会出示自己的自签名证书。

  • 确认: issuer 是某个网络设备或厂商名,只在某个网络下报错。
  • 修复: 联系网络管理员;如果是公司代理,应该信任它的 CA,而不是一张自签名的站点证书。

4. 信任之后又报主机名不匹配

信任证书后如果改报 ERR_TLS_CERT_ALTNAME_INVALID,说明证书的 SAN 里没有你访问的主机名,用正确的名字重新生成。

各环境修法

在开发框架、Docker 和 Electron 里,都用 ca 选项或 NODE_EXTRA_CA_CERTS 信任指定的证书或 CA。

Vite、Next.js、webpack-dev-server 本地开发: 用 mkcert 生成证书,服务端代码要调用开发服务器时设置 NODE_EXTRA_CA_CERTS。

Docker Compose: 挂载 CA 文件,在服务的 environment 里设置 NODE_EXTRA_CA_CERTS。

Axios / node-fetch / undici: Axios 支持传 httpsAgent;基于 undici 的 fetch 使用 Node.js 默认信任库,设置 NODE_EXTRA_CA_CERTS 最简单;直接用 undici 时给 Agent 传 connect: { ca }。

Electron: 应用代码用 ca 选项或 NODE_EXTRA_CA_CERTS;Chromium 渲染进程走操作系统证书库。

对接内网系统(OA、ERP、政企内网接口): 向对方要根证书或证书指纹,按上面方法信任,并在代码注释里写明证书来源和到期时间,方便以后更换。

不要这样做

下面 3 种做法会让一批连接失去校验,而不只是报错的那一个。

  • NODE_TLS_REJECT_UNAUTHORIZED=0。 整个进程的所有连接都不再校验,而且很容易留在 .env 文件里被带到生产环境。
  • 在公共 HTTP 客户端里默认 rejectUnauthorized: false。 调用公网 API 时问题也被一起掩盖了。
  • “内网主机一律接受自签名证书”。 内网并不天然可信,应该只信任特定的证书或 CA。

常见问题

它和 SELF_SIGNED_CERT_IN_CHAIN 有什么区别?

DEPTH_ZERO_SELF_SIGNED_CERT 是服务器自己的证书就是自签名的(链条只有一张);SELF_SIGNED_CERT_IN_CHAIN 是服务器发了一条证书链,链顶的根证书是自签名且不受信任的,多见于私有 CA 和企业代理。见证书链中有自签名证书。

能用 NODE_EXTRA_CA_CERTS 信任一张自签名证书吗?

可以,这张证书会被当作信任锚。但证书的 SAN 仍然必须包含你连接的主机名;而且证书每次重新生成,都要更新这个文件。用本地 CA 就没这个麻烦。

访问公网网站也报这个错,是怎么回事?

说明你连错了站点,或者网络上有设备在拦截,公网网站不应该出示自签名证书。要么你连到了默认或配置错误的站点(比如用 IP 访问),要么网络上有设备在拦截连接(公共 Wi-Fi 认证页、某些安全设备)。用 SSL 检测工具从外部检查一下这个网站。

国内云服务器上的测试环境也要这么麻烦吗?

有域名就不用。直接申请免费的 Let's Encrypt 证书或云厂商的免费 DV 证书,开发机和服务器都不用额外配置信任。只有纯内网、没有公网域名的服务,才需要本地 CA 或自签名证书。

资料来源

  1. Node.js TLS (SSL) documentation
  2. Node.js command-line options (NODE_EXTRA_CA_CERTS)
  3. OpenSSL X509_STORE_CTX_get_error (verification error codes)