OpenSSL verify报error 20:CAfile、fullchain与本地信任链分层排查

先看懂 error 20 到底在说什么

看到 unable to get local issuer certificate,很多人先想到证书过期。其实 OpenSSL 的 verify 是在当前信任材料里找不到能继续向上构链的签发者。常见场景是:叶子证书已换新,服务端能完成 TLS 握手,但客户端拿不到中间证书,于是报 error 20 at 0 depth lookup。

四个文件先分角色:叶子、根、untrusted 与 fullchain

叶子证书 是给域名用的终端证书;根证书 是客户端信任的锚点;中间证书 负责把叶子连接到根。对 openssl verify 来说,-CAfile 表示信任锚点,-untrusted 表示"可以拿来拼链、但不因此直接信任"的中间证书。Nginx 的 ssl_certificate 文件则有另一个交付语义:主证书在前,中间证书紧随其后,也就是常说的 fullchain.pem。

因此,"把中间证书放进 CAfile"不是服务器链配置的替代方案;"本机 verify 成功"也不等于客户端从 HTTPS 服务端收到了完整链。

第一步:确认 PEM 对象和 issuer 没拿错

不要只看文件名。先把证书的主题、签发者、序列号和 SAN 读出来,再比较叶子证书的 issuer 是否对应中间 CA 的 subject。如果 issuer 对不上,继续补证书只是在错误链上堆文件。

bash 复制代码
set -eu
for f in leaf.pem intermediate.pem root.pem; do
  echo "--- $f ---"
  openssl x509 -in "$f" -noout -subject -issuer -serial -dates
  openssl x509 -in "$f" -noout -text | grep -A1 -E 'Subject Alternative Name|Basic Constraints'
done
openssl x509 -in leaf.pem -noout -checkend 0

-checkend 0 只回答"当前是否已经过期",不能证明签发者可达,更不能替代整条链验证。证书格式能被 x509 读出来,也不代表 Nginx 发送的顺序正确。

第二步:用 CAfile 与 untrusted 重现 error 20

下面是最小的验证对照。根证书放在信任锚点,中间证书先故意不提供,再通过 -untrusted 提供。我的隔离实验在 OpenSSL 1.1.1k FIPS 上复现了原文:缺中间证书时为 error 20 at 0 depth lookup: unable to get local issuer certificate,补上中间证书后输出 leaf.pem: OK,临时目录随后删除。

bash 复制代码
# 只信任根,故意缺中间证书
openssl verify -CAfile root.pem leaf.pem

# 根是信任锚点,中间证书只用于构链
openssl verify -CAfile root.pem \
  -untrusted intermediate.pem leaf.pem

# 显示实际构造出的链(支持时使用)
openssl verify -show_chain -CAfile root.pem \
  -untrusted intermediate.pem leaf.pem

命令失败时请保留完整 stderr 和退出码。此次实验的缺中间证书退出码为 2;它说明当前材料不足,不直接说明线上一定没发中间证书。反过来,命令成功也只说明本地材料能构链。

第三步:区分 CAfile、CApath 与服务端 fullchain

如果团队使用目录信任库,CApath 里的证书文件名必须按主题哈希建立链接,不能把"目录里有一个 pem 文件"当作已加载。先用显式 -CAfile 排除目录索引问题,再决定是否执行 openssl rehash。不要直接把业务证书放进全局系统信任目录来"修好"验证。

bash 复制代码
# 先用显式 CAfile 做基线
openssl verify -CAfile root.pem \
  -untrusted intermediate.pem leaf.pem

# 独立目录测试 CApath 的索引
mkdir -p ca-path
cp root.pem ca-path/root.pem
openssl rehash ca-path
openssl verify -CApath ca-path \
  -untrusted intermediate.pem leaf.pem

对 Nginx 来说,证书文件应按"叶子在前、中间在后"拼接,根证书由客户端信任库提供。

