← 全部笔记

HTTPS 打不开时,从头把证书链查一遍

2026-10-24 网络与协议 约 8 分钟

第一次给站点挂证书的时候,我遇到的现象很怪:浏览器说证书无效,但 curl 没报错, 用手机打开又正常。折腾了一下午才明白——不是证书签错了,是服务器只发了叶子证书,没发中间证书。 有些客户端会自己去补,有些不会。

这篇按「链 → 名字 → 时间 → 协议」四层来写,基本上 HTTPS 的报错都能落到其中一层。

一、证书链到底有几张证书

你从 CA 拿到的东西通常是三样(有的打包成一个 fullchain.pem):

  • 叶子证书(leaf):签给你域名的这张,Subject 是 CN = 你的域名。
  • 中间证书(intermediate):签发叶子证书的那张,由根证书签发。
  • 根证书(root):自签名的最高一层,不随服务器下发,预装在操作系统和浏览器的信任库里。

握手时服务器必须把叶子 + 中间一起发过去,客户端再用本地信任库把链补全到根。 只发叶子,客户端手上又没有那个中间证书,校验就断在半路——这就是最常见的 NET::ERR_CERT_AUTHORITY_INVALID。

判断方法:openssl s_client -connect 域名:443 -servername 域名 -showcerts 的输出里,如果只有一段 BEGIN CERTIFICATE,而证书的 Issuer 又不是某个根 CA 的名字,基本可以确定缺中间证书。

二、报错对号入座

现象 通常是
AUTHORITY_INVALID / unable to get local issuer 缺中间证书,或客户端信任库里没有对应根
COMMON_NAME_INVALID / hostname mismatch 证书的 SAN 里没有你访问的那个域名
DATE_INVALID / certificate has expired 证书过期,或本机系统时间不对
握手直接断开、没有证书报错 协议版本或加密套件不匹配,不是证书问题

三、名字这一关:CN 已经不作数了

老证书看 CN,现在的浏览器只认 subjectAltName(SAN)扩展里的域名列表。 也就是说,一张只写了 CN = example.com 而没有 SAN 的证书, 在现代浏览器里是无效的——这是很多人用旧脚本自签证书时踩的坑。

# 看 SAN 列表
openssl x509 -in cert.pem -noout -ext subjectAltName

# 输出形如:
# X509v3 Subject Alternative Name:
#     DNS:example.com, DNS:www.example.com

另外注意通配符只覆盖一级:*.example.com 匹配 a.example.com, 但不匹配 a.b.example.com,也不匹配裸域 example.com。 所以正规做法是签一张同时包含裸域和通配符的证书。

四、时间这一关

# 有效期、签发者、使用者
openssl x509 -in cert.pem -noout -dates -subject -issuer

# 直接看线上服务实际在用的那张
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

如果证书没过期但浏览器说过期,先看客户端时间。 服务器上跑着定时任务的机器时钟漂移是很常见的事,尤其虚拟机休眠恢复之后。 校时用 timedatectl(systemd 系统)看 System clock synchronized 是否为 yes。

五、把验证过程一次跑完

排查时我习惯直接用 openssl s_client 看完整输出,比浏览器的一句话报错信息量大得多:

openssl s_client -connect example.com:443 \
  -servername example.com \
  -CAfile /etc/ssl/certs/ca-certificates.crt \
  -verify_return_error < /dev/null
  • -servername 必须带。一个 IP 上挂多个站点时,不带 SNI 会拿到默认站点的证书,看到的就不是你要查的那张。
  • 末尾 < /dev/null 让命令读完就退出,不然它会停在交互式输入等你说 QUIT。
  • 输出里 Verify return code: 0 (ok) 才算链完整;21 (unable to verify the first certificate) 就是缺中间证书。

六、协议版本:1.3 比 1.2 少一个来回

TLS 1.2 的完整握手要 2 个 RTT 才能开始传数据,TLS 1.3 压到 1 个 RTT (会话复用时甚至可以 0-RTT)。对高延迟线路来说这一步的差别比想象中大: 跨境访问时一个 RTT 常常就是 150 毫秒起。

# 看协商结果和实际用的协议版本
openssl s_client -connect example.com:443 -servername example.com < /dev/null \
  | grep -E "Protocol|Cipher"

nginx 里只开 1.2 和 1.3 就够了,1.0/1.1 早已被主流浏览器弃用:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;   # 1.3 时代交给客户端选更合适

七、续期与两个容易忘的开关

Let's Encrypt 的证书有效期是 90 天,官方建议在剩余 30 天内续。 certbot 装好后系统会带一个定时器自动跑,但自动续期失败往往是静默的,所以要定期看一眼:

# 干跑一次,验证续期流程能不能走通(不会真的签发)
certbot renew --dry-run

# 看系统的续期定时器
systemctl list-timers | grep -i certbot

两个和证书配套、但经常被漏掉的配置:

  • OCSP stapling:由服务器代为获取证书吊销状态并随握手一起发给客户端, 省掉客户端自己去查的那一次请求。nginx 里是 ssl_stapling on;, 同时要配 ssl_trusted_certificate 指向完整链,否则不生效。
  • HSTS:Strict-Transport-Security: max-age=31536000 告诉浏览器以后只用 HTTPS 访问。 上线前把 max-age 设小一点试,确认全站(含子域)都能走 HTTPS 再加长—— 这个头一旦下发,浏览器会记住,回退很麻烦。

一句话总结:先分清是「链不完整」「名字不对」还是「时间不对」,三种原因三条查法,别一上来就重签。