DNSSEC 实战:给域名加上防劫持的信任链,从 KSK/ZSK 生成到 DS 发布与故障排查

你的域名,可能正在被别人"无声地"劫持

先说一个让很多人不舒服的事实:DNS 协议设计于 1983 年,它从来没有考虑过安全问题。当你的用户在浏览器里敲下你的域名,操作系统向递归 DNS 服务器发起查询,递归服务器再去问根、问顶级域、问你的权威 DNS,最后把 IP 返回。整个链条上,没有任何一步在验证"这个回答到底是不是真的来自域名主人"

这意味着什么?意味着任何一个能在这条链路上插一脚的人,都可以伪造一个回答,告诉用户"zz1984.com 的 IP 是 1.2.3.4",而用户的浏览器会毫不犹豫地连过去——因为 DNS 的回答没有签名,无法分辨真伪。这个攻击叫 DNS 缓存投毒(Cache Poisoning),历史上著名的 Kaminsky 攻击就是靠它,在几秒钟内就能污染一台递归服务器上任意域名的缓存。

劫持的后果不只是"网站打不开"。对站长来说,最可怕的是:你的域名可以指向一个长得和你一模一样的钓鱼站,而用户看不出来;或者你的邮箱 MX 记录被改,所有发往你域名的邮件都进了攻击者的服务器——包括那些"点击重置密码"的邮件。你的 SSL 证书对此毫无帮助,因为攻击者会用他自己的证书去申请一个合法签发(他有域名控制权,DV 证书几分钟就能拿到)。

DNSSEC(DNS Security Extensions)就是为了解决这个问题而生的。它给 DNS 记录加上密码学签名,让递归服务器可以验证"这个回答确实由域名的权威服务器签发,且内容没有被篡改"。验证失败的响应会被直接丢弃,攻击者哪怕污染了缓存也没用——因为伪造的回答签不出来

DNSSEC 的工作原理:一条信任链,从根一直签到你

DNSSEC 最核心的设计思想是信任链(Chain of Trust)。它不依赖任何一个中心机构的"我说了算",而是靠层层签名,让验证者可以从根一直验证到你的记录。

链条是这样的:

根区(.)
  │  用根区的 KSK 签名,公钥是公开的、被所有解析器内置(信任锚)
  │  在根区里放置了 .com 的 DS 记录(.com 的公钥哈希)
  ▼
.com 顶级域
  │  用 .com 的 ZSK 签名
  │  在 .com 里放置了 zz1984.com 的 DS 记录(你的公钥哈希)
  ▼
zz1984.com 权威区(你的域名)
  │  用你的 ZSK 签名所有记录
  └─ 在区里放置 DNSKEY 记录(你的公钥)

验证的流程是自下而上发起、自上而下验证:递归服务器拿到你的 A 记录回答时,会要求你同时提供 RRSIG(签名);它用你区里的 DNSKEY(公钥)验签;然后它要确认这个 DNSKEY 是真的——于是去问 .com 要你的 DS 记录,用自己的公钥验 DS 的签名;再确认 .com 的公钥是真的——去问根区要 .com 的 DS……一直验到根,而根的 KSK 公钥是解析器软件里内置的(信任锚)。

链条上任何一环断掉,验证就失败。这也是 DNSSEC 最常见的两个故障原因:DS 记录和 DNSKEY 不匹配(漏配或换了密钥忘了更新 DS),以及签名过期(忘了续期)。

顺便解释四个会反复遇到的缩写:

  • KSK(Key Signing Key):密钥签名密钥。只用来签名 DNSKEY 记录,也就是给其他公钥"背书"。它变动很少,是信任链的锚点,所以它的公钥哈希会被登记为 DS 记录放到上级域。
  • ZSK(Zone Signing Key):区域签名密钥。用来签名区里所有普通记录(A、AAAA、MX、CNAME、TXT……)。它变动相对频繁,因为签名有效期短。
  • RRSIG:签名记录类型。每一条被签名的记录旁边都有一条对应的 RRSIG,里面含签名值和有效期。
  • DS(Delegation Signer):委派签名者记录。它存在于上级域,内容是下级域 KSK 的哈希。这是把上下两级串起来的唯一纽带,也是 DNSSEC 配置里最容易出错的地方。

