Caddy 中间证书过期导致 HTTPS 报错:完整排查与根治指南
【免费下载链接】caddyFast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS项目地址: https://gitcode.com/GitHub_Trending/ca/caddy
生产环境的 Caddy 在一次例行升级后突然"挂了":浏览器打开站点直接提示NET::ERR_CERT_DATE_INVALID,而caddy validate正常、日志里也看不到启动报错,抓证书一看,叶子证书的有效期居然还有大半年。影响范围是所有走 HTTPS 的域名,监控面板上的成功率在十分钟内跌到零。
快速止血:先恢复服务再谈原因
第一步,确认 Caddy 实际吐给客户端的是哪条链:
openssl s_client -connect 你的域名:443 -servername 你的域名 -showcerts 2>/dev/null | \ openssl x509 -noout -issuer -subject -dates目的:定位到底哪一张证书过期(叶子还是中间),避免修错文件。
第二步,如果你配过storage或者想让 Caddy 接管签发,先删掉手动tls静态证书块,让自动 HTTPS 生效,然后热重载:
caddy reload --config /etc/caddy/Caddyfile # 不断连重载,reload 会失败时才需 restart目的:Caddy 的 ACME 流程会自动拉取 CA 当前有效的完整链,这是最快的恢复路径。
第三步,reload 不生效或有客户端长连接卡住时,重启进程兜底:
systemctl restart caddy && systemctl status caddy根因拆解:Caddy 只认文件里写的链
因果链是这样的:CA 定期轮换中间证书(R3 换 R10,Let's Encrypt 大约每 4-5 年一次)→ 你当年用cat leaf.crt intermediate.crt > fullchain.pem拼好的链文件里,中间那张已经过了有效期 → Caddy 加载静态证书时按文件原样加载,不做链校验、不会自动补中间 → 浏览器用系统信任库验链时发现中间已过期 → 即使叶子证书还有效,整条链也判定失败,报CERT_DATE_INVALID。
这里最关键的一点:tls 证书文件 密钥文件这条指令走的是 PEM 文件加载器,见 modules/caddytls/pemloader.go。它只做一件事——把文件内容读进来配对密钥,链对不对、有没有过期,一概不管。大白话讲:你喂给 Caddy 一条"半新鲜"的链,它就原封不动端给每个访客,Caddy 不会替你悄悄换掉那张过期的中间。真正在握手侧选择证书的逻辑在 modules/caddytls/connpolicy.go,它只是"按 SNI 选已加载的证书",不负责刷新内容。
精准诊断:一条命令确认是不是这个问题
| 症状 | 疑似原因 | 验证命令 |
|---|---|---|
叶子notAfter未过期,但浏览器报日期错误 | 链文件里的中间证书已过期 | openssl s_client -connect 域名:443 -showcerts逐张看Not After |
s_client输出末尾有verify error:num=10:certificate has expired | 同上的确认信号,且客户端信任库也找不到有效签发者 | openssl verify -CAfile 域名.fullchain.pem 域名.leaf.pem |
| 只有部分客户端报错,部分正常 | 客户端信任库缓存了旧中间(移动端/浏览器更明显),本质还是链不对 | 换一台设备 + 手机直连复现 |
| 日志出现 ACME 签发失败但站点照常访问 | Caddy 回落到旧静态证书继续服务 | grep -i acme /var/log/caddy/caddy.log |
第三行是最迷惑人的现象:iOS 和部分浏览器会缓存已见过的中间证书,CA 轮换初期只有新客户端先挂,容易造成"时好时坏"的错觉,其实链文件从中间过期那一刻起就是坏的。
根治方案:两种改法,选你证书的来源
改法一:继续用自签/自管证书——重建链文件。从 CA 官网下载当前有效的中间证书(Let's Encrypt 在官网 certificates 页可下),重建 fullchain:
# 顺序必须是 叶子在前、中间在后 cat example.com.crt intermediate-isrg-root-x1.crt > example.com.fullchain.pem # 校验链完整性,应输出 OK openssl verify -CAfile example.com.fullchain.pem <(head -1 /dev/null; cat example.com.crt)改什么:Caddyfile 中tls /path/fullchain.pem /path/key.pem的 fullchain 路径。为什么:静态加载路径只认文件内容,文件不换、故障永在。改后看到什么:openssl s_client输出里每张证书都有效,verify return code: 0 (ok)。
改法二(推荐):把域名交给 Caddy 自动 HTTPS。删掉手动tls指令,Caddy 通过 ACME 直接向 CA 申请,CA 返回的链永远是当前有效的中间,链文件这个中间产物直接消失。签发参数(renewal window 等)由 modules/caddytls/automation.go 中的RenewalWindowRatio控制,默认取证书寿命的一定比例,不需要手工干预。改后看到什么:caddy list-modules正常,且证书目录 modules/filestorage/filestorage.go 对应的存储路径下出现自动管理的证书与 key。
改法三:调宽自动续期窗口。自动 HTTPS 的域如果续期失败,Caddy 会沿用旧证书直到过期,静默风险不小。可以在tls指令块里显式设置renewal_window_ratio 0.2(含义:剩余寿命低于总寿命 20% 时提前重签),把触发点从默认值拉宽,给排障留时间。改后看到什么:caddy log --level DEBUG里能看到obtaining certificate日志提前出现而不是临近过期才出现。
长期预防:监控与告警
自动化层面,只要域名走 ACME,续期、链获取都由 modules/caddytls/acmeissuer.go 背后的流程闭环处理,你要做的是保证 80 端口可达、邮箱告警能收到——续期前 Caddy 会向注册邮箱发通知,收到即代表流程在跑。监控层面,给站点加一条每天一次的链探测任务即可:
# crontab 每日探测,链上任何一张过期就退出码非 0,接邮件/短信告警 echo "0 8 * * * openssl s_client -connect 你的域名:443 -verify_return_error > /dev/null 2>&1 || curl -sSf https://你的域名 -o /dev/null" >> /etc/crontab/caddy-cert-check核心指标只有一个:s_client的verify return code是否为 0。它同时覆盖了"叶子过期、中间过期、链缺失"三种情况,比单独看叶子notAfter可靠得多。
把要点收拢成一句:Caddy 对静态tls证书是"文件即真相",链文件里的中间证书过期它既不校验也不会换,恢复靠换文件或回退自动 HTTPS,根治靠让 ACME 闭环管证书,外加每日一次verify return code: 0探测。相关入口:modules/caddytls/tls.go(TLS App 与应用配置)、modules/caddytls/certmanagers.go(各证书管理器实现)、modules/caddytls/automation.go(自动化策略)。
【免费下载链接】caddyFast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS项目地址: https://gitcode.com/GitHub_Trending/ca/caddy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考