
周一早上打开网站,浏览器突然弹出”您的连接不是私密连接”,订单提醒邮件停在半小时前——这种场景多数不是因为服务器被入侵,而是 SSL(安全传输协议)证书自动续期失败了。证书自动续期失败几乎不会主动报错,它安静地躺在日志里,直到最后一刻才让整站失去信任标识。这篇文章教你如何排查 certbot 续期失败、用钩子让续期真正生效,并搭建一套到期告警,把问题拦在证书过期之前。
先搞清楚:续期失败为什么总是悄无声息
certbot 安装证书时会自动写入两个定时任务:systemd 系统上的 certbot.timer(每天随机两次触发 certbot renew),以及传统系统上的 /etc/cron.d/certbot。每次 renew 只会续期”距今 30 天内到期”的证书,Let’s Encrypt 证书有效期 90 天,也就是大约第 60 天起进入续期窗口。
这套机制的问题在于它”静默失败”:定时任务在后台执行,HTTP 验证失败只会写进 /var/log/letsencrypt/letsencrypt.log,服务器不会弹任何提示。更危险的是 Let’s Encrypt 会在证书到期前 20 天、10 天、最后 1 天分别发提醒邮件到注册邮箱——如果当初填的是不常看的邮箱,或者邮件进了垃圾箱,站点就会毫无征兆地掉回”不安全”状态。所以排障的第一步不是改配置,而是先把失败证据找出来。

第一步:三条命令确认续期是否真的失败
先看证书还剩几天,再看失败原因。以下命令在 Ubuntu 22.04 与 Debian 12 上验证可用:
certbot certificates
输出里关注 Expiry Date 字段,日期小于 30 天说明续期没跑成功;再看 Certificate Path,等下排障要用。
certbot renew --dry-run
这是官方推荐的模拟续期命令,它会向 Let’s Encrypt 的 staging 环境申请一张测试证书,不消耗正式配额(每个域名每周最多 5 张正式证书,直接反复 renew 正式证书会触发限速)。命令输出会明确到每个域名的模拟结果是 success 还是 fail。
tail -n 100 /var/log/letsencrypt/letsencrypt.log
日志里搜 urllib3 或 Connection refused 开头的段落,能直接看到验证请求卡在了哪一步。三条命令跑完,失败原因基本锁定在下面四类里。
四类高频失败原因与对应修法
第一类:80 端口验证被占用或被防火墙拦截。 HTTP-01 验证要求 Let’s Encrypt 能访问 http://你的域名/.well-known/acme-challenge/,这一步走的是 80 端口——即使你的站点是纯 HTTPS,80 端口也必须放行。云服务器(云端托管的虚拟服务器)先检查安全组是否放行 80 入站;服务器本机用 ss -tlnp | grep :80 确认是 Nginx 还是 Apache 在监听。如果两者都没问题,还要检查网站配置里有没有把 /.well-known/acme-challenge/ 重定向到 HTTPS 时改写了路径。
第二类:DNS(域名解析系统)解析不再指向这台服务器。 站点迁移后忘了改回解析、或者换了 CDN(内容分发网络)代理模式,验证请求就会打到别的机器上。用 dig +short 你的域名 核对返回的 IP 是否是本机公网 IP。如果域名走了 CDN 代理,要么临时切回 DNS only 模式完成验证,要么改用 DNS-01 验证方式。
第三类:证书目录与 web 服务配置漂移。 手动挪过 /etc/letsencrypt/ 目录、换过 certbot 版本,会出现 renew 找不到 renewal 配置的情况。检查 /etc/letsencrypt/ 下的续期配置目录中是否有对应域名的 .conf 文件,缺失的话用 certbot reconfigure 或按原参数重新签发一次即可恢复。
第四类:续期成功了,但 web 服务没加载新证书。 这是被忽略最多的一类:certbot renew 只负责把新证书写到磁盘,Nginx 不会自动读取。判断方法很简单——certbot certificates 显示的到期日是新的,浏览器里看到的证书却是旧的。这类问题不叫续期失败,叫”生效失败”,需要靠 deploy 钩子解决。
用 deploy 钩子让新证书真正生效
certbot 的钩子分几种,最关键的是 --deploy-hook:它只在”证书真正续期成功”后执行,正好用来重载 web 服务。对 Nginx 来说,一条命令就能永久写入钩子:
certbot reconfigure --deploy-hook "nginx -s reload"
之后每次续期成功,Nginx 都会自动加载新证书。如果服务是 systemd 管理的其他组件(例如自建的 API 网关),把命令换成 systemctl reload 你的服务名 即可。有两点容易踩坑:一是 Debian/Ubuntu 打包的 python3-certbot-nginx 插件会自动处理重载,但前提是 Nginx 配置里证书路径指向 /etc/letsencrypt/live/域名/,自定义路径就享受不到这个便利;二是钩子脚本第一次建议手动跑一遍,用 chmod +x 确认可执行权限,否则钩子失败同样只写日志、不报警。
对于有多台服务器、证书分散管理的团队,可以考虑把证书统一托管到负载均衡层,避免每台机器各自续期。我们之前在讲服务器运维基础配置时提到过类似的思路:把证书、定时任务这些”平时看不见”的组件集中化,出问题的概率会低很多。

到期告警:别把宝押在邮件上
Let’s Encrypt 的到期邮件是最后防线,不是第一防线。真正可靠的方案是自己动手监控剩余天数,按距离到期的远近分两级处理:
- 剩余 21 天以上:正常区间,每周巡检一次即可,可以纳入现有的服务器监控方案统一看板。
- 剩余 8 到 21 天:关注区间。理论上 certbot 早已完成续期,如果天数还在往下走,说明续期链路有故障,立即按上文四类原因排查。
- 剩余 7 天以内:告警区间。直接把告警推送到能吵醒人的渠道(Telegram、企业微信机器人、短信),并准备手动续期预案
certbot renew --force-renewal。
落地最简单的方式是 uptime 监控类工具的 SSL 到期检查(Uptime Kuma、各类免费拨测服务都支持),把阈值设为 14 天,低于阈值即触发通知,全程不用写代码。如果偏好命令行,一行脚本放进 cron 也能实现同样效果:
echo | openssl s_client -connect 你的域名:443 -servername 你的域名 2>/dev/null | openssl x509 -noout -enddate
输出形如 notAfter=Dec 11 23:59:59 2026 GMT,把它和当前日期做差值,再按上面的分级决定是记录还是报警。
总结与行动建议
回到最初的问题:证书自动续期失败之所以致命,不是因为它难修,而是因为它失败得毫无声音。建议你现在就做四件事,总耗时不超过 15 分钟:
- 跑一次
certbot renew --dry-run,确认当前所有证书的模拟续期结果为 success。 - 检查 deploy 钩子是否存在,Nginx 环境执行
certbot reconfigure --deploy-hook "nginx -s reload"。 - 用 openssl 命令或监控工具把证书剩余天数纳入告警,阈值 14 天起步。
- 登录 Let’s Encrypt 后台或查看注册邮箱,确认到期提醒邮件能正常收到。
如果你正在规划整站 HTTPS 或考虑把证书管理迁移到更省心的托管环境,也可以考虑 Hostease 的服务器与主机方案,证书由平台统一维护,省去每台机器单独配置钩子和告警的成本。更多建站与运维实践,欢迎持续关注我们的中文博客。