启用前的准备:先摸清你的 DNS 在哪

DNSSEC 的启用方式完全取决于你的 DNS 托管在哪里,因为签名这件事本质上是权威 DNS 服务器干的活。分三种情况:

情况一:域名商自带的 DNS(最常见)

Cloudflare、阿里云 DNS、DNSPod、华为云 DNS、Namecheap 等都已经支持一键开启 DNSSEC。你只需要在控制台点一下"启用 DNSSEC",它会生成 DNSKEY 和 DS 记录,你把 DS 记录填到域名注册商(注意:是注册商,不是 DNS 服务商)那边,就完成了。这是最省事的路径,强烈推荐。

情况二:自建权威 DNS(BIND / PowerDNS / Knot)

如果你像我一样把 DNS 抓在自己手里,那就得自己管密钥。签名的活儿由 dnssec-keygen 生成密钥、dnssec-signzone 签名、BIND 加载签名后的区文件。这套流程复杂且极易出错(密钥轮换、签名有效期、区文件重签的自动化),下面会专门讲。

情况三:CDN 或第三方提供 DNS

部分 CDN 商不开放 DNSSEC。这时候要么接受没有 DNSSEC,要么把 DNS 迁到支持的服务商(很多免费服务商支持),二者可以并存——DNS 服务和 CDN 服务本来就是分离的,你可以用 A 或 CNAME 指向 CDN。

在动手之前,先确认当前状态:

# 查看你域名当前是否有 DNSKEY 和 DS
dig +dnssec DNSKEY zz1984.com @1.1.1.1
dig DS zz1984.com @1.1.1.1

# 用 Google/Cloudflare 的判断工具(最直观)
# https://dnssec-debugger.verisignlabs.com/  输入你的域名
# https://dnsviz.net/  会画出完整的信任链图

# 命令行验证:如果 +dnssec 查询返回结果里带 "ad" flag,说明验证通过
dig +dnssec www.zz1984.com @1.1.1.1 | grep -E "flags:"

最后一条命令里的 ad 标志位(Authenticated Data)是关键:它表示解析器已经完成了 DNSSEC 验证且通过。如果查得到结果但没有 ad,说明这个域名没有启用 DNSSEC(或者解析器没开启验证)。

实战:给自建 BIND 启用 DNSSEC(完整流程)

这套流程我配过好几次,坑非常多,逐步讲清楚。假设你的域名是 example.com,BIND 的区文件目录是 /var/named/

第一步:生成 KSK 和 ZSK

cd /var/named

# 生成 KSK(只签 DNSKEY,用 RSA 2048 或 ECDSA P-256)
dnssec-keygen -a ECDSAP256SHA256 -f KSK -n ZONE example.com
# 输出:Kexample.com.+013+12345

# 生成 ZSK(签所有记录)
dnssec-keygen -a ECDSAP256SHA256 -n ZONE example.com
# 输出:Kexample.com.+013+67890

# 查看生成的文件(每个密钥有 .key 公钥 和 .private 私钥两个文件)
ls -l Kexample.com.*

关于算法选择:ECDSAP256SHA256(算法 13)是当前的最优解——签名短(对 DNS 报文大小友好)、验证快、性能好。RSA 2048(算法 8)兼容性天花板最高但报文大,容易触发 TCP 回退。Ed25519(算法 15)很优雅但部分老解析器不支持。中小站点选 ECDSAP256SHA256 即可。

第二步:把公钥写进区文件

# 把两个 .key 文件里的公钥追加到区文件末尾
cat Kexample.com.+013+12345.key >> example.com.zone
cat Kexample.com.+013+67890.key >> example.com.zone

# 重要:把区的 SOA 行加上这些配置(在 $ORIGIN 附近)
# 对于 BIND 9.16+,可以用 dnssec-policy 自动管理(推荐)
# 见下面的 named.conf 配置

第三步:配置 named.conf 启用自动签名

BIND 9.16 之后引入了 dnssec-policy,可以自动处理签名、重签和密钥轮换——这是最省心的方式,强烈建议用这个而不是手动 dnssec-signzone

