读书笔记

Let’s Encrypt 通配符证书续期失败:一次 DNS 负缓存导致的“幽灵 NXDOMAIN”排障实录

· 约 3,812 字 · 阅读约 20 分钟
目录

Let’s Encrypt 通配符证书续期失败:一次 DNS 负缓存导致的“幽灵 NXDOMAIN”排障实录

引子

那天我像往常一样打开自己的一组网站,却被浏览器冷冰冰地拦在门外

红色警告铺满屏幕:“您的连接不是私密连接”。熟悉的域名、熟悉的页面入口,却因为 HTTPS 证书过期,瞬间变成了“高危站点”。

那一刻的感觉很微妙:不是服务器宕机,不是代码报错,而是一张小小的数字证书,把所有访问者都挡在了门外。更尴尬的是,这不是第一次自动续期,而是“明明配置了自动化,却还是翻车”。

于是,一场从表象到根因的排查就此开始:从证书续期日志,到 DNS 解析,再到 Let’s Encrypt 的校验机制。问题不复杂,却藏得很深。

Image

Image

Image

Image

结论(先给结果)

  • 根因:DNS-01 校验时命中 NXDOMAIN 负缓存(negative caching),触发 Let’s Encrypt 的 secondary validation 失败。

  • 表象:本机能查到 _acme-challenge 的 TXT,但 CA 仍报 NXDOMAIN during secondary validation

  • 修复要点

    1. _acme-challenge 永久存在(占位 TXT),避免再次出现 NXDOMAIN;
    2. 在 auth-hook 中加入写入后多解析器校验等待
    3. 统一 certbot 的 config/work/logs 绝对路径
    4. 仅在证书实际变更时 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 永远存在,是解决这类问题的终极方案。

写在最后

这次经历也让我重新认识到一件事:自动化不是“配置过一次就万事大吉”,而是一条需要持续验证的链路。

系统真正的脆弱点,往往不在复杂逻辑里,而是在那些“理所当然应该没问题”的基础设施环节。

把故障当作一次体检,比把它当作一次事故更有价值——因为每一次翻车,都是系统变得更稳的一次机会。

相关文章