结论:SSL 证书续期要做成“自动续期 + 到期日监控告警 + 上线前演练”三层防护,只部署 certbot/acme.sh 并不够。证书到期属于运维事故,通常是因为续期任务静默失败,且没有人监控到期时间。Let's Encrypt 的免费证书有效期为 90 天,当剩余有效期不足 30 天时就应完成续期;监控告警的阈值建议设置在剩余 21 天。
为什么自动续期了还需要监控?
结论:自动续期只能降低人为续期的成本,无法防止“续期任务失败却没人发现”。常见失败原因包括:验证端口被封、webroot 权限不足、reload 命令没执行、系统时间漂移、域名解析变更;这些都可能让续期静默失败。
Let's Encrypt 签发的证书有效期为 90 天,官方建议在剩余 30 天内续期,客户端默认也是到期前 30 天开始续期。这意味着一次续期失败不会立刻导致服务中断,但连续失败约 3 次就会导致证书过期。监控的本质是给自动续期留出人工介入的缓冲时间:当剩余 21 天且仍未续期时发出告警,可以立刻排查,而不必在证书到期前几小时才处理。
用 certbot 做自动续期怎么做?
结论:用系统自带的 systemd timer 或 cron 调用 certbot renew,并通过 --deploy-hook 在续期成功后自动 reload Web 服务,这是最简且可靠的方案。
首次签发示例(Nginx + webroot,HTTP-01 验证指 CA 回源访问 /.well-known/acme-challenge/ 下的验证文件):
certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
之后续期不用重复指定域名,一条命令全部处理:
certbot renew --dry-run # 先演练,不消耗签发配额
certbot renew --quiet --deploy-hook "systemctl reload nginx"
用 cron 定时(避开整点,加随机分钟,并写绝对路径):
17 3,15 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
注意两点:--deploy-hook 只在真正续期成功时触发,--dry-run 不触发,所以不能拿 dry-run 验证 reload 是否生效;renew 只会处理距到期不足 30 天的证书,不是每次执行都重新签发。如果用 certbot 的系统包,通常已自带 certbot.timer,用 systemctl list-timers | grep certbot 确认,不要和 cron 重复配置。
用 acme.sh 做自动续期和通配符证书怎么做?
结论:acme.sh 安装时会自动写入 cron,且必须显式执行 --install-cert 把证书复制到 Web 目录并绑定 reload 命令,否则续期成功但线上证书不变。
acme.sh --issue -d example.com -d www.example.com --webroot /var/www/html
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/example.com.key \
--fullchain-file /etc/nginx/ssl/example.com.fullchain.pem \
--reloadcmd "systemctl reload nginx"
使用通配符证书时,必须采用 DNS-01 验证,即在 DNS 中添加一条 TXT 记录,例如阿里云:
acme.sh --issue --dns dns_ali -d example.com -d '*.example.com'
fullchain.pem 是服务器证书加中间证书的合并文件;Nginx 的 ssl_certificate 必须指向它,否则部分安卓和旧客户端会报证书链不完整。
怎么监控证书到期并告警?
结论:监控应检查线上 443 端口实际生效的证书,而不是本地文件,因为本地文件新、线上没 reload 是典型故障。核心指标是“剩余天数”。
命令行快速检查:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate -subject
Prometheus 体系用 blackbox_exporter 的 https_2xx 模块,它暴露 probe_ssl_earliest_cert_expiry(Unix 时间戳),告警规则:
(probe_ssl_earliest_cert_expiry - time()) / 86400 < 21
没有 Prometheus 的,用一段 shell 脚本 + cron 每天跑一次,剩余天数小于 21 就走邮件/钉钉/企业微信 Webhook 告警。另外建议监控“证书指纹变更”:本地 fullchain.pem 的 SHA256 指纹和线上取到的指纹不一致,说明 reload 没生效,这类问题到期告警是发现不了的。
自动续期最常见的失败原因有哪些?
结论:80% 的续期失败集中在验证和重载两个环节。
- CDN/WAF 拦截 HTTP-01:域名走 CDN 后,
/.well-known/acme-challenge/ 请求被打到节点,源站收不到验证请求。解决办法是改用 DNS-01,或临时回源。
- webroot 权限:Nginx 运行用户读不到目录,验证文件返回 403。确认目录权限和
location ^~ /.well-known/acme-challenge/ 未被重写规则吞掉。
- reload 命令未执行:ctrbot 的 hook 里写了
nginx -s reload 但 PATH 缺失导致失败,一律写绝对路径。
- 系统时间漂移:服务器时间偏差过大,证书会被判定为未生效或已过期,启用 NTP 同步。
- 多域名单点失效:一张证书带 5 个域名,其中 1 个解析已失效,整单续期失败。定期清理不用的域名。
上线前怎么验证这套流程是有效的?
结论:三步验证:先 dry-run,再强制续期一次,最后比对线上指纹。
第一步 certbot renew --dry-run 走完整个验证流程;第二步用 --force-renewal 或 acme.sh 的 --renew --force 真实续期一次,观察 reload 是否执行、systemctl status nginx 是否正常;第三步分别取本地和线上的 SHA256 指纹对比,一致才算闭环。之后每季度抽查一次即可,其余交给到期告警兜底。
把这条链路固定下来:90 天有效期的证书、到期前 30 天自动续期、剩余 21 天告警、上线前演练一次。证书到期这类事故,靠的不是运气,而是“续期会失败”这个前提下的监控设计。