nginx 复制代码
server {
    listen 443 ssl;
    server_name app.example.test;

    ssl_certificate     /etc/nginx/tls/fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/leaf.key;
}
# fullchain.pem 的顺序:leaf.pem,然后 intermediate.pem

第四步:从 HTTPS 实际发出的链反查

本地文件通过而浏览器仍报错,通常要看网络端点实际发出的证书。使用测试域名或明确的回环地址,不要对生产站点反复 reload。-servername 用来固定 SNI,-showcerts 用来观察服务端发送的证书集合;两者都不能把"服务端发了链"变成"客户端一定信任这条链"。

bash 复制代码
# 仅对测试端点读取服务端证书集合
openssl s_client -connect 127.0.0.1:8443 \
  -servername app.example.test -showcerts 

如果输出只有叶子证书,优先修复 fullchain;链完整仍失败时,检查 CAfile、CApath、容器信任库和代理层。不要把缓存和信任问题混成重新申请证书。

故障对照表:看到哪一层就修哪一层

现象 更可能的层 先做什么
error 20 at 0 depth 本地缺 issuer 或未提供 untrusted 核对 issuer/subject,补正确中间证书
本地 OK,线上仍 error 20 服务端未发送 fullchain,或终点不同 用 SNI + showcerts 读取实际端点
CApath 失败,CAfile 成功 目录哈希索引或权限 独立目录执行 rehash 后复测
证书能读,Nginx -t 失败 顺序、权限、密钥或链接库差异 先看完整错误,不把它归为 error 20
链完整仍不受信任 客户端根库、代理或策略 确认实际 CAfile/容器信任库与终点

验收清单:把"能读"变成"可交付"

  • 记录 CLI 的 ``openssl version``;本机实测为 1.1.1k FIPS。
  • 单独读取叶子、中间和根的 subject、issuer、SAN、有效期。
  • 用根 CAfile + 中间 untrusted 做正例,并保留 error 20 负例原文。
  • 按需验证 CApath 哈希,不把系统信任目录当临时修复点。
  • 检查 fullchain 顺序:叶子在前,中间在后,不把根硬塞进发送链。
  • 用固定 SNI 从测试端点读取实际证书集合,避免只看本地文件。
  • 记录 Nginx 构建库与 CLI 版本差异;本机分别为 1.1.1w 与 1.1.1k。
  • 确认没有把 CRL、EKU、私钥配对或域名 SAN 的错误误写成 error 20。

官方依据与边界

依据 OpenSSL verify 文档,``-CAfile``、``-CApath``与``-untrusted``分别承担信任库、目录库与构链材料角色;Nginx 文档要求主证书在前、中间证书随后。本文未把 1.1.1k 退出码推广到 OpenSSL 3.x,也未声称验证生产环境。

相关推荐
阿星_1 天前
pydio登陆不了分析问题
网络协议·http·https
Csvn1 天前
认识 Nginx
后端·nginx
陈希1232 天前
Nginx 从入门到能扛事:反向代理、负载均衡与 HTTPS 实战
nginx·https·负载均衡
木头左3 天前
【Nginx】内网服务源 IP 白名单实战:动态域名解析放行网段 + 端口接管零改造
运维·nginx·白名单·反向代理
skywalk81633 天前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd
没差c3 天前
winsw将jar、nginx等注册为Windows服务
windows·nginx·jar
FungLeo3 天前
成为全栈·基础补充·域名、DNS 与 HTTPS 证书
https·网络基础·网站部署·tls证书·成为全栈·域名 dns
Lsetea5 天前
Docker更新SSL证书后容器仍读旧文件:单文件挂载与目录挂载排查
运维·nginx·docker·https·ssl证书
2501_916008896 天前
iPhone 设备信息查看,用 KeyMob 看型号、序列号、电池健康和 UDID
android·ios·小程序·https·uni-app·iphone·webview
枫叶轻语7 天前
【Nginx 从入门到实战 09】前后端统一入口:部署页面与接口的完整配置
nginx·全栈