DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Certbot HTTP-01 验证收到其他站点的内容

Certbot 验证文件返回另一个网站的 HTML,IPv4 访问却正常。比较有效挑战在 IPv4、IPv6 上的响应,核对 AAAA 和 Nginx 监听及虚拟主机,并用 staging 续期确认修复。

Certbot 续期报 unauthorized,错误中说验证文件内容不匹配,返回的却是另一个网站的 HTML。通过 IPv4 打开网站正常,反复检查 A 记录也没有发现问题。检查 AAAA 指向的 IPv6 入口,以及同一域名在 IPv6 上命中的虚拟主机。

挑战请求返回同服务器另一个域名的页面时,检查目标域名的 IPv6 监听和虚拟主机匹配。这个结果不足以判断 Certbot 或 Nginx 存在版本缺陷。

下面的步骤适用于 Let's Encrypt 的 HTTP-01 域名验证,使用当前 Certbot 命令和 Linux 上的 Nginx 配置示例。DNS-01、TLS-ALPN-01 的验证路径不同;经过 CDN 或负载均衡时,还需要核对公开入口实际转发到的节点,不能只测试其中一台源站。

IPv4 正常不能替 IPv6 完成验证

HTTP-01 会访问域名 80 端口上的 /.well-known/acme-challenge/<TOKEN>,要求收到本次挑战对应的内容。网站首页能打开,不能证明这条路径也返回正确内容。

域名同时有 A 和 AAAA 时,验证的首次连接优先使用 IPv6。IPv6 收到错误页面、404 或不匹配的正文,不会因为 IPv4 正常就重新验证 IPv4。IPv6 到 IPv4 的回退只发生在连接超时,而且只针对首次 HTTP-01 请求,后续重定向没有同样的回退处理。

选择这条连接路线的是验证服务器。改变 Certbot 本机访问 ACME API 所用的地址族,不能让远端验证服务器忽略域名的 AAAA。

记录失败的域名、挑战类型、返回地址、状态码和正文,再查询该域名的两类记录:

dig example.com A +noall +answer
dig example.com AAAA +noall +answer

example.com 是占位域名,替换为证书中实际失败的那个名字;根域与 www 应分别检查。若返回多个地址,每个入口都应能处理该域名的挑战。如果 AAAA 仍指向旧服务器,先修正 DNS;若地址正确,继续检查服务器在 IPv6 上选中了哪个站点。需要比较权威记录与缓存时,可沿用DNS 记录检查步骤。

用仍有效的挑战地址比较两条路径

失败结束后,Certbot 可能已经清理临时挑战内容。此时拿日志里的旧 TOKEN 再访问,得到 404 并不能证明新的挑战也会失败。

已有证书可以在 staging 测试中暂停,趁挑战内容仍存在时检查。先保存 Nginx 与 Certbot 配置,查看续期插件和已有 pre/post hook;--dry-run 仍会联网,并可能临时修改配置、重载服务或运行这些 hook,不能当作纯只读命令。安排能够接受这些操作的时间,再执行:

sudo certbot certificates
sudo certbot renew --cert-name example.com --dry-run \
  --server https://acme-staging-v02.api.letsencrypt.org/directory \
  --debug-challenges -v

--cert-name 应使用第一条命令显示的证书名称,它可能带数字后缀,不一定等于域名。--debug-challenges 会在设置挑战后等待输入,配合 -v 显示挑战地址及预期值。这里显式使用 Let's Encrypt staging,测试证书不会保存;保留原续期插件,不要为了测试擅自切换成 standalone 或 webroot。

保持 Certbot 等待,在另一台有 IPv4、IPv6 公网连通性的机器上请求同一个挑战地址:

curl --noproxy '*' -4 --max-time 15 -i \
  http://example.com/.well-known/acme-challenge/CHALLENGE_TOKEN
curl --noproxy '*' -6 --max-time 15 -i \
  http://example.com/.well-known/acme-challenge/CHALLENGE_TOKEN

把域名与 CHALLENGE_TOKEN 都替换为这轮输出的值,对照预期正文,不能只看 HTTP 200。--noproxy '*' 排除本机代理影响;暂不加 -L,先看第一跳是否返回其他页面或重定向。若测试机器本身没有 IPv6 路由,它的连接失败不足以说明服务器 IPv6 不可达。

两条路径的结果 下一步
IPv4 正确,IPv6 返回其他网站 核对 AAAA 地址与 IPv6 虚拟主机匹配
IPv4 正确,IPv6 返回 404 查 IPv6 站点中的挑战 location、实际文件路径
IPv6 连接超时 查地址、路由和 80 端口入口;另查是否发生重定向
两边都返回应用首页或登录页 查挑战路径是否进入应用路由、认证或统一重写

