续期证书遗漏中间证书,导致部分 TLS 客户端握手失败的复盘

某服务完成证书续期后,主流浏览器访问正常,但 Java、命令行工具和部分 API 调用持续报证书链验证错误。故障根因是部署文件未包含新的中间证书,且部分客户端缓存了旧链路。

本文目录

证书续期上线后,站点首页可以正常打开,但部分业务调用开始失败。监控中出现的错误并不完全一致:

  • Java 客户端报出 PKIX path building failed 或类似的证书路径构建错误;
  • curl、Python 或设备端 SDK 提示无法验证颁发者;
  • 个别浏览器访问正常,甚至在同一台电脑上的不同网络环境下结果不同;
  • TLS 握手失败发生在 HTTP 请求到达应用之前,因此应用日志中没有对应请求记录。

这类故障容易被误判为客户端证书库过旧、CDN 节点异常或网络劫持。实际问题通常出在服务端:新证书部署时只配置了叶子证书,没有同时提供完整的中间证书链。

故障经过

服务使用的服务器证书到期前完成了续期。运维人员从 CA 下载了新证书文件,并替换了 Web 服务器中的证书配置。配置重载成功,端口监听正常,浏览器访问域名也显示新证书已经生效。

上线后一段时间,依赖该域名的部分后端任务开始失败。最初的排查重点放在调用方,因为失败的调用方主要是 Java 服务和较旧的基础镜像;而办公网络中的浏览器访问一直正常。

进一步对比发现,成功和失败的客户端并非使用不同的 TLS 协议版本或密码套件。它们的关键差异在于证书链构建能力:

  • 一些浏览器曾经访问过同一 CA 体系下的站点,本地已缓存所需中间证书;
  • 某些操作系统或浏览器可以通过 AIA 扩展获取缺失的中间证书;
  • Java 运行时、容器镜像中的 CA 信任库以及许多严格的服务端客户端不会主动下载中间证书;
  • 服务端没有发送中间证书时,这些客户端无法将叶子证书连接到受信任根证书。

因此,“浏览器能打开”并不能证明服务端证书链部署正确。

根因:服务端发送的证书链不完整

TLS 服务端通常需要发送以下内容:

  1. 服务器证书,也称叶子证书;
  2. 签发该证书的一个或多个中间 CA 证书。

根证书一般不应由服务端发送。根证书应存在于客户端的受信任根证书库中。

本次问题中,Web 服务器配置引用的是单张服务器证书文件,而不是 CA 提供的完整链文件。旧证书使用的中间 CA 曾被较多客户端缓存,因此旧配置长期没有暴露问题。续期后,CA 使用了不同的中间证书,缺失链路立刻影响到没有缓存、不能联网补链或策略更严格的客户端。

另一个常见诱因是部署人员将 CA 下载包中的文件理解为可任选其一:

  • certificate.crt:通常仅包含服务器证书;
  • ca-bundle.crt、chain.pem 或类似文件:通常包含中间证书;
  • fullchain.pem:通常包含服务器证书和中间证书,常用于 Web 服务器配置;
  • root.crt:根证书,一般不应拼入服务端发送链。

不同 CA 和证书管理平台的文件命名并不统一。应以证书内容和部署文档为准,而不是只依据文件名判断。

排查过程

排查时,应从外部客户端实际收到的证书链开始,而不是只检查服务器本地磁盘上的证书文件。

可以使用 OpenSSL 查看服务端发送的证书:

openssl s_client -connect example.com:443 -servername example.com -showcerts

重点观察输出中的证书数量和顺序。正常情况下,输出应至少包含服务器证书和对应的中间证书。若只看到一张服务器证书,或者中间证书的颁发者与下一张证书的主体无法对应,优先检查服务端链配置。

随后可检查验证结果:

openssl s_client -connect example.com:443 -servername example.com -verify_return_error

如果输出显示无法获取本地颁发者证书、无法构建证书链或类似错误,说明客户端无法从服务端提供的信息建立可信路径。

还需要确认以下几项:

  • -servername 使用的域名是否与实际访问域名一致。缺少 SNI 时,可能拿到默认站点证书,导致结论偏离实际问题。
  • 负载均衡、CDN、反向代理和源站是否都已更新。不同入口可能返回不同证书链。
  • IPv4 和 IPv6 地址是否指向不同节点。双栈环境中常出现一侧完成更新、另一侧仍保留旧配置的情况。
  • 证书链文件的拼接顺序是否正确。通常应为服务器证书在前,随后是签发它的中间证书,再往上是更高层中间证书。
  • 配置变更后是否真正加载到运行进程。配置文件修改、热重载和实际监听进程使用的证书文件并不总是同步。

对于 Java 客户端,还可以保留完整异常栈。SunCertPathBuilderException 一类错误通常说明信任路径构建失败,但不能单独证明服务端缺少中间证书;也可能是客户端信任库过期、访问了错误域名,或遭遇了 TLS 流量代理。应结合服务端实际发送链判断。

修复

修复方式是为对应的 TLS 虚拟主机配置完整证书链,而不是把中间证书安装到每一个调用方。

部署前应确认链文件内容。可以分别查看证书主体和颁发者:

openssl x509 -in certificate.pem -noout -subject -issuer

对链中的每一张证书重复检查,确认前一张证书的 issuer 与后一张证书的 subject 能够衔接。服务器证书应放在最前面,中间证书按签发关系依次排列。

更新配置并重新加载服务后,再次从外部执行 openssl s_client -showcerts。确认实际返回链符合预期后,还应使用受影响类型的真实客户端验证,例如使用对应的 Java 运行时、容器基础镜像或设备 SDK 发起连接。

修复后,原先依赖浏览器缓存而“看似正常”的现象不再是判断依据。验证重点应放在此前失败的客户端上。

为什么故障只影响部分客户端

证书链验证并不总是完全一致。客户端的行为会受到以下因素影响:

  • 本地受信任根证书库的版本;
  • 是否缓存过中间证书;
  • 是否允许通过 AIA 地址下载中间证书;
  • 网络出口是否允许访问 CA 的证书分发地址;
  • TLS 库和运行时对证书路径构建的实现差异;
  • 企业代理、终端安全软件或私有根证书策略。

因此,某个浏览器显示“连接安全”,只说明该浏览器在当时能够完成验证。它不能代替服务端证书链检查,也不能覆盖后端服务、批处理任务、移动端和物联网设备等客户端。

预防措施

证书续期不应只检查有效期和域名匹配,还应把完整链作为发布物的一部分管理。

建议在证书变更流程中固定以下检查:

  • 明确记录每个入口实际使用的证书文件和链文件,包括 CDN、负载均衡、网关、反向代理和源站;
  • 将“外部握手检查”纳入发布后的验证项,检查 SNI、证书主题、有效期和服务端发送的完整链;
  • 在自动化检查中使用干净的运行环境,避免本机缓存的中间证书掩盖问题;
  • 对 IPv4、IPv6、不同地域入口和不同负载均衡节点分别抽查;
  • 保留续期前后的证书链信息,便于识别中间 CA 变更;
  • 不要通过向所有客户端导入中间证书来掩盖服务端配置错误。除非是私有 PKI 场景,否则中间证书应由 TLS 服务端提供。

证书链问题的核心不在于“证书是否有效”,而在于客户端能否仅凭服务端发送的链和本地信任库,构建出一条完整、可信且用途正确的验证路径。把这项检查放在续期发布流程中,可以避免大量只影响部分客户端的隐蔽握手故障。

← 返回资讯中心