五月的某个周日早上,我被 Bark 的告警推醒:ziwen.io 的证书还有零小时过期。严格说不算"还有",是已经过期了。挂上小绿锁的十几个服务集体变脸,浏览器一片红色警告。最有意思的是,最先报警的不是证书监控,而是探活——所有 HTTPS 探针在同一分钟全部失败,监控面板上像放了一排烟花。
事后查因并不复杂:我在这台机器上用过 certbot 的 HTTP-01 续期,后来整理 nginx 配置时顺手删掉了一个看似无用的 location 块——那正是 /.well-known/acme-challenge/ 的入口。续期任务连续失败,失败通知邮件躺在服务器本地的 mailbox 里,没有任何人读。自动化最危险的状态就是"它以前是好的"。
三种挑战方式,三种失败模式
ACME 协议验证域名所有权的方式主要有三种,各自的死法不一样:
- HTTP-01:在 80 端口放一个临时文件让 CA 来取。简单直接,但要求 80 端口可从公网直达,且不能签发 wildcard 证书。我的事故就是它——nginx 配置任何一次重构都可能顺手把它埋了。
- DNS-01:往 DNS 里写一条
_acme-challengeTXT 记录。唯一能签 wildcard 的方式,不依赖 Web 服务器,对藏在反代后面的服务也有效。代价是要把 DNS 服务商的 API token 交给 ACME 客户端,token 泄露等于域名解析权泄露,权限要收到最小。 - TLS-ALPN-01:在 443 端口用 ALPN 扩展完成握手验证。不需要碰 80 端口,适合纯 TLS 环境,但同样不支持 wildcard,而且要求客户端能直接独占 443,和现有站点共存时比较别扭。
我的结论很朴素:单域名、单一入口、80 端口稳定暴露的场景用 HTTP-01 就够;只要涉及 wildcard、多台机器、或者服务藏在 CDN 后面,就直接上 DNS-01,别犹豫。TLS-ALPN-01 我至今没有非用不可的场景。
现在的方案:一张 wildcard,集中签发
三台云厂商、六台 VPS、十几个服务,如果每台机器各自跑一套续期,那就是六份定时炸弹。现在的做法是集中化:用 Certimate 在一台机器上统一走 DNS-01 签发 *.ziwen.io,再通过它自带的部署功能把证书推到各台 VPS 和 CDN。签发和分发是两条独立的流水线,各自有日志和失败通知,不再依赖某台服务器上某个没人看的 cron 输出。
顺带把 CAA 记录补上了,把能给我签证书的 CA 收窄到实际使用的那一家。它不防所有问题,但成本是零:
ziwen.io. CAA 0 issue "letsencrypt.org"
ziwen.io. CAA 0 iodef "mailto:admin@ziwen.io"复盘留下来的三条规矩
事故本身五分钟就修好了,真正值钱的是复盘。我给自己定了三条规矩:第一,续期失败的通知必须和探活告警走同一条通道,发进我真正会看的推送里,而不是服务器的本地邮箱;第二,除了"续期成功"之外,还要有一个独立的证书有效期监控,从外部探测,剩余天数小于 14 就告警——续期系统和监控不能是同一套东西,否则它死的时候也是安静的;第三,任何涉及 nginx 的改动,改完跑一次 certbot renew --dry-run 再收工。
证书这东西,平时毫无存在感,出事就是全站级别的面子工程崩塌。基础设施的浪漫就在于:把它调到足够无聊,然后忘记它——前提是你确定,它出问题的时候一定会有人踹门叫醒你。