有多个地址时,可以逐个固定连接目标,同时保留域名作为 HTTP Host:

curl --noproxy '*' --max-time 15 -i \
  --resolve 'example.com:80:203.0.113.10' \
  http://example.com/.well-known/acme-challenge/CHALLENGE_TOKEN
curl --noproxy '*' --max-time 15 -i \
  --resolve 'example.com:80:[2001:db8::10]' \
  http://example.com/.well-known/acme-challenge/CHALLENGE_TOKEN

这些 IP 是文档示例地址,必须替换。直接请求 IP 可能命中默认站点,不能代表用域名验证的结果。

完成观察后按提示继续本轮测试。需要修改配置时,等测试结束、插件的临时配置清理后再编辑原配置,避免修改被它的回滚覆盖。若挑战路径发生跳转,继续查 Location 中的地址和目标站点;不要把“第一跳已回退 IPv4”理解为后续跳转也会回退。

Nginx 要在 IPv6 监听地址上匹配这个域名

Nginx 先按请求到达的地址和端口筛选 server,再在匹配范围内查 server_name。目标域名只有 IPv4 监听,而 IPv6 入口属于另一个站点时,即使请求带了正确 Host,也可能由 IPv6 的默认站点处理。

查看目标实例对应的配置:

sudo nginx -T

核对目标域名的 server_name、listen 80、IPv6 listen 及挑战路径。-T 检查并打印磁盘配置,并不是运行中配置的快照;配置修改后还需要成功重载。若实例使用自定义 -c 或 -p,检查与重载应沿用它们;容器部署要检查容器内实际使用的配置及对外入口。

当地址正确,且确实缺少这个域名的 IPv6 监听时,在原来的目标 server 中补齐对应监听,例如:

listen 80;
listen [::]:80;
server_name example.com;

不要另建一个重复域名的站点块来覆盖原配置,也不要随意把它设为所有 IPv6 请求的 default_server。若原部署只绑定指定地址,应按原地址范围补充 IPv6;还要确认该地址的 80 端口已经通过实际防火墙及负载均衡路径到达这个实例。

使用 Nginx 认证插件的已有证书,先保留该插件的挑战处理方式。若原本使用 webroot,Nginx 应从 Certbot 写入文件的同一目录读取;例如实际 webroot 已约定为 /var/www/acme 时,可以采用下面的 HTTP 站点结构:

server {
    listen 80;
    listen [::]:80;
    server_name example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/acme;
        default_type text/plain;
        try_files $uri =404;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

域名和目录必须对应自己的部署。这段配置放在 Nginx 的 http 上下文中,展示已有 webroot 模式的 HTTP 入口,不能直接覆盖原有 HTTPS、认证、代理或其他站点配置。不要把它叠加到 Certbot Nginx 插件正在生成的临时挑战配置上。

root 会把完整 URI 接到目录后,因此该请求读取的是 /var/www/acme/.well-known/acme-challenge/CHALLENGE_TOKEN。目录需要能被 Nginx 读取;容器中的目录与 Certbot 写入的目录若没有共享同一份文件,也会返回 404。

示例让挑战路径直接返回文件,其他请求继续转 HTTPS。若原配置把 return 301 写在 server 层,只添加一个 location 不会绕过它;server 层的 rewrite 模块指令先执行,应调整为只在需要跳转的 location 中执行。

重载后,重新跑完整的测试续期

检查通过后再重载:

sudo nginx -t && sudo nginx -s reload

重载成功才表示新监听及路由开始生效,继续核对错误日志和两条地址路径。Nginx 无法应用新配置时会继续使用旧配置,不能只因为发送了 reload 就认定恢复。

如果域名本来就不提供 IPv6,可以删除错误的 AAAA;这会改变 IPv6 用户的访问路径,应先确认没有其他服务依赖这条记录,保存原值,并等待旧记录缓存按 TTL 到期。需要继续提供 IPv6 时,应修正地址或站点配置。

修复后重新执行同一证书的 staging 测试,这次让它完成:

sudo certbot renew --cert-name example.com --dry-run \
  --server https://acme-staging-v02.api.letsencrypt.org/directory

检查测试摘要确实对选中的证书完成了验证,证书中的其他域名也通过,并查看原来失败的 IPv6 入口是否命中正确站点。测试通过证明这次 staging 续期路径可用;生产证书是否续期、是否部署生效,还要以正常续期结果和实际 HTTPS 返回的证书为准,不必用反复强制签发来验收。

配置变更影响其他域名时,恢复本次保存的站点配置,检查后重载,再验证原有业务入口。DNS 回退也会受缓存影响,应按原记录恢复并观察;不要删除 Certbot 的证书目录来重新开始。