
凌晨被叫起来一次 DNS 缓存投毒导致全站 HTTPS 握手失败的排查实录凌晨 2:17手机狂震。打开监控一看全站 HTTPS 握手失败率飙到 87%。用户刷不出页面支付链路全断。这个锅_DNS 缓存投毒_背了。事情是怎么发生的两周前我们做了一次灰度发布把部分流量切到了新 CDN 供应商。新供应商的 DNS 配置里有个坑TTL 设了 24 小时但他们在全球不同节点的 A 记录返回了不一致的 IP。更坑的是我们的 Nginx 反代层开启了resolver缓存默认缓存时间是 5 分钟。当某个节点的 IP 被运营商 DNS 投毒后Nginx 的 resolver 缓存把这个脏 IP 牢牢锁了 5 分钟。5 分钟在凌晨是 eternity。排查时间线真实记录02:17告警触发HTTPS 握手失败率 87%错误码SSL_ERROR_SYSCALL。02:21我登上故障机器先抓了个包tcpdump-ianyhost故障IP-w/tmp/dns_fail.pcapWireshark 一看TLS ClientHello 发出去服务端直接 RST。根本不是我们的证书链。02:28怀疑证书问题但证书没过期。排查证书路径openssl s_client-connect故障IP:443-servernameourdomain.com返回的证书是*.somecdn.net根本不是我们的通配符。IP 被劫持了。02:35确认 DNS 解析异常# 不同 DNS 服务器返回不同结果dig8.8.8.8 ourdomain.com Adig223.5.5.5 ourdomain.com Adig114.114.114.114 ourdomain.com A三个结果两个是脏 IP。运营商 DNS 缓存被投毒了。02:42紧急止血切回旧 CDNDNS 切换5 分钟生效清空 Nginx resolver 缓存nginx -s reload临时把被污染域名写进/etc/hosts指向已知干净的 IP02:50握手失败率降到 3%业务恢复。03:10开始根因定位。发现新 CDN 的 DNS 配置问题不同 Anycast 节点返回不同 A 记录且 TTL 不一致。运营商 DNS 在递归解析时某些区域拿到了被投毒的结果。根因分析DNS 缓存投毒的三层防线是怎么破的第一层DNSSEC我们没有开。开了 DNSSEC 的域名运营商 DNS 在返回结果时会校验 RRSIG 签名投毒难度指数级上升。但 DNSSEC 在国内普及率不到 5%很多解析商甚至不支持。第二层HTTPS 证书校验浏览器在 TLS 握手阶段会校验证书域名。如果 IP 被劫持到钓鱼站点证书域名对不上浏览器会拦截。但我们的问题更隐蔽劫持到的 IP 也是 CDN 节点证书是*.somecdn.net虽然域名不匹配但某些老版本客户端或内部 API 调用没有严格校验 SNI导致握手失败而不是安全拦截。第三层应用层健康检查我们的 Nginx upstream 健康检查是 30 秒一次 TCP 连通性探测。TCP 能通但 TLS 握手失败健康检查没报异常。这是个盲区。修复方案四层加固1. DNS 层DNSSEC 多解析商# 在域名注册商开启 DNSSECdnssec-keygen-aRSASHA256-b2048-fKSK ourdomain.com dnssec-keygen-aRSASHA256-b1024-fZSK ourdomain.com# 上传 DS 记录到注册商同时部署两个独立解析商主 备任何一方异常可快速切换。2. Nginx 层缩短 resolver 缓存 SNI 严格校验resolver 8.8.8.8 223.5.5.5 valid10s; # 默认 5 分钟改为 10 秒牺牲一点点性能换实时性 # 强制 SNI 校验不匹配直接 444 server { listen 443 ssl; server_name ourdomain.com; if ($host ! $server_name) { return 444; } }3. 监控层HTTPS 握手成功率 证书域名校验# Prometheus 告警规则-alert:HTTPSHandshakeFailureRateexpr:|( sum(rate(nginx_http_requests_total{status~4..|5..}[1m])) / sum(rate(nginx_http_requests_total[1m])) ) 0.05for:30slabels:severity:criticalannotations:summary:HTTPS 握手失败率超过 5%再加一个主动探测#!/bin/bash# check_ssl.shforhostinourdomain.com api.ourdomain.com;docert_name$(echo|openssl s_client-connect$(dig short $host|head-1):443-servername$host2/dev/null|openssl x509-noout-subject2/dev/null|grep-oPCN\K[^,])if[$cert_name!*.ourdomain.com];thenechoALERT:$hostcert mismatch:$cert_namefidone4. 架构层DNS-over-HTTPSDoH兜底在移动端和内部服务集成 DoH 客户端绕过运营商 DNS// Go 示例transport:http.Transport{DialContext:func(ctx context.Context,network,addrstring)(net.Conn,error){// 使用 DoH 解析returndohResolver.DialContext(ctx,network,addr)},}踩坑记录Nginx resolver 缓存默认 5 分钟很多人不知道resolver指令的valid参数默认缓存时间是由 DNS 响应的 TTL 决定。如果上游 DNS 返回 24 小时 TTLNginx 也会缓存 24 小时。dig 命令不显示 DNS 缓存层级dig trace才能看到完整的递归解析路径普通dig只返回最终结果看不出是哪层缓存出了问题。SSL 证书不匹配 ≠ 浏览器拦截某些内部 API 客户端比如老版本 curl、Java 8 默认不校验 SNI只会握手失败日志里很难直接看出是 DNS 问题。DNS 切换后不要急着走A 记录 TTL 是 24 小时切换解析后全球 DNS 缓存刷新需要的时间 TTL 传播延迟。我们第一次切回旧 CDN 后5 分钟就以为恢复了结果欧洲节点 20 分钟后还在连脏 IP。写在最后DNS 是互联网的「基石」但也是最容易被忽视的环节。这次事故教会我三件事DNS 配置要审计不要只盯着代码和服务器DNS 解析链、TTL 设置、多供应商策略都是生产环境的一部分。监控要覆盖握手层TCP 连通不代表服务可用TLS 握手、证书校验、HTTP 响应内容都应该在监控里。止血要快要全清空 Nginx 缓存、切解析、写 hosts三板斧一起上不要一步一步试。DNS 投毒不常见但一旦发生就是全站级别。希望这篇记录能帮你在凌晨 2 点少熬一个小时。附排查 Checklistdig trace检查完整解析路径openssl s_client验证证书链tcpdump抓包确认 RST 来源nginx -s reload清空 resolver 缓存检查 DNSSEC 状态dig dnssec ourdomain.com DNSKEY