// /etc/named.conf
dnssec-policy default {
    keys {
        ksk lifetime unlimited algorithm ecdsap256sha256;
        zsk lifetime 90d algorithm ecdsap256sha256;
    };
    // 签名有效期相关
    signatures-refresh 5d;
    signatures-validity 14d;
    signatures-validity-dnskey 14d;
};

zone "example.com" {
    type master;
    file "/var/named/example.com.zone";
    // 关键:让 BIND 自动签名并维护密钥
    dnssec-policy default;
    inline-signing yes;
    // 自动重签,不需要手动跑 dnssec-signzone
    auto-dnssec maintain;
};

改完配置,named-checkconf 检查语法,然后 rndc reload。BIND 会在后台完成签名,并把签名后的内容维护在 .signed 文件里。日志会显示 zone example.com/IN: signing with policy default

第四步:验证签名是否生效

# 在权威服务器上直接查(绕过递归,看原始回答)
dig @localhost DNSKEY example.com +dnssec +short

# 应该看到 DNSKEY 记录和对应的 RRSIG 记录,形如:
# 257 3 13 xxxxxx... (KSK,flags 是 257)
# 256 3 13 yyyyyy... (ZSK,flags 是 256)
# DNSKEY 13 2 604800 20261001000000 ... (RRSIG)

# 查一条普通记录,看是否带 RRSIG
dig @localhost www.example.com A +dnssec +multiline

注意 DNSKEY 的 flags:257 = KSK,256 = ZSK。如果只有 256 没有 257,说明 KSK 没配进去,DS 就没法建。

第五步:在注册商处添加 DS 记录(最关键的一步)

这一步是把你的域名接入全球信任链的唯一动作。在你注册域名的那个地方(不是 DNS 托管商,是注册商,比如 GoDaddy、Namecheap、阿里云域名、腾讯云域名),找到"DNSSEC"或"DS 记录"设置,添加一条:

# 生成 DS 记录(在权威服务器上执行)
dnssec-dsfromkey -2 Kexample.com.+013+12345.key
# 输出格式:example.com. IN DS 12345 13 2 <64位十六进制哈希>

# 拆解含义:
# 12345  = Key Tag(KSK 的标识)
# 13     = 算法(ECDSAP256SHA256)
# 2      = 摘要类型(SHA-256,必须是 2,1 是 SHA-1 已废弃)
# <hash> = KSK 公钥的 SHA-256 摘要

在注册商控制台填入这四个值。有的平台让你填一行完整记录,有的让你分字段填,都行。注意 Key Tag 和哈希一定要从上面这条命令的输出里原样复制,手抄极容易错位。

填完之后,DS 记录要通过上级域(.com)发布出去,这通常需要几分钟到几小时(取决于 TLD 的刷新周期)。在这个窗口期内,你的域名处于最危险的中间状态:DS 已经发布但验证者如果缓存了旧数据会不一致。

第六步:全链路验证

# 1. 确认 DS 已经发布到 .com
dig DS example.com @8.8.8.8 +short
# 应该返回你刚才填的 DS 记录

# 2. 确认递归解析器验证通过(关键:看 ad 标志)
dig +dnssec www.example.com @1.1.1.1 | grep -A1 "flags:"
# 期望看到: flags: qr rd ra ad;

# 3. 用 dnsviz 看完整信任链(会画出每条记录的验证状态)
# https://dnsviz.net/d/example.com/dnssec/

看到 ad 标志才算成功。如果 dig 返回 SERVFAIL 或者没有 ad,说明链条某处断了。

故障排查:DNSSEC 配错会让整站"全球不可解析"

这是 DNSSEC 最需要警惕的地方,也是它比 HTTPS 危险得多的原因:HTTPS 配错了,只是浏览器报警;DNSSEC 配错了,是所有启用验证的解析器直接返回 SERVFAIL——你的域名在全球范围内"消失"。用户连"证书错误"都看不到,就是打不开。

下面是按出现频率排序的坑位清单:

坑一:DS 与 DNSKEY 不匹配(最高频)

症状:dig +dnssec 返回 SERVFAIL,dnsviz 上你的域名显示红色 DS 不匹配。

原因通常是:换了密钥之后忘了同步更新注册商那边的 DS 记录。权威服务器上已经是新 KSK,但上级域还挂着旧 KSK 的哈希,验证者一比对就失败。

