一行重定向挡住了续期
目标
在 Pod 内让 HTTP-01 挑战通过,重现 HTTP→HTTPS 重定向吞掉挑战而阻塞续期的事故,然后只把挑战路径设为例外来修复。编写用于常态检查的 probe.sh 和 cert-manager 的 Certificate 清单。
为什么重要
ACME 自动续期在首次签发成功之后,两个月内不会出现任何症状。其间加入的一行重定向,会在续期时阻塞挑战,而那时已经没有人会想起那次变更。这是我在家庭实验室中实际遇到的事故,一重现就一目了然。素材有两个——/opt/app/edge.py 是每次请求都会重新读取配置文件(/root/acme/edge.json)的边缘服务(HTTP 8080、HTTPS 8443),/opt/app/acme_va.py 是会跟随重定向并把正文与密钥授权进行比较的验证服务器。
步骤
- 令牌是
zk3r9QmZ7Vf1Yb2Ld8Ns4Hj6Xc5Pa0Wt,账户密钥指纹是Q7pVw2xK9mN4rT1sB6yL0cH3fJ8dG5eA。请在/root/acme/challenges/zk3r9QmZ7Vf1Yb2Ld8Ns4Hj6Xc5Pa0Wt文件中写入密钥授权字符串。 - 创建
/root/acme/edge.json——http_port为 8080,https_port为 8443,redirect_to_https为 false,exempt_paths中放入/.well-known/acme-challenge/,challenge_dir为/root/acme/challenges。启动python3 /opt/app/edge.py --config /root/acme/edge.json,并把curl -s http://127.0.0.1:8080/.well-known/acme-challenge/<토큰>(占位符为令牌)的正文保存到/root/acme/02-fetch.txt。 - 把
python3 /opt/app/acme_va.py --url http://127.0.0.1:8080/.well-known/acme-challenge/<토큰> --expect <키인가>(占位符依次为令牌、密钥授权字符串)的输出保存到/root/acme/03-valid.txt。第一行必须是VALID。 - 重现事故:把
edge.json中的redirect_to_https改为 true,把exempt_paths改成空列表。把curl -sI http://127.0.0.1:8080/.well-known/acme-challenge/x的输出保存到/root/acme/04-curl.txt,把验证服务器的输出保存到/root/acme/04-broken.txt(必须是 INVALID)。 - 修复:在保持重定向开启的情况下,把
/.well-known/acme-challenge/重新放回exempt_paths。把验证服务器的输出保存到/root/acme/05-fixed.txt(VALID),把curl -sI http://127.0.0.1:8080/保存到/root/acme/05-app.txt(仍然是 301)。 - 创建
/root/acme/probe.sh <http://host:port>。如果<base>/.well-known/acme-challenge/probe-check的状态码是 3xx,就输出REDIRECT并以 1 结束;其他情况(200、404 等)则输出OK并以 0 结束。把针对http://127.0.0.1:8080的输出保存到/root/acme/06-probe.txt。 - 在
/root/acme/certificate.yaml中编写 cert-manager 的Certificate——apiVersion: cert-manager.io/v1、spec.secretName、spec.dnsNames中放入www.lab.internal、spec.duration和spec.renewBefore(时间单位用h)、spec.issuerRef.kind: ClusterIssuer。 - 在
/root/acme/08-report.md中整理这次事故。必须包含HTTP-01、作为证据看到的301、设为例外的路径.well-known/acme-challenge,以及renewBefore为什么会使事故延迟暴露。
参考
- edge.json 每次请求都会被重新读取,所以修改之后不需要重启边缘服务。
- 验证服务器即使失败也会输出内容。用
> 파일(占位符为文件)保存,无论退出码如何都会留下。 curl -sI是 HEAD 请求,只显示状态行和头部。请查看Location:头指向哪里。- 常见错误:在第 4 步如果不清空
exempt_paths,就无法重现事故。在第 7 步如果像renewBefore: 15d那样使用d,cert-manager 是不会接受的。
放置密钥授权文件
在 /root/acme/challenges/zk3r9QmZ7Vf1Yb2Ld8Ns4Hj6Xc5Pa0Wt 中写入密钥授权字符串(令牌.指纹)。
RFC 8555 的密钥授权,是把 token、句点和账户密钥指纹连接起来的字符串。文件名就是令牌本身,内容就是这一行。指纹已在说明中给出。
启动边缘服务并获取挑战
创建 /root/acme/edge.json(重定向 off,挑战路径为例外),启动边缘服务,并把挑战 URL 的正文保存到 /root/acme/02-fetch.txt。
JSON 的键名与说明中的一致。边缘服务用 nohup 放到后台,日志输出到 /root/acme/edge.log。用 curl -s 取得的正文必须与第 1 步的密钥授权相同。
请验证服务器来确认
运行 acme_va.py,把输出保存到 /root/acme/03-valid.txt。第一行必须是 VALID。
--url 是挑战 URL,--expect 是密钥授权字符串。输出的第一行是判定结果,其下逐行列出所跟随的路径。
重定向吞掉了挑战
把 edge.json 改为重定向 on、无例外,把 curl -sI 的输出保存到 /root/acme/04-curl.txt,把验证服务器的输出保存到 /root/acme/04-broken.txt。
用一行 python3 修改 JSON,出错会比较少。curl -sI 的第一行会显示 301,Location 头中会出现 https,验证服务器跟随 301 → 404 之后给出 INVALID。
只把挑战路径设为例外
在保持重定向开启的情况下,把挑战路径放进 exempt_paths,把验证输出保存到 /root/acme/05-fixed.txt,把 curl -sI http://127.0.0.1:8080/ 保存到 /root/acme/05-app.txt。
要改的只是例外列表中的一行。应用路径(/)仍然要以 301 转到 HTTPS,只有挑战路径要在 HTTP 上直接返回 200。
查看续期是否被阻塞的一行
创建 /root/acme/probe.sh <http://host:port>(3xx 则 REDIRECT、1,否则 OK、0),并把针对 http://127.0.0.1:8080 的输出保存到 /root/acme/06-probe.txt。
用 curl -s -o /dev/null -w '%{http_code}' 只取状态码,再用 case 分类即可。404 是 OK——没有文件不会阻塞续期,阻塞续期的只有重定向。评分器会分别启动一台 301 服务器和一台 404 服务器来运行。
用 cert-manager 声明
在 /root/acme/certificate.yaml 中编写 Certificate(cert-manager.io/v1、secretName、dnsNames 中放 www.lab.internal、duration 和 renewBefore 用 h 为单位、issuerRef.kind 为 ClusterIssuer)。
请按文档中的示例结构,但值用本实验的。duration 必须大于 renewBefore,两者都是 Go 时间字符串(h、m、s)。要在其他命名空间中使用的签发者是 ClusterIssuer。
事故报告
在 /root/acme/08-report.md 中写下 HTTP-01 被阻塞的经过、证据(301)、设为例外的路径,以及 renewBefore 使事故延迟暴露的原因。
第 4 步的 curl 结果是证据,第 5 步的例外列表是修复。签发之后没有症状,要到 renewBefore 的时候挑战才会再次运行,这就是“延迟暴露”的原因。