返回资讯

incidents

证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘

线上证书明明在有效期内,桌面浏览器一切正常,却有一批 Android 客户端持续报证书错误。复盘发现根因是服务器漏配中间证书,部分客户端无法自动补链。记录症状差异、排查路径与预防措施。

证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘

凌晨两点,告警群里的报错很有迷惑性:「SSL handshake failed: certificate verify failed」。第一反应是看证书到期时间——还有 70 多天。桌面 Chrome、Firefox、curl 全都握手成功,证书链看起来绿得很健康。但客户端侧的错误日志在持续上涨,而且集中度很高:基本全是 Android,且以老版本居多。

这种「有人能连、有人不能连」的 TLS 故障,十有八九不是证书本身坏了,而是链没发全

症状的矛盾点

先整理一下当时看到的事实:

最后一条是关键线索:报错和「客户端新旧」不完全相关,而和「客户端本地有没有缓存过那张中间证书」相关。

根因:服务器只发了叶子证书

openssl s_client -connect example.com:443 -servername example.com 看握手过程,Certificate chain 一段里只有一张证书——站点自己的叶子证书。中间 CA 证书没配上去。

TLS 握手时,服务器有义务把从叶子到根(不含根)的整条链发给客户端。问题在于,现代桌面浏览器非常宽容:它们实现了 AIA fetching(根据证书里的 Authority Information Access 扩展去下载缺失的中间证书),或者干脆缓存了大量常见中间证书。所以漏配中间证书在浏览器上经常「看起来没问题」,一跑就是好几个月。

而老 Android、部分 Java 客户端、严格的 OpenSSL 配置不会帮你补链。链断了,验证就失败。证书没过期,但信任路径建不起来——对客户来说效果和证书坏了一样。

后来排查部署流程,发现这台机器的证书是从另一台服务器手动拷过来的,运维只拷了 cert.pem 和私钥,漏了 chain.pem / fullchain.pem。Nginx 配置里 ssl_certificate 指向的就是那张孤零零的叶子证书。这种故障不罕见,Let's Encrypt 用户尤其容易踩:很多人不知道 cert.pemfullchain.pem 的区别,随手挑了一个。

修复与验证

修复本身一分钟:把完整链配上去。Nginx 场景就是把 ssl_certificate 指向 fullchain.pem(叶子在前、中间证书在后的拼接文件),reload 后重新验证。

验证不要只信浏览器,它太宽容了。两个更诚实的办法:

  1. 在一台干净的机器上跑 openssl s_client,确认 Certificate chain 里能看到叶子加中间证书,且 Verify return code: 0 (ok)
  2. 用 SSL Labs 之类的在线检测跑一次,它会明确标出 "Chain issues: Incomplete",这个项在修复前是黄色的警告,很直观。

另外把「openssl 验证链完整性」加进了证书变更后的 checklist——浏览器绿灯不再作为验收标准。

预防:别再手拷证书文件

这次故障的直接诱因是人工拷贝证书文件、挑错了文件。只要流程里有「人手动搬运 PEM」,这种事迟早还会发生,区别只是下次断在哪个客户端上。

在 CertbotX 中实践:签发环节走 ACME 自动签发(包括 DNS-01 CNAME 委托场景),拿到的本来就是完整证书材料;部署环节直接推送到 Agent、EdgeOne 或 CDN,不经过人工拷贝这一步,fullchain 和私钥的配对关系由工具维护。每次部署都有记录可查,出问题能回溯是哪次变更把链弄断的。到期前的自动重新签发也走同一条路径,不会引入「这次换个人操作、换了个文件」的变量。

如果暂时还在手工管理,至少记住一条经验:部署证书永远用 fullchain,验收永远用 openssl 而不是浏览器。浏览器的补链能力是兜底,不是你配置正确的证明。

相关官方文档,可用于核验细节:

证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:an operations team tracing one broken link in a certificate delivery chain while the old service remains available
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:an operations team tracing one broken link in a certificate delivery chain while the old service remains available
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:DNS ownership verification using a delegated CNAME, shown as a precise handoff between two controlled zones
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:DNS ownership verification using a delegated CNAME, shown as a precise handoff between two controlled zones
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:an ACME request being validated at separate stations and turned into a new deployable artifact while private key material remains at its origin
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:an ACME request being validated at separate stations and turned into a new deployable artifact while private key material remains at its origin
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:a new certificate version moving from its source to several edge appliances while the previous working version remains ready for rollback
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:a new certificate version moving from its source to several edge appliances while the previous working version remains ready for rollback
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:an approaching certificate expiry being detected early, followed by a fresh request and a safe replacement before the live connection is interrupted
证书没过期,为什么只有部分 Android 用户报「证书不受信任」:一次中间证书缺失复盘:an approaching certificate expiry being detected early, followed by a fresh request and a safe replacement before the live connection is interrupted