DNSSEC 与私有 DNS 实战:签名链、服务发现与内网解析体系设计
在《DNS 深度解析》里,我们把一次解析的递归、缓存与基本安全讲透了。这一篇往前走两步:一是DNSSEC 到底怎么签、怎么验,密钥怎么管——把「完整性」这件事彻底拆开;二是私有 DNS 在云原生里的新角色——它早就不只是「内网域名解析」,而是服务发现与流量治理的基础设施。
一、DNSSEC 的核心:一条从根下传的签名信任链
DNSSEC 用非对称密码学给资源记录签名。它的巧妙之处在于不要求每个权威服务器都可信,只需要信任「根」这一个锚点,然后沿着一级级的委托,把信任「下传」到你的域名。
| 记录类型 | 作用 | 通俗理解 |
|---|---|---|
| RRSIG | 对一组 RRset 的数字签名 | 「这批记录是我签的,签名如下」 |
| DNSKEY | 该区域的公钥(分 KSK/ZSK) | 「验证我签名请用这把公钥」 |
| DS | 子域 KSK 的摘要,存放在父域 | 「我认可子域用的是这把钥匙」 |
| NSEC / NSEC3 | 证明「某记录不存在」 | 防攻击者伪造 NXDOMAIN |
验证的链条是这样走通的:解析器信任根的公钥 → 根用 DS 记录背书了 .com 的 KSK → .com 用 DS 背书了 example.com 的 KSK → example.com 的 KSK 签了 ZSK → ZSK 签了具体记录(RRSIG)。每一级的「DS 摘要」就是父域对子域公钥的信任背书,环环相扣,篡改任何一环都会导致验证失败。
KSK / ZSK 分离:为什么要两把钥匙
DNSSEC 里有两把钥匙,各司其职:
- KSK(Key Signing Key):只签 ZSK,很少变动;其摘要(DS)要提交给父域/注册商,换 KSK 需要走「密钥轮转 + 父域更新 DS」的流程。
- ZSK(Zone Signing Key):签具体记录,可以相对频繁地轮换而不惊动父域。
把两者分开,是为了让「日常换钥匙」不影响「信任链的锚点」:换 ZSK 是区域内部的事,换 KSK 才是需要父域配合的大事。这背后还有一个更隐蔽的风险点——DNSSEC 的密钥轮转如果做错(旧 KSK 撤销了、新 DS 还没生效),会导致整个域名被判定为验证失败(bogus),等于把域名「打挂」。所以轮转必须严格按「先加新、再过渡、最后撤旧」的顺序。
二、上手验证:dig 怎么读 DNSSEC
# 1. 验证一个已启用 DNSSEC 的域名,看 flags 里是否有 ad(authenticated data)
dig example.com A +dnssec
;; flags: qr rd ra ad; # ad 位 = 这条链被验证通过
# 2. 看 DS 记录(父域对子域公钥的背书)
dig example.com DS
# 3. 看 DNSSEC 链的起点:根区的信任锚
dig . DNSKEY | head
排障时最有用的是 ad 位:解析器返回的应答 flags 里有 ad,说明它完成了 DNSSEC 验证;若验证失败,会返回 SERVFAIL 而不是一个「看似正常但未验证」的答案(这是 DNSSEC 的 fail-closed 设计——宁可不答,不给假答案)。一个常见坑:本地时间不准。RRSIG 有有效期,解析器本地时钟偏差过大,会把仍在有效期内的签名误判为过期,导致大面积 SERVFAIL——启用 DNSSEC 的递归器,NTP 校时是硬前提。
三、私有 DNS:从「内网解析」到「服务发现」
私有 DNS 的历史用法是「内网域名 + 本地权威」:公网域名向上游转发,内网域名本地应答。但到了云原生时代,私有 DNS 的角色发生了质变——它成了服务发现(Service Discovery)的核心机制。
1. 服务发现为什么离不开 DNS
在微服务/K8s 架构里,服务的 IP 是动态漂移的(Pod 重建、扩缩容、滚动发布),硬编码 IP 必死。于是大家约定:服务名 = 域名,DNS 查询 = 服务寻址。K8s 集群里的 svc.namespace.svc.cluster.local 就是这套逻辑的典型落地:
# K8s 内:Pod 之间用「服务名」互相调用,而不是 IP
curl http://api-svc.default.svc.cluster.local:8080/health
# 这个域名由 CoreDNS 解析,返回 Service 的 ClusterIP(虚拟 IP)
# Service 再通过 kube-proxy/iptables 把流量负载均衡到后端 Pod
这里 DNS 解决了两件事:寻址(把稳定的名字映射到漂移的 IP)和解耦(调用方不关心后端具体是谁、有几个、在哪)。配合 Service 的负载均衡,形成了「名字 → 虚拟 IP → 后端实例」的两层抽象。
2. CoreDNS:可插拔的 DNS 引擎
CoreDNS 之所以取代 Kube-DNS 成为 K8s 默认 DNS,在于它把 DNS 功能拆成了一个个可编排的插件(plugin),通过 Corefile 组合。几个关键插件:
.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa { # 接 K8s API,动态提供服务发现
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
forward . /etc/resolv.conf # 其余域名转发给上游
cache 30
loop
reload
loadbalance
}
这种「中间件式」架构让 CoreDNS 既能做服务发现,又能做内网权威、递归转发、负载均衡、缓存,甚至能对接 etcd(SkyDNS 风格)做自定义服务注册。理解了插件模型,就理解了为什么现代 DNS 方案都长这样。
3. 内网解析体系设计:分层与容错
一个稳健的内网 DNS 体系,通常会设计成多层:
| 层 | 角色 | 典型实现 |
|---|---|---|
| 递归层 | 面向客户端,缓存 + 转发 | Unbound / dnsmasq / CoreDNS |
| 权威层 | 内网域名的最终答案 | BIND9 / 云厂商私有域 |
| 服务发现层 | 动态服务名 → 实例 | CoreDNS(K8s)/ Consul DNS / etcd |
设计上几条硬规则:
- 递归与权威分离——对外递归的服务器不承载内网权威,避免「递归被投毒」连带污染权威数据。
- 转发链要防环——dnsmasq/CoreDNS 的
forward与loop检测,防止「A 转发给 B、B 又转发回 A」的死循环。 - 上游要冗余——至少配两个独立上游,避免单点;并设合理的
timeout与attempts。 - 负缓存与 TTL 要权衡——服务发现的 TTL 通常很短(几十秒),保证实例下线后名字能尽快「失活」。
四、私有 DNS 的排障清单
# 1. 先定位是哪一层的问题
nslookup api-svc.default.svc.cluster.local # 服务发现层返回了吗?
dig @内网权威 example.internal A # 权威层对吗?
dig @递归器 example.internal A +trace # 转发链路走得通吗?
# 2. CoreDNS 常见故障
kubectl logs -n kube-system -l k8s-app=kube-dns # 看 CoreDNS 日志
# 高频关键词:loop(转发环)、i/o timeout(上游不可达)、NXDOMAIN(服务没注册/名字拼错)
# 3. 服务发现的经典坑:DNS 缓存 TTL 过长
# 实例已下线,但客户端还在拿旧 IP → 检查 TTL 是否过大、是否有多级缓存未失效
服务发现场景里,「改了没生效」最可能是 TTL 与各级缓存(客户端 OS 缓存、应用内 DNS 缓存、递归层缓存)叠加导致的。这也是为什么云原生里普遍倾向短 TTL + 客户端重连兜底(如 gRPC 的 load balancing + 连接探活),而不是单纯依赖 DNS 的更新速度。