# 对比当前 DNSKEY 生成的 DS 和实际发布的 DS
dnssec-dsfromkey -2 Kexample.com.+013+<新的tag>.key
dig DS example.com @8.8.8.8 +short
# 两者必须完全一致

解决:立刻到注册商更新 DS。如果密钥轮换是计划内的,正确做法是先发布新 DS、等 TTL 过期、再撤旧 KSK(双签名过渡期),绝对不能"先撤旧再上新"。顺序错了就是一段时间的全球不可解析。

坑二:签名过期

症状:原本正常,某天突然 SERVFAIL。dig 看 RRSIG 的 expiration 字段已经过了当前时间。

如果用的是手动 dnssec-signzone,这就是必然会发生的事——签名有效期默认 30 天,你忘了重签,30 天后域名就趴了。所以别用手动签名,用 dnssec-policy + inline-signing 让 BIND 自动重签,或者用 PowerDNS 的 pdnsutil secure-zone(它也是自动的)。

# 查看当前签名的有效期
dig example.com SOA +dnssec | grep -A1 RRSIG
# RRSIG 的第二个字段是 expiration(YYYYMMDDHHMMSS 格式)

坑三:报文过大导致 TCP 回退失败

症状:部分用户能访问、部分不能,特别是通过某些中间设备(老的防火墙、运营商设备)的用户。

原因:DNSSEC 的签名让响应报文显著变大。RSA 2048 签名约 256 字节,加上 DNSKEY 经常让响应超过 1232 字节,超过 UDP 的安全上限就会触发 EDNS0 分片或 TCP 回退,而很多网络路径会丢弃分片或阻断 53 端口的 TCP。

# 检查响应大小
dig example.com DNSKEY +dnssec | grep "MSG SIZE"
# 如果超过 1232,就该优化

# 优化手段一:换更短的算法(ECDSA P-256 的签名只有 ~64 字节)
# 优化手段二:减小 TTL、精简 DNSKEY(只保留一个 KSK 一个 ZSK)
# 优化手段三:确保权威服务器响应设置了正确的 EDNS0 UDP size

这是选 ECDSAP256SHA256 而不是 RSA 的核心理由——不只是性能,更是为了让报文留在 UDP 范围内,避免回退到 TCP 带来的各种中间设备问题。

坑四:CDN 和 DNSSEC 的一条与多条 CNAME 冲突

症状:主域名能用,但某个走 CDN 的子域(比如 img.example.com)验证不通过。

核心规则:DNSSEC 下,CNAME 只能有唯一一条记录,不能有多条 CNAME 做负载均衡。而且 CDN 的 CNAME 指向的域名本身也必须支持 DNSSEC(否则签名链断在 CNAME 那里)。Cloudflare 的 CNAME flattening 会帮你把 CDN 的 CNAME 解析成 A/AAAA 并一起签名,是可以的;但如果是普通的多 CNAME,就会出问题。

坑五:只签了主域,忘了所有子域

DNSSEC 的签名是逐区(zone)的。如果你的 DNS 里同时托管了 example.comblog.example.com(后者作为独立 zone 存在,带自己的 SOA 和 NS),那 DNSSEC 需要分别对两个 zone 配置。只配了主域,子域就没保护(但也不会 SERVFAIL,因为验证者发现没有 DS 就退回"不验证"模式)。

DNSSEC 能防什么,不能防什么

理解边界比理解原理更重要,否则会对它产生错误的依赖。

能防:

  • 缓存投毒:伪造的回答签不出来,验证者直接丢弃。这是它的核心价值。
  • 链路中间人篡改:运营商 DNS 劫持、公共 Wi-Fi 的 DNS 篡改、透明代理插播广告,全部无效。
  • DNS 记录被未授权修改后不被察觉:如果攻击者拿到了你 DNS 管理后台的权限改了 A 记录,他同时也有权限改签名(因为签名的密钥在同一个后台)——所以这种情况 DNSSEC 防不住。但如果你把密钥存在本地(自建 BIND),控制台被黑也改不了签名,那么在攻击者拿到 DNS 控制台权限后,签名会立即失效并在 dnsviz 上报警——这反而成了一个强力的入侵检测信号。

