
问题背景同事把一份内部巡检脚本生成的“网站综合诊断报告”甩过来总分41/100红的黄的一大片问一句“先修哪个”。第一反应通常是去看报错最多的那一栏然后开始改 Nginx 配置。改完重跑分数没动报错换了个位置。原因很简单综合诊断报告里的指标不是并列关系而是依赖关系。DNS 不通TCP 检测会失败TCP 不通TLS 肯定握手失败TLS 不通过HTTP 状态码那一栏根本不会执行。指标之间是串联的不是并联的。所以读报告的正确姿势不是“找红点”而是从最底层开始逐层核对证据DNS → TCP → TLS → HTTP → 应用层。上游没过下游的 FAIL 一律先当成“连带失败”不要浪费时间去修。下面用一份可复现的示例报告把每一项指标拆开讲。环境与版本本文所有命令都在以下环境验证过读者可以照着敲组件版本OSUbuntu 22.04.3 LTSdig (BIND utils)9.18.18OpenSSL3.0.2curl7.81.0报告由自建脚本diag.sh生成目标是一个演示域名portal.example.comIP 段使用 RFC 5737 文档地址203.0.113.0/24不指向任何真实服务。./diag.sh portal.example.com443现象复现脚本输出的报告片段如下 网站综合诊断报告 target : https://portal.example.com/ generated : 2025-01-15 10:22:31 08:00 score : 41 / 100 [1] DNS A : 203.0.113.8, 203.0.113.9 CNAME : not present TTL : 60 resolve_ms : 12 NS 一致性 : 2/2 一致 status : PASS [2] TCP 443 connect: PASS (203.0.113.8, 18ms) 80 connect: PASS [3] TLS protocol : TLSv1.2 cipher : ECDHE-RSA-AES128-GCM-SHA256 notBefore : 2024-01-01 00:00:00 UTC notAfter : 2024-04-01 00:00:00 UTC SAN : DNS:www.example.com chain : 2 (leaf intermediate) hostname : MISMATCH expire : EXPIRED (已过期 289 天) status : FAIL [4] HTTP status : 未执行上游 TLS 未通过 [5] 应用层 status : 未执行重点看两处[3]里同时出现了两个失败原因——hostname: MISMATCH和expire: EXPIRED[4]、[5]明确写了“未执行”。这就是依赖链的体现。原因分析为什么 DNS 显示 PASS 也不能跳过[1] DNS这一栏检查的是“能不能拿到解析结果”而不是“解析结果对不对”。它正确的部分是A记录存在返回了 2 个地址TTL 60说明这条记录变更生效很快适合做灰度切换NS 一致性 2/2表示两个权威 NS 给出的结果一致没有出现解析分裂但它没查的东西同样关键解析出的 IP 是否属于预期 CDN 段、CAA 记录是否允许当前 CA 签发、是否存在陈旧的 CNAME 残留。DNS PASS 只代表链路通不代表语义对。TLS 两个失败原因是独立的hostname: MISMATCH和expire: EXPIRED是两条互不影响的失败路径在 OpenSSL 里分别对应不同的校验环节expire由X509_V_ERR_CERT_HAS_EXPIRED触发和证书内容无关只看系统时钟与notAfter的大小关系hostname由X509_check_host()完成比对的是 SAN 列表跟有效期无关任何一条不通过curl都会以退出码60SSL certificate problem中断HTTP 层根本不会发出请求。所以报告里[4]不执行是正确行为不是脚本漏检。阅读顺序原则把上面这些串起来报告的正确阅读顺序是从[1]往下读遇到第一个 FAIL 就停下来先确认它是不是根因如果 FAIL 出现在[3]及之后回看上游指标是否都 PASS判断当前是根因还是连带每条 FAIL 都要能找到一条可复现的命令和它的原始输出不能只看脚本给的颜色解决方案不要急着改服务端配置先用三条命令把证据坐实。第一步DNS 证据核对digshort time3tries1A portal.example.comdignoall answer authority portal.example.comdigshort NS example.comtime3 tries1是必须的。默认dig会重试 3 次、单次超时 5 秒遇到挂掉的权威服务器会卡住十几秒巡检脚本里非常容易假死。第二步TLS 证书链与 SANecho|openssl s_client\-connectportal.example.com:443\-servernameportal.example.com\2/dev/null|openssl x509-noout-dates-extsubjectAltName-servername不能省。多证书站点SNI 分流的情况下不带 SNI 拿到的可能是默认证书跟浏览器看到的完全不是一张。第三步主机名与有效期校验echo|openssl s_client-connectportal.example.com:443\-servernameportal.example.com2/dev/null\|openssl x509-noout-checkhostportal.example.com不匹配时输出Hostname portal.example.com does NOT match certificate退出码非 0可以直接用于 CI 断言。有效期的判断用-checkend更方便# 检查证书在 0 秒后是否会过期退出码非 0 表示已过期echo|openssl s_client-connectportal.example.com:443-servernameportal.example.com2/dev/null\|openssl x509-noout-checkend0核心代码把上面的检查串成一个分层脚本上游失败就标记下游为SKIP避免把连带失败误判成独立问题#!/usr/bin/env bashset-uopipefailHOST${1:?usage:$0 host[port]}PORT${2:-443}say(){printf%-12s %s\n$1$2;}# ---------- [1] DNS ----------echo[1] DNSA_REC$(digshort time3tries1A$HOST2/dev/null|grep-E^[0-9]\.||true)if[-z$A_REC];thensaystatusFAIL: 无 A 记录sayevidencedig short time3 tries1 A$HOSTexit1fisayA$(echo$A_REC|tr\n )# ---------- [2] TCP ----------echo[2] TCPif!timeout5bash-c/dev/tcp/$(echo$A_REC|head-1)/$PORT2/dev/null;thensaystatusFAIL:$PORT不可达saynote下游 TLS/HTTP 均为连带失败跳过exit2fisaystatusPASS# ---------- [3] TLS ----------echo[3] TLSTLS_OUT$(echo|timeout10openssl s_client-connect$HOST:$PORT\-servername$HOST2/dev/null)if[-z$TLS_OUT];thensaystatusFAIL: 握手未建立exit3fisaynotAfter$(echo$TLS_OUT|openssl x509-noout-enddate|cut-d-f2)echo$TLS_OUT|openssl x509-noout-checkend0/dev/null21\sayexpireOK||sayexpireEXPIREDifecho$TLS_OUT|openssl x509-noout-checkhost$HOST/dev/null21;thensayhostnameMATCHelsesayhostnameMISMATCH$HOST不在 SAN 中exit3fi# ---------- [4] HTTP ----------echo[4] HTTPsaycode$(timeout10curl-s-o/dev/null-w%{http_code}https://$HOST:$PORT/||echoFAIL)关键设计是两个exitDNS 或 TCP 失败就直接终止不把下游噪声写进报告TLS 失败有效期或主机名也终止因为此时 HTTP 层的结果没有诊断价值。验证结果修完证书续期 把portal.example.com加进 SAN后重跑脚本[1] DNS A 203.0.113.8 203.0.113.9 [2] TCP status PASS [3] TLS notAfter Apr 1 23:59:59 2026 GMT expire OK hostname MATCH [4] HTTP code 200对应报告里原来的[3] TLS → FAIL、[4] HTTP → 未执行全部转绿。原来的score: 41回升到96扣分项只剩 TTL 偏短。整个修复过程中服务端 Nginx 配置一行没改——一开始如果顺着“HTTP 那一栏没结果”去查应用方向就完全错了。常见坑把连带失败当独立问题。报告里[4]不执行、[5]不执行是设计如此不是漏检。看到“未执行”要往上找不要往下找。dig不带超时参数。默认超时 重试叠加遇到挂掉的 NS 会让整个巡检脚本假死。巡检场景下time3 tries1是底线。openssl s_client不带-servername。SNI 分流站点上会拿到默认证书校验结果和浏览器不一致容易误判为“证书没问题”。只看notAfter不看notBefore。时钟漂移或者 CA 误签发时notBefore在未来同样会导致握手失败报错文案不一样但结果一样。用 IP 去连 TLS。证书 SAN 里不会有裸 IP除非站点特意签了 IP SAN这样校验必然失败属于自己造的错。本地 DNS 缓存干扰复现。验证解析改动时用dig 8.8.8.8或1.1.1.1指定解析器别信本机 systemd-resolved 的缓存。总结综合诊断报告的价值不在于总分而在于它把一条链路拆成了可核对的分层证据。阅读顺序固定为DNS → TCP → TLS → HTTP → 应用层三个执行要点遇到第一个 FAIL 就停先判断是根因还是连带失败每一条 FAIL 都要能落到一条可复现的命令上脚本输出的颜色不作为证据上游失败时下游标记SKIP而不是FAIL避免误导出错误的修复方向按这个顺序读一份 41 分的报告看完你会知道该改的是证书而不是 Nginx。