一、现象与影响面:红叉背后有四种不同的病
症状:浏览器地址栏证书红叉,或者用户反馈”网站不安全”,但你自己打开却好像又能过。证书问题的影响面比其他故障更致命——用户看到的不是加载慢,而是明确的”别进这个站”,转化直接归零。
动手之前先记住一个关键事实:不同客户端对同一份坏证书的反应不一样。桌面浏览器可能正常、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 根证书过期影响旧客户端为已发生事实。各错误码的精确文案以当前版本实测为准。