Let’s Encrypt 通配符证书续期失败:一次 DNS 负缓存导致的“幽灵 NXDOMAIN”排障实录
目录 ▾
Let’s Encrypt 通配符证书续期失败:一次 DNS 负缓存导致的“幽灵 NXDOMAIN”排障实录
引子
那天我像往常一样打开自己的一组网站,却被浏览器冷冰冰地拦在门外
红色警告铺满屏幕:“您的连接不是私密连接”。熟悉的域名、熟悉的页面入口,却因为 HTTPS 证书过期,瞬间变成了“高危站点”。
那一刻的感觉很微妙:不是服务器宕机,不是代码报错,而是一张小小的数字证书,把所有访问者都挡在了门外。更尴尬的是,这不是第一次自动续期,而是“明明配置了自动化,却还是翻车”。
于是,一场从表象到根因的排查就此开始:从证书续期日志,到 DNS 解析,再到 Let’s Encrypt 的校验机制。问题不复杂,却藏得很深。




结论(先给结果)
-
根因:DNS-01 校验时命中 NXDOMAIN 负缓存(negative caching),触发 Let’s Encrypt 的 secondary validation 失败。
-
表象:本机能查到
_acme-challenge的 TXT,但 CA 仍报 NXDOMAIN during secondary validation。 -
修复要点:
- 让
_acme-challenge永久存在(占位 TXT),避免再次出现 NXDOMAIN; - 在 auth-hook 中加入写入后多解析器校验等待;
- 统一 certbot 的 config/work/logs 绝对路径;
- 仅在证书实际变更时 reload Nginx。
- 让
背景与症状
环境使用 Certbot + DNS-01(manual hook) 续期通配符证书 kuangyichen.com / *.kuangyichen.com。
续期日志反复出现:
Certbot failed to authenticate some domains (authenticator: manual)
Detail: During secondary validation: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.kuangyichen.com
同时本机查询却能看到 TXT:
dig +short _acme-challenge.kuangyichen.com TXT
"test-value-123"
这是一类典型的“我这边是好的,但 CA 那边不认”问题。
第一阶段:排除“写错 DNS”的可能
先确认权威 DNS 在哪家:
dig +short kuangyichen.com NS
lock.dnspod.net.
bookstore.dnspod.net.
域名托管在 DNSPod(腾讯云 DNS)。
然后直接查权威 NS(绕过递归缓存):
dig @lock.dnspod.net +short _acme-challenge.kuangyichen.com TXT
dig @bookstore.dnspod.net +short _acme-challenge.kuangyichen.com TXT
权威 NS 均能看到:
"test-value-123"
结论:hook 确实把 TXT 写到了正确的权威 DNS,不是“写错服务商/账号/zone”。
第二阶段:识别“负缓存 + secondary validation”
Why NXDOMAIN 还会出现?
当 _acme-challenge.kuangyichen.com 此前从未存在时,递归解析器会缓存 NXDOMAIN(按 SOA 的 negative TTL)。
随后我们创建了 TXT,但:
- 本机递归解析器可能已刷新
- 但 Let’s Encrypt secondary validation 使用的解析器 仍命中旧的 NXDOMAIN 缓存
这时 CA 看到的仍是“该名字不存在”,于是报 NXDOMAIN(不是 TXT mismatch)。
验证方法:多递归解析器对比
dig @1.1.1.1 +short _acme-challenge.kuangyichen.com TXT
dig @8.8.8.8 +short _acme-challenge.kuangyichen.com TXT
dig @9.9.9.9 +short _acme-challenge.kuangyichen.com TXT
常见现象:有的能查到,有的仍为空或 NXDOMAIN —— 典型负缓存不一致。
第三阶段:结构性修复(消灭 NXDOMAIN 源头)
1️⃣ 创建“常驻占位 TXT”
在 DNSPod 手工新增:
| 类型 | 主机记录 | 值 | TTL |
|---|---|---|---|
| TXT | _acme-challenge | "placeholder" | 600 |
作用:
以后这个名字永远存在,续期时只是在已有记录下增加 TXT 值,不再经历“从不存在到存在”的状态变化,自然不会再触发 NXDOMAIN 负缓存。
权威验证:
dig @lock.dnspod.net +short _acme-challenge.kuangyichen.com TXT
"placeholder"
2️⃣ hook 加入“写入后可见性校验等待”
在 au.sh ... add 成功写入后增加等待逻辑:
-
同时查询:
- 两台权威 NS
- 1.1.1.1
- 8.8.8.8
-
全部都能查到
$CERTBOT_VALIDATION才返回 0
伪逻辑:
for i in {1..60}; do
if dig @lock.dnspod.net ... | grep "$CERTBOT_VALIDATION" &&
dig @bookstore.dnspod.net ... | grep "$CERTBOT_VALIDATION" &&
dig @1.1.1.1 ... | grep "$CERTBOT_VALIDATION" &&
dig @8.8.8.8 ... | grep "$CERTBOT_VALIDATION"; then
exit 0
fi
sleep 10
done
exit 1
这样可以规避 secondary validation 命中旧缓存的窗口期。
第四阶段:修正 certbot 脚本隐患
原脚本问题:
- 使用
--config-dir config等相对路径 → cron 下 cwd 不确定 → 可能出现 No renewals were attempted - 无论成功失败都 reload nginx
修正要点
- 使用绝对路径
- 仅在证书指纹变化后 reload
关键命令形式:
/snap/certbot/current/bin/certbot renew \
--config-dir /root/config \
--work-dir /root/work \
--logs-dir /root/logs
最终状态
| 检查项 | 状态 |
|---|---|
| 权威 DNS TXT 可见 | ✅ |
| 多递归解析器可见 | ⏳ 等待负缓存过期 |
_acme-challenge 永久存在 | ✅ |
| hook 写入可靠性 | ✅ 加入等待机制 |
| 续期目录一致性 | ✅ |
| nginx reload 安全性 | ✅ |
下一次续期将不再触发 NXDOMAIN secondary validation。
额外发现:系统存在异常 preload
日志反复出现:
ERROR: ld.so: object '/$LIB/libonion.so' from /etc/ld.so.preload cannot be preloaded
这通常是异常残留或潜在入侵迹象,应立即排查:
cat /etc/ld.so.preload
cp -a /etc/ld.so.preload /etc/ld.so.preload.bak.$(date +%F_%T)
: > /etc/ld.so.preload
核心经验总结
| 问题类型 | 误判方向 | 真正根因 |
|---|---|---|
| DNS-01 失败 | TXT 没写进去 | NXDOMAIN 负缓存 |
| 本机能查到 | 以为 DNS 正常 | CA 使用不同递归解析器 |
| 偶发 secondary validation | 认为 LE 不稳定 | 负缓存传播不一致 |
| 临时修复 | 重试/延长等待 | 需要“常驻 TXT + 多点校验”结构性方案 |
一句话总结: DNS-01 最大的坑不是 TXT 写入失败,而是 “曾经不存在”被全球解析器记住了。 让
_acme-challenge永远存在,是解决这类问题的终极方案。
写在最后
这次经历也让我重新认识到一件事:自动化不是“配置过一次就万事大吉”,而是一条需要持续验证的链路。
系统真正的脆弱点,往往不在复杂逻辑里,而是在那些“理所当然应该没问题”的基础设施环节。
把故障当作一次体检,比把它当作一次事故更有价值——因为每一次翻车,都是系统变得更稳的一次机会。