浏览器证书红叉:证书链不完整的排查

一、现象与影响面:红叉背后有四种不同的病

症状:浏览器地址栏证书红叉,或者用户反馈”网站不安全”,但你自己打开却好像又能过。证书问题的影响面比其他故障更致命——用户看到的不是加载慢,而是明确的”别进这个站”,转化直接归零。

动手之前先记住一个关键事实:不同客户端对同一份坏证书的反应不一样。桌面浏览器可能正常、curl 报错、Java 程序直接断连,这是证书故障独有的”薛定谔”现象,也正是定位的第一条线索。先别急着重新签发证书——证书没坏的几率远比想象中大,坏的是”递链条”的方式。

排查主线共四层:中间证书缺失 → 系统时间 → 域名不匹配 → SNI 选错证书。顺序不是随意的:链不完整占了日常案例的大头,时间与域名是低频但秒杀级的低级失误,SNI 藏在共享 IP 部署的深处。每层确认正常就往下走。

二、第零步:先读报错关键字,别上来就开 openssl

浏览器红叉页上有一串错误码,它已经帮你把方向缩小到某一层了。以下命令与页面在出问题的客户端机器上看(用户的浏览器、你的开发机),不是服务器:

报错关键字(Chrome 系) 指向的层 语义
NET::ERR_CERT_AUTHORITY_INVALID 第一节:证书链 证书没法追溯到受信任的根,典型的中间证书缺失
NET::ERR_CERT_DATE_INVALID 第二节:系统时间 证书过期,或验证方时钟不对
NET::ERR_CERT_COMMON_NAME_INVALID 第三节:域名匹配 证书里没有当前访问的域名
证书显示的是另一个域名的证书 第四节:SNI 服务器按默认站点把别的证书发过来了

curl 的报错同样值得抄下来,比如 curl: (60) SSL certificate problem: unable to get local issuer certificate——”找不到签发者”就是链不完整的直译。关键字指向哪层,就从哪节开始查;两三层同时坏的案例也存在,逐层排除即可。

三、第一层:证书链,中间证书缺了没

HTTPS 证书不是孤立的:浏览器信任根证书(内置在系统里),根签发中间证书,中间证书再签你的站点证书。服务器必须在握手时把”站点证书 + 中间证书”一起递出去,根证书不用递(验证方自己有)。少递一张中间证书,链条接不上,验证就失败。

看服务器实际递了什么,用 openssl 的 s_client 子命令。这条命令在任意机器上都能跑(本机、跳板机、你的开发机),它模拟一次真实握手:

# 任意一台机器上;-servername 指定 SNI,第 2 节讲它为什么不能省
openssl s_client -connect blog.example.com:443 -servername blog.example.com -showcerts

1. 输出里盯两处:
1. Certificate chain 0 s:CN = blog.example.com      ← 站点证书
1.   i:CN = Let's Encrypt R3                        ← 它的签发者
1. Certificate chain 1 s:CN = Let's Encrypt R3      ← 中间证书有没有跟着出现?
1. Verify return code: 21 (unable to verify the first certificate)  ← 链断了
1. Verify return code: 0 (ok)                       ← 链完整

-showcerts 会列出整条链。数一数 chain 条目:只有一条(站点证书自己)就是缺中间证书;有两条以上再看顺序对不对。Verify return code 是最省事的判断:21 或 19 = 链有问题;0 = 这层没病,往下走。

确认是缺链之后,回到服务器修配置。高频翻车点在 Nginx 的 ssl_certificate——它必须指向包含中间证书的 fullchain 文件,而不是只有站点证书的 cert 文件:

