编程语言

self-signed certificate in certificate chain 报错原因与修法

作者: · 最后核实

报错信息SSL certificate problem: self-signed certificate in certificate chain

self-signed certificate in certificate chain 的意思是:服务器的证书链最终落在一张自签名根证书上,而你的客户端不信任这张根证书。 证书链本身是完整的,缺的是对顶端根证书的信任。在办公电脑上,几乎都是公司的 HTTPS 解密代理造成的;其他情况多是内网 CA 的根证书没有下发到客户端。

快速修复

修法是找到证书链顶端的根证书,核对后加进报错工具的信任库。先看顶端是哪张根证书:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null \
  | grep -E '^ *[0-9] s:|^ *i:'

看最后一张证书:如果它的 subject 和 issuer 相同,并且写的是公司名、安全厂商名或内部 CA,那就是要信任的根证书。向 IT 部门要 PEM 格式的文件(或从系统证书库导出),核对指纹后,加到报错的工具里:

export NODE_EXTRA_CA_CERTS=/path/company-root.pem          # Node.js
npm config set cafile /path/ca-bundle.pem                 # npm
git config --global http.sslCAInfo /path/ca-bundle.pem    # Git
export REQUESTS_CA_BUNDLE=/path/ca-bundle.pem             # Python requests
curl --cacert /path/ca-bundle.pem https://example.com     # curl

npm、Git、curl 和 Python 的这些参数会“替换”默认 CA 列表,所以 ca-bundle.pem 应该是系统证书包再追加上公司根证书;NODE_EXTRA_CA_CERTS 则是在 Node 内置根证书基础上“追加”。

原因

办公电脑上最常见的原因是公司代理或杀毒软件解密 HTTPS,其次是内网 CA 没有下发到客户端。

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

上网行为管理、安全网关以及部分杀毒软件的“HTTPS 扫描”功能,会解密流量后用自己根证书签发的证书重新加密。

  • 确认: 访问任何外部网站,链顶端都是同一张陌生的根证书;换到家里网络或手机热点后不再报错。
  • 修复: 按上面的方法逐个工具信任代理根证书,或者请 IT 部门把 registry.npmjs.org、pypi.org、国内镜像源等软件源加入解密白名单。

2. 内网 CA 没有下发到客户端

GitLab、Nexus、Harbor、内部 API 等服务常用公司自建的私有 CA,只有装了它根证书的机器才能验证通过。

  • 确认: 只有访问内网域名时报错,根证书的名称是公司内部 CA。
  • 修复: 通过系统信任库(update-ca-certificates 或 update-ca-trust)分发根证书,再给各个运行时单独配置。

3. 服务器发送了错误或过时的根证书

有些服务器把根证书也拼进了证书链。如果这张根证书是私有的、已过期的,或者与客户端信任的版本不同(交叉签名的旧版本),客户端可能就在这里停下。

  • 确认: 用证书解析工具查看,服务器发出的链里包含根证书,而其他客户端在没有它时也能正常访问。
  • 修复: 服务器只发送站点证书和中间证书,可以用证书链补全工具重新生成证书链。在宝塔或云厂商控制台粘贴证书时,注意不要把根证书也贴进去。

4. 开发环境的本地 CA(mkcert、本地 Kubernetes、测试环境)

本地 CA 安装后浏览器会信任,但自带 CA 列表的运行时不会。

  • 修复: 用 NODE_EXTRA_CA_CERTS 等方式显式加载开发根证书,不要把它带进生产镜像。

各环境修法

每个工具都要单独信任这张根证书,区别只在信任库的位置和命令。

Linux 系统信任库(Debian、Ubuntu、Alpine):

sudo cp company-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

CentOS、RHEL、Rocky、Alibaba Cloud Linux: 复制到 /etc/pki/ca-trust/source/anchors/,再执行 sudo update-ca-trust。

Windows: Git for Windows 可以执行 git config --global http.sslBackend schannel 改用 Windows 证书库,自动信任域策略下发的根证书。Node.js 和 Python requests 仍需要上面的单独配置。

macOS: 把根证书导入“系统”钥匙串并设为始终信任,Node、npm、Python 仍要分别配置。

Java: 把根证书导入 JVM 信任库,见 PKIX path building failed。

Docker 构建: 在任何 npm install、pip install 之前把根证书复制进镜像并执行 update-ca-certificates,运行 Node 的地方再设置 NODE_EXTRA_CA_CERTS。如果你已经把 npm 源换成了 npmmirror 等国内镜像,经过公司代理时同样会遇到这个报错,换源并不能绕过它。

不要这样做

关闭校验能消除报错,但对应工具从此不再验证任何服务器的身份。

  • NODE_TLS_REJECT_UNAUTHORIZED=0。 Node 会打印警告,因为它关闭的是整个进程所有 TLS 连接的校验。
  • npm config set strict-ssl false。 之后下载软件包时完全不验证服务器身份,这正是供应链投毒常用的入口。
  • git config --global http.sslVerify false。 它对所有远程仓库生效,不只是报错的那一个。
  • verify=False 或 curl -k。 同样的问题:失去的是全部保护,而不只是报错的那一项检查。

常见问题

self-signed certificate 和 self-signed certificate in certificate chain 有什么区别?

前者(错误 18,Node 里叫 DEPTH_ZERO_SELF_SIGNED_CERT)是服务器证书本身就是自签名的;后者(错误 19)是服务器证书由某个 CA 签发,但整条链最终落在一张你不信任的自签名根证书上。前一种情况见 DEPTH_ZERO_SELF_SIGNED_CERT。

为什么只有在公司网络里才报这个错?

很多公司的上网行为管理或安全网关会解密 HTTPS,再用公司根证书现场签发证书。域控下发的电脑上,浏览器通过系统证书库信任这张根证书;但 Node.js、npm、Linux 上的 Git、Python requests、Java 都用自己的 CA 列表,所以会拒绝。换成手机热点后报错消失,基本就能确认是这个原因。

信任公司代理的根证书安全吗?

在公司管理的电脑上,这本来就是预期的配置,代理本身已经能看到你的流量。只信任 IT 部门提供的那一张根证书,并和他们核对指纹;不要把它装进个人设备或公开发布的容器镜像里。

Node.js 怎么信任 mkcert 生成的开发 CA?

mkcert -install 会把根证书装进系统和浏览器,但 Node.js 用的是内置的 CA 列表。启动 Node 时加上 NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem",让它在启动时额外加载这张根证书。

资料来源

  1. OpenSSL 3.0: X509_STORE_CTX_get_error (verify error codes)
  2. Node.js CLI: NODE_EXTRA_CA_CERTS
  3. npm config: cafile and strict-ssl
  4. git-config: http.sslCAInfo and http.sslBackend
  5. curl: SSL Certificate Verification