Caddy 可以自动获取并续签受管理的证书,不需要运维人员手工实现 ACME 协议。自动化省去了重复操作,却没有取消签发前提,也不能代替对失败的监控。今天证书有效,不证明下次续期仍能完成,更不证明整条链路中的每一跳都使用同一张证书。应先明确服务名称、验证路径、持久存储和 TLS 终止位置,再判断自动化覆盖到哪里。
先让自动 HTTPS 知道服务名称
当配置让 Caddy 知道符合条件的主机名或地址时,自动 HTTPS 才会按规则启用,同时还要考虑文档列出的例外与显式覆盖。Caddyfile 中的站点地址包含主机名,是常见触发方式。file_server 指令负责提供文件,本身并不等于为某个尚未指定的域名申请公开证书,不能把文件服务功能与证书管理的启用条件混在一起。
命令行区别很明确:不带参数的 caddy file-server 默认监听八十端口;提供 --domain 后,才会尝试为指定主机名启用 HTTPS,并将默认监听端口改为四百四十三。无论命令怎样拼写,都不能单独证明 DNS、端口转发或证书授权已经正确。显式 HTTP 配置、手动加载的证书等条件,也可能改变自动行为,应该检查实际生效配置。
本地开发还有不同的信任边界。对于内部名称,Caddy 可以使用自己的本地证书机构。客户端需要通过适当流程信任该机构;某台机器能够正常访问,不代表这张证书已经获得公网客户端的普遍信任。更不能为了修复客户端信任问题,把私有 CA 的签名密钥发给所有客户端。信任根分发与私钥保管是两项不同工作。
让挑战方式与真实路径匹配
Caddy 默认同时启用 HTTP-01 与 TLS-ALPN-01。前者通过公网八十端口验证,后者通过四百四十三端口上的特殊 TLS 握手验证;Caddy 可以在可用方式之间选择。某次 HTTP 挑战失败,并不说明 DNS 验证就是唯一替代方案。应先从日志确认实际失败的是哪种挑战,再检查该方式的入口、转发和防火墙规则。
代理或 CDN 可能干扰这些路径,但它们的存在不会自动使签发变得不可能。即使普通 HTTPS 在 CDN 终止,HTTP 挑战请求也可能被正确转发。DNS-01 则通过 TXT 记录证明控制权,不要求申请证书的服务器接收入站验证流量,但需要受支持的服务商集成和范围适当的 DNS 凭据。选择方式应依据真实路由和权限,而不是凭拓扑名称猜测。
坏掉的样子
陈旧的 A 或 AAAA 记录、端口阻断、错误转发或 CDN 规则变化,都可能让首次签发成功之后的验证失败。应同时检查两个地址族,并读取日志中的具体挑战错误。反复清空存储再重试,不是有效诊断:这样会丢掉本可复用的证书状态,还可能增加不必要的签发尝试,消耗正常业务需要的配额。
实例替换时应保留 Caddy 的数据存储,并保护其中的敏感密钥材料。多个实例需要符合 Caddy 协调语义的存储安排;几台机器上恰好有同名目录,并不表示它们共享了状态。应验证这套配置如何恢复,同时避免把私钥打印到日志、复制进报告或提交到版本库。自动管理证书并不会免除持久化与访问控制责任。
现有证书仍然可用时,续期自动化就可能已经失败。应监控续期错误,以及客户端实际收到的证书到期时间,为处置留下足够提前量。源站续签成功,不能证明 CDN 对外证书已经更新;CDN 对访客启用 TLS,也不会取消对另一条 HTTPS 源站连接的安全和身份检查。这两个终止位置需要分别验收。
签发实验应使用预发签发者,避免把不受公开信任的测试证书带到生产。DNS 凭据轮换后,也需要受控的验证检查。告警提前量应按响应能力与证书寿命选择,不能把一个固定三十天的算术示例当成 Caddy 的续期调度规范,更不能因脚本输出正常就断言真实证书一定会自动更新。
快速验证
保存为 expiry_alert.py 并运行 python3 expiry_alert.py,只需要 Python 标准库。程序使用固定 UTC 观察时刻,三十天阈值是本例运维告警的自主选择,不是 Caddy 内部策略。它不会联系域名或证书机构,不启动监听器,不创建密钥,也不修改系统信任设置,适合单独验证日期边界。
from datetime import datetime, timedelta, timezone
# An operator-chosen alert threshold, not Caddy's renewal scheduler.
ALERT_BEFORE = timedelta(days=30)
NOW = datetime(2026, 10, 15, 12, tzinfo=timezone.utc)
def expiry_alert(not_after, now):
for value in (not_after, now):
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("timezone-aware timestamps required")
remaining = not_after - now
if remaining <= timedelta(0):
return "expired"
if remaining <= ALERT_BEFORE:
return "warning"
return "ok"
for name, days, expected in [
("fresh", 80, "ok"), ("due", 20, "warning"),
("boundary", 30, "warning"), ("expired", -1, "expired"),
]:
not_after = NOW + timedelta(days=days)
actual = expiry_alert(not_after, NOW)
assert actual == expected
print(f"{name}: remaining={days}d alert={actual}")
assert expiry_alert(NOW, NOW) == "expired"
try:
expiry_alert(NOW.replace(tzinfo=None), NOW)
except ValueError:
print("naive timestamp rejected")
else:
raise AssertionError("missing timezone accepted")
print("PASS: expiry alert boundaries; no certificate issuance tested")
四个打印案例依次得到正常、警告、警告和过期。恰好剩余三十天也进入警告范围,到期瞬间则单独归为过期。没有时区的信息会被拒绝,避免将含义不明的日期偷偷当成本地时间或 UTC。断言验证的是这套明确的告警规则,不依赖执行当天,也不假设所有证书都拥有相同总寿命。
该实验没有运行 Caddy、获取证书或观察真实续期。部署验收仍需要检查客户端实际收到的证书、已经配置的挑战路径,并在预期持久存储条件下观察续期成功。应把这些运行证据与这份可重复的小型日期算术检查分开记录,才能知道哪一部分已经证明,哪一部分仍需要实际环境验证。
延伸阅读
来源
- Caddy 自动 HTTPS,启用条件、本地信任、挑战选择及存储,已核验 2026-10-15。
- Caddy file-server 命令,默认监听端口与 domain 参数,已核验 2026-10-15。
- Let’s Encrypt 挑战类型,验证端口、DNS 自动化及受限凭据,已核验 2026-10-15。
- Let’s Encrypt 速率限制,实验使用预发环境与签发失败,已核验 2026-10-15。