# 在服务器上看配置指向的文件里到底有几张证书
grep ssl_certificate /etc/nginx/sites-enabled/*
openssl x509 -in /etc/letsencrypt/live/blog.example.com/fullchain.pem -noout -subject
1. 别只看一张;数 PEM 数量更直接:
grep -c "BEGIN CERTIFICATE" /etc/letsencrypt/live/blog.example.com/fullchain.pem
1. 输出 2 以上才正常(站点+中间链);cert.pem 只有 1,配错成它就是缺链

补一句为什么”浏览器却好好的”:桌面浏览器在验证失败时,会尝试通过证书里的 AIA 扩展(Authority Information Access)在线补下缺失的中间证书,补到了照样亮绿锁。curl、Java、各路移动端不做这件事——于是出现”用户说网站坏了,你打开却正常”的经典错位。反模式一:只用自己桌面浏览器验证证书就宣布修好了。测试必须覆盖 curl 或 openssl,最好在没访问过该站的一台干净机器上做。

四、第二层:系统时间,谁的钟错了

证书是带有效期的法律文书,Not Before 到 Not After 之间才算数。验证用的是验证方的本地时钟——服务器时间错了会导致握手失败,客户端时间错了会让明明正常的站点报红叉。

# 服务器上
date                                  # 肉眼对照真实时间,误差超过几分钟就有问题
timedatectl                           # 看 NTP synchronized: yes/no

1. 客户端机器上(用户报障时先让用户看右下角)
date                                  # 客户端时钟倒退几天,一切证书都会"未生效"

两边都对过之后,把证书自身的有效期也核一遍:

# 任意机器上,看远端证书的起止时间
openssl s_client -connect blog.example.com:443 -servername blog.example.com 2>/dev/null \
  | openssl x509 -noout -dates
1. notBefore=Jun  1 08:00:00 2024 GMT
1. notAfter=Aug 30 08:00:00 2024 GMT    ← 现在的时间必须落在这个区间里

时钟漂移的本质是没挂 NTP 或 NTP 源不通,修法是把 timedatectl 的同步打开并保证 UDP 123 出网,而不是手拨时间。这层时间都正常、报错仍是 DATE_INVALID,说明证书真过期了,续期流程属于另一类故障,往下看域名层。

五、第三层:域名匹配,证书里有这个名字吗

证书只为写在 SAN(Subject Alternative Name,主体备用名称)里的域名背书。访问 a.example.com 却拿到只含 b.example.com 的证书,红叉毫无悬念。两个常被忽略的规则:通配符 *.example.com 只匹配一层子域,a.b.example.com 不在内;裸域 example.com 也不在 *.example.com 的覆盖范围内。

# 任意机器上,列远端证书覆盖的所有域名
openssl s_client -connect blog.example.com:443 -servername blog.example.com 2>/dev/null \
  | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
1. DNS:blog.example.com, DNS:example.com    ← 逐条对照你实际访问的域名

这一层的分诊很简单:域名在 SAN 列表里,这层没病;不在,要么重签证书把域名补进去,要么你访问的域名本身就配错了。反模式二:发现不匹配就无条件重新签发一张更大的证书——先看是不是用户访问了错的域名(老书签、硬编码的旧地址),签证书是最贵的修法。

六、第四层:SNI,共享 IP 上拿错人的证书

一台服务器一个 IP 上挂几十个 HTTPS 站点是常态。TLS 握手时服务器靠客户端报上来的 SNI(Server Name Indication,服务器名称指示)判断”你要的是哪个站的证书”,再递出对应的链条。SNI 缺失或不对,服务器只能回默认站点的证书——于是在第四层,你看到的是”证书是别人的域名的”。

# 任意机器上,对比带与不带 SNI 时拿到的证书
openssl s_client -connect 203.0.113.10:443 2>/dev/null | openssl x509 -noout -subject
1. subject = CN = default.example.com       ← 不带 -servername,拿到默认站点证书

openssl s_client -connect 203.0.113.10:443 -servername blog.example.com 2>/dev/null \
  | openssl x509 -noout -subject
1. subject = CN = blog.example.com          ← 带上 SNI,证书就对了

两次结果不同,说明服务器端证书与 SNI 分派本身是正常的,问题出在客户端没发 SNI:极老的客户端、某些内网扫描器、硬编码 IP 访问的脚本都会踩这里。两次结果相同且都是错的证书,查 Nginx 的默认 server 块——第一个加载的 listen 443 ssl 块就是默认站点,把谁的证书放在那里要看清楚。

四层都过一遍,红叉的来源基本跑不出:链缺一张、钟差几分钟、名字差一个字、SNI 差一个握手字段。

七、根因清单:按出现频率排序

根因 典型报错 一行验证
ssl_certificate 配成 cert.pem(缺中间证书) AUTHORITY_INVALID / curl 60 s_client -showcerts 数 chain 条目
客户端时钟或服务器时钟漂移 DATE_INVALID,证书明明没过期 两端各跑一次 date 对照
访问域名不在证书 SAN 里 COMMON_NAME_INVALID openssl x509 -noout -text 看 SAN
通配符只覆盖一层,深子域裸奔 COMMON_NAME_INVALID 对照 SAN 与完整域名的层数
客户端不发 SNI,拿到默认站点证书 证书显示别的域名 带/不带 -servername 各测一次
Nginx 默认 server 块放错证书 同一 IP 下部分站全错 不带 SNI 看返回的是谁的证书
老客户端信任库过期(如 2021 年 DST Root CA X3 过期波及老系统) 仅老旧设备/旧 Java 报错 换新系统同版本 curl 对照

八、预防

措施 做什么 边界
上线前干净机自检 把 openssl s_client -showcerts 与 curl -v https://… 固化成发布清单,在没缓存过链的机器上跑 桌面浏览器的 AIA 补链会掩盖缺链,不能作为唯一验证手段
到期监控 用 openssl x509 -noout -enddate 写入巡检,剩 14 天告警 自动续期也要监控,续期本身失败是另一类故障
配置规范 ssl_certificate 一律指向 fullchain;新域名先核 SAN 再发版 cert.pem/fullchain.pem 的文件名习惯随签发工具而变,以工具文档为准

这套走法收拢成一句:先抄报错关键字定层,再用 openssl s_client 复现握手,两端时钟与 SAN 名单逐项核对,最后才轮到怀疑 SNI——见到红叉就重签证书,是用最贵的动作赌最便宜的问题。

延展阅读:本文用到的命令行基本功都出自《Linux 常用命令》;开发机上自建一条与线上一致的测试链路,见《开发环境》。

版本核实(以官方文档为准):桌面浏览器通过 AIA 扩展在线补取缺失中间证书、curl/Java/移动端通常不做此行为,属长期稳定表现,具体策略以各浏览器厂商文档为准;openssl s_client 的 -servername/-showcerts、curl 的证书校验错误码(60 系 unable to get local issuer certificate)为长期稳定接口;Chrome 系错误码 NET::ERR_CERT_* 命名长期沿用,文案随版本演进;2021 年 DST Root CA X3 根证书过期影响旧客户端为已发生事实。各错误码的精确文案以当前版本实测为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