Cloudflare 代理(橙云)下 Caddy 能否自动签发 TLS 证书?tls-alpn 失败后的 http-01 回退实测

八月 21, 2026 [网络与安全] #caddy #cloudflare #ssl #证书

现象与背景

VPS 上用 233boy 脚本部署了代理栈,Caddy 负责 80/443 的自动 TLS(站点配置在 /etc/caddy/sites/*.conf 下批量管理)。域名托管在 Cloudflare:

网上流传的说法是"Cloudflare 橙色代理会拦截 ACME 验证请求,导致证书签发失败",甚至需要为 /.well-known/acme-challenge/* 单独配置 Bypass 策略。但直觉存疑:Caddy 支持多挑战类型,为什么回退到 http-01 后会失败?

本文用真实日志证据 + 一个全新子域的主动实验,完整回答:橙云下 Caddy 能不能自动签发/续期,走的是什么路径,以及"回退"到底是怎么发生的。

第一步:只读取证——历史续期到底走的哪条路

先不新建任何配置,翻 VPS 上已有的证据(caddy 证书存储 + journalctl -u caddy):

证据 1:证书的签发/续期时间线

web1: notBefore=Jun 27 | notAfter=Sep 25   (文件 mtime: Jun 27)
web2: notBefore=Jun 27 | notAfter=Sep 25
relay0: notBefore=Jun 27 | notAfter=Sep 25
relay1: notBefore=Jun 26 | notAfter=Sep 23

再配合日志里的 replaced certificate in cache 事件,还原出完整续期史:

时间事件
2-27首次签发(灰云,5 个域名含 web0)
4-28全员续期成功(含 web0)
6-26/274 个域名续期成功(web0 掉队——DNS 已被指到另一台机器)
7-13 后tls-alpn 直连验证骤停(每天 4 次 -> 总共 4 次)——开启橙云的时间点

证据 2:谁在连源站做 tls-alpn 验证?

journalctl 里 "tls-alpn challenge" 事件 483 次,来源 IP 98% 是 LE 官方验证服务器。LE 验证服务器能直连源站 443 协商 acme-tls/1,只可能在灰云下发生(橙云下 CF 边缘会终结 TLS)。

证据 3:221 次失败全部来自一个域名

"could not get certificate from issuer" 共 221 次,按域名拆分 100% 是 web0:

web0: n=221  first=Jun 27  last=Aug 10  error: 2.3.4.5: remote error: tls: internal error
web1/web2/relay0/relay1: n=0

web0 的 DNS 记录被指到了另一台 VPS(2.3.4.5),LE 验证连接打到那台没有 ACME 服务的机器上,必然失败。它成了天然对照组:证书 7-27 过期,每天制造 4 次失败 + 216 条 "queuing for renewal" 日志噪音。

证据 4:http-01 从未被实际使用过

"served key authorization"(Caddy 服务 http-01 token 的事件)全日志 0 次。而 /.well-known/acme-challenge/* 的请求日志有 496 条,全是 "no information found"(无活动挑战时的空请求)——但这些请求能到达源站,恰好证明 http-01 经 CF 转发的路径本身是通的。

第一步结论

  1. 过去所有成功续期(4-28、6-26/27)都发生在灰云下,走 tls-alpn-01 直连
  2. 橙云下的续期一次都还没发生过——"开启代理后仍能续期"目前只是未验证的信念
  3. 流传的"第一次失败是 80 端口没放行"不成立(安全组一直放行 22/80/443)

第二步:主动实验——全新子域橙云开局

理论上的预期(来自 ACME 机制):橙云下 tls-alpn-01 必然失败(LE 连到 CF 边缘,协商不了 acme-tls/1);Caddy 会"回退"到 http-01;CF 把 /.well-known/acme-challenge/* 转发到源站 80/443,Caddy 服务 token,签发成功。

操作

  1. CF 面板新建 cftest 子域 A 记录 -> VPS IP,代理状态直接选橙云
  2. 等 dig 解析到 CF IP(104.21.x/172.67.x)确认橙云生效
  3. VPS 写最小测试站点并重启 caddy:
# /etc/caddy/sites/cftest.conf
cftest.demo.com {
    respond "ok"
}

注意:233boy 的 Caddyfile 全局块有 admin off,systemctl reload 走 admin API(:2019)会被拒,只能 systemctl restart(秒级中断,不影响其它入口)。

日志实录(节选)

12:46:12  trying to solve challenge  identifier=cftest.demo.com  challenge_type=tls-alpn-01
12:46:13  challenge failed  challenge_type=tls-alpn-01
          detail: Cannot negotiate ALPN protocol "acme-tls/1" for tls-alpn-01 challenge
          (attempt:1, max_attempts:3)
          ...同 order 内再重试 2 次,全部失败...
          [跨 order 切换]
          trying to solve challenge  challenge_type=http-01
          served key authentication × 4~5
          (LE 多视角验证,从不同网络位置各取一次 token,全部经 CF 转发到达源站)
12:47 左右  certificate obtained successfully

外部验证

实验全链路通过,耗时约 1~2 分钟,全程无需人工干预。

机制解释

1. 为什么 tls-alpn-01 在橙云下必失败

ACME 的 tls-alpn-01 要求 LE 验证服务器与源站 443 端口协商 acme-tls/1 ALPN 协议。橙云下域名解析到 CF 边缘,CF 终结 TLS,LE 的握手根本到不了源站,报 Cannot negotiate ALPN protocol "acme-tls/1"。

2. "回退"是跨 order 实现的,不是一次订单内先 A 后 B

ACME 流程里"order"是一次证书订单:客户端提交域名列表 -> CA 返回待完成的挑战 -> 全部验证通过才签发。

所以"回退"发生在订单级别,中间有重试退避延迟。

3. 为什么 http-01 能穿过 CF

http-01 走的是 http://域名/.well-known/acme-challenge/<token>。CF 代理对 80 端口的请求是普通转发(不涉及 TLS 终止),/.well-known/acme-challenge/* 原样转发到源站 80;Caddy 在 80/443 上都注册了挑战处理器,服务 token,LE 多视角验证从多个位置各取一次,全部成功。

4. 触发时机:与客户端访问完全无关

签发/续期由 Caddy 主动自动执行:新域名进配置 -> 加载时自动签发;续期按 LE 的 ARI(ACME Renewal Information)窗口自动进行。普通 https 访问只是"消费"证书,既不能触发也不能阻止。

适用边界与注意事项

  1. 前提是源站 80 可达(或 443 可达且无死锁)。安全组/防火墙必须放行 80,否则 http-01 转发到了也取不到 token
  2. 潜在死锁场景:CF 开启 "Always Use HTTPS" + SSL 模式为 Full (strict) 时,http-01 请求被边缘 301 到 https 再回源,Full (strict) 要求源站证书当前有效——对首次签发(源站还没有证书)是死锁。本次实验的 CF 配置下没有这个死锁,直接成功
  3. DNS-01 仍是最稳方案:需要 CF API token,优点是完全不依赖端口/代理状态,适合 Full (strict)+Always Use HTTPS 这类配置,或 80 端口被运营商封禁的场景
  4. 别让 DNS 记录指错机器:web0 的教训——DNS 指到没有 ACME 服务的机器后,证书过期、续期永远失败,每天产生大量噪音日志。清理配置后噪音消失

总结

说法结论
"CF 橙云会拦截 ACME 验证"不准确。拦截的只是 tls-alpn-01;http-01 经 CF 的 80 端口转发正常工作
"必须 DNS-01 或临时关代理"对 Caddy 不必须。tls-alpn 失败后自动跨 order 回退 http-01,1~2 分钟完成签发
"续期失败是因为 80 端口没开"对这台 VPS 不成立(80 一直放行)。真正的失败源是 DNS 指错机器的 web0
"橙云下续期也能正常"实验证实:能。tls-alpn 失败 -> http-01 成功,全程自动

验证方法论:先翻证书存储的签发时间 + journal 日志的 ACME 事件还原历史(0 风险),再建全新子域橙云开局做主动实验(可随时清理),比争论理论高效得多。

常见疑问(FAQ)

Q1:挑战的发起方是谁?token 是谁生成的?

发起方永远是 Caddy(客户端),token 是 CA 在响应里生成的。

步骤方向内容
①Caddy → CAnewOrder(我要给 cftest.demo.com 签证书)
②Caddy → CA获取 authorization
③CA → Caddy(响应)challenge 对象,内含 token——CA 侧现场生成的 128 位随机数

token 必须由 CA 生成:key authorization = token + "." + 账户公钥指纹,CA 手里握着 token 的权威副本,验证时才比对得上。Caddy 只是把它挂到挑战路径/证书扩展里展示。

Q2:http-01 若被重定向到 443,源站还没有证书能取到内容吗?

默认根本不会重定向。Caddy 对 /.well-known/acme-challenge/* 不执行 HTTP→HTTPS 自动重定向(挑战处理器优先级最高),VA 直接走 80 明文取 token。

只有 CF 开启 "Always Use HTTPS" 时才会 301 到 443,此时:

  1. TLS 握手由 CF 边缘自己的 Universal SSL 证书完成——与源站证书毫无关系
  2. CF 回源按 SSL 模式:
    • Flexible → CF 以明文连源站 80,照样取到
    • Full → CF 连源站 443,Caddy 在证书签发期间用临时自签证书完成握手;RFC 8555 §8.3 明确验证时不校验证书链,照样取到
    • Full (strict) → 临时证书校验失败 → 526 → 挑战失败(唯一死锁场景)

结论:无论哪条路径,token 最终都是经 80 端口取到的。

Q3:tls-alpn-01 是 TLS 请求还是 HTTP 请求?

纯 TLS 层,没有任何 HTTP 消息:

VA → 域名:443(TCP)
  ClientHello:  ALPN 扩展 = acme-tls/1
域名 → VA
  ServerHello:  ALPN = acme-tls/1
  Certificate:  特殊证书(id-pe-acmeIdentifier 扩展含 key authorization)
VA 校验扩展内容 → 断开连接

没有路径、没有 GET、没有 HTTP 层数据。

Q4:http-01 全是明文,很容易被伪造吗?

不是漏洞,因为它验证的不是保密性而是控制权——"谁能响应这个随机路径,谁就控制该域名的 80 服务",这本身就是 CA 要证明的东西。防伪靠三层:

  1. token 即时生成 + 128 位随机(2^128 可能):挑战发起时才掷骰子,无法预置、无法枚举
  2. token 分发走加密通道:CA → Caddy 通过 TLS 加密 + JWS 签名的 ACME API(LE 的 443 端口)传输,劫持 80 端口的人看不到
  3. 多视角验证 + 短时效:从多个网络位置独立验证,单点路径被劫持时其他视角失败

伪造者要成功,必须实时劫持 VA → 源站的网络路径——那等于控制域名解析/网络,本来就配拿到这张证书。对比 dns-01:它的 TXT 记录同样是公开明文,靠"只有域名控制者能写记录"防伪。三者都是"证明控制权"模型,没有一个依赖保密。

Q5:token 通过 443 分发?Caddy 自己还没有证书啊?

这个 443 是 Let's Encrypt API 服务器的 443,不是源站的 443。

TLS 只需要服务器端有证书:

Caddy(TLS 客户端,无需任何证书)
   │  主动连接
   ▼
acme-v02.api.letsencrypt.org:443(TLS 服务器)
   │  出示 LE 自己的服务器证书(公网权威 CA 签发)

Caddy 验证 LE 的服务器证书(像浏览器验证任何 https 网站),与"为 cftest.demo.com 申请证书"是两件独立的事。Caddy 的 443 只在 tls-alpn-01 验证时被 VA 连接,那时用的是临时证书,也不需要正式证书。整条链路不存在"先有鸡还是先有蛋"问题。