不能防:

  • 域名注册商账户被攻破。攻击者可以改 NS 记录把整个域名指向他自己的服务器,也可以同时改 DS。DNSSEC 在这里帮不了你,唯一解法是给注册商账户开双因素认证、开启注册局锁(Registrar Lock)。
  • 你的权威 DNS 服务器被攻破。密钥在服务器上,攻击者拿到就能重新签名一切。这时 DNSSEC 反而"帮着"攻击者把伪造内容签得合法。
  • 隐私。DNSSEC 不加密任何东西,只是签名验证。DNS 查询内容依然明文可见(要隐私得靠 DoH/DoT)。
  • 可用性。它只保证"你收到的是真的",不保证"你收得到"。DNSSEC 甚至会降低可用性——因为它把原本"出错也能凑合用"的情况变成了硬失败。

对 SEO 到底有没有影响

直接回答:DNSSEC 不是排名因素。Google、百度都不会因为你的域名开了 DNSSEC 而给你加权。

但它通过可用性间接影响你的流量,而且路径很明确:

  • 防劫持 = 防流量被偷。没有 DNSSEC 的域名一旦被投毒劫持到钓鱼站,用户在搜索结果的点击会落在别人的站点上,你的排名和流量会凭空消失,而且你查自己的站都查不出问题。
  • 爬虫可达性。搜索引擎的爬虫用的是自己的递归解析器,如果它遇到了投毒或者解析不一致,抓取会失败,直接表现为"已发现未编入索引"或抓取异常。
  • 邮件送达率。这个跟 SEO 没直接关系,但跟站长关系极大——你域名的 MX 被劫持,注册/找回密码/收录通知的邮件就全进了别人邮箱。DNSSEC 加 DMARC 的组合,能把邮件投递的信任度提上去。

所以正确的态度是:别为了 SEO 去开 DNSSEC,要为了"域名主权"去开它。它的收益不在于多几个排名,而在于没人能假装是你。

一份可以直接执行的启用清单

  1. 先评估dig +dnssec 你的域名 @1.1.1.1,看有没有 ad。有就说明已经开了,没有就继续。
  2. 选路线:能一键开就用托管商(Cloudflare / 阿里云 / DNSPod),自建 BIND 就走 dnssec-policy 自动签名。绝对不要用手动 dnssec-signzone——签名过期无人重签是必然故障。
  3. 算法选 ECDSAP256SHA256(算法 13),报文小、性能好、兼容性够。
  4. 先在权威服务器上生成并验证 DNSKEY 和 RRSIG,确认 dig @localhost +dnssec 能看到签名(257 和 256 都在),再动 DS。
  5. 挑低峰时段发布 DS(比如凌晨),因为 DS 发布后的几分钟到几小时是风险窗口。
  6. 发布后立刻三重验证dig DS(上级域有记录)、dig +dnssec(有 ad 标志)、dnsviz.net(链条无红点)。
  7. 记录密钥指纹和 DS 内容到密码管理器。密钥轮换时你需要它们,找不到就得从头来一遍。
  8. 设置监控:用一个脚本每天检查 dig +dnssec 是否仍有 ad 标志,以及 RRSIG 的有效期剩余天数,低于 7 天就告警。这是唯一能救你的自动化。
  9. 密钥轮换要留过渡期:先上新的,等双份 TTL 过期,再撤旧的。永远不要"先撤后上"。
  10. 同时把注册商账户的双因素和 Registrar Lock 打开——DNSSEC 防不了注册商被黑,这两件事才是防那个的。

最后说一个我自己心态上的转变。刚做站的时候我觉得 DNSSEC 是"大公司才需要的东西",我的小站没人会花力气去劫持。后来想明白了:DNS 劫持是自动化的、批量的,攻击者不针对谁,他只是跑一个脚本扫全网,把所有能投毒的域名都指向自己的钓鱼集群。你的站小,恰恰意味着你可能连被劫持了都不会发现。开 DNSSEC 花不了多少时间,而且现在大部分平台都是一键——这是投入产出比最高的一项域名保护,没有之一。

Last modification:September 21st, 2026 at 07:25 pm

Leave a Comment