上个月底有几个站友在群里问同一个问题:我的证书是开过自动续期的,怎么今天访问自己的站点突然变成502了,浏览器报「ERR_CERT_DATE_INVALID」。这种情况十有八九不是站点被打了,而是证书到点过期,而自动续期这条链路在某个环节早就断了,只是平时没人去查,直到过期那天才暴露出来。
如果你手上的服务器也是一台机器扛着几个站、证书全靠脚本续着,可以先看看这两家的机器再做规划:RackNerd(全球VPS,特价常推)、狗云DOGYUN(高性价比云服务器,多机房可选),挑一台带宽和线路都够用的做主站,剩下的挂到后面做备用,比所有业务都压在一台机器上要踏实得多。


一、ACME到底在做什么:把「发证」拆成两个角色
要理解续期为什么失败,先得知道 ACME 这套协议是怎么跑起来的。Let's Encrypt 这类免费证书机构走的都是 ACME 协议,全称是 Automatic Certificate Management Environment,翻译过来就是「自动证书管理环境」。它把发证这件事拆成了两个角色:
一个是 ACME 客户端,也就是你服务器上装的 certbot、acme.sh 或者lego 这些命令行工具;另一个是 CA 服务器,也就是给你发证书的那个机构。
流程大致是这样:客户端先向 CA 注册一个账号(用邮箱),然后发起一次「订单」,说明我要申请一个 www.example.com 的证书;CA 收到订单后不会直接发证,而是要验证「你真的控制这个域名」。验证通过后,CA 才把私钥和证书链返回给你。整个过程走的是 HTTPS 接口,每一次交互都有签名证明「请求确实来自这个账号」,所以不需要把你的 DNS 服务商密钥交出去,也不需要你手工去点确认。
关键在于:验证环节会写一个「挑战」(challenge),客户端必须按要求在域名上摆出指定内容,CA 那边来读。HTTP-01 是让你在 80 端口的 /.well-known/acme-challenge/ 路径下放一个文件;DNS-01 是让你在 DNS 里加一条 TXT 记录。前者简单直接,后者能泛域名、能给内网或走 CDN 的站用,代价是需要能动 DNS。

二、为什么「cron 跑了」证书还是过期
这里是最容易误导人的地方。很多人加了定时任务,看日志里 certbot 也确实输出了「Congratulations! Your certificate has been saved」,就认为万事大吉。但续期链条上至少有三个环节,任何一环断了,页面上的证书纹丝不动。
第一环:定时任务真的被调度了。 在 Linux 上 cron 是有服务名的(cronie / crond),容器环境里更常见的是根本没起 cron 或者没挂载 crontab 文件。你在终端手动跑一遍当然成功,但那个「每天两点跑」的定时任务可能压根没被触发。验证办法很直接:去系统日志里翻 cron 的执行记录,或者在脚本第一行写一行时间戳,跑到日志文件里看看今天有没有新行。另外注意 crontab 里的 % 需要写成百分号加反斜杠,命令里带百分号又不转义,整条任务会直接报错。
第二环:验证环节通不通。 也就是上面说的 HTTP-01 或 DNS-01 是否成功。用 certbot renew --dry-run 可以跑一遍完整的流程而不消耗额度,它是排查续期问题最该先敲的命令。HTTP-01 常见的坑是:80 端口被防火墙拦了、服务器上有个反代把 /.well-known/ 路径劫持走了、Nginx 没监 80、或者域名已经套了 CDN 回源到内网导致 CA 读不到。DNS-01 常见的坑是 DNS 服务商的 API 密钥过期、脚本没装对应 provider 的插件、或者 TXT 记录还没生效就发起了验证。
第三环:续期完成之后,新证书有没有被服务加载。 这是最隐蔽的一段。certbot 续期成功后通常会在 Debian 系上装一个 systemd timer,在 CentOS 系上装 cron 任务,脚本里一般带 --deploy-hook 或者 certbot renew && systemctl reload nginx。但如果这个 hook 没写、或者写的是 reload 而 Nginx 编译成了别的名字、或者 systemd 的 timer 服务名写错、或者 reload 时报错但没人看,新证书就只是躺在 /etc/letsencrypt/live/ 目录里,Nginx 内存里跑的还是旧的那张。查证方式很朴素:openssl x509 -in /etc/letsencrypt/live/你的域名/fullchain.pem -noout -enddate 看文件里的到期日,再 echo | openssl s_client -connect 你的域名:443 2>/dev/null | openssl x509 -noout -enddate 看线上服务吐出来的到期日,两个日期对不上,就是没加载成功。

三、续期本身也不是每次都成功
另外一个容易被忽略的事实是:certbot 并不是无限续的。Let's Encrypt 的证书有效期是 90 天,acme.sh 默认走的是 60 天,续期成功的证书在 CA 侧还有「每张证书每周续期次数上限」这类风控限制。如果你的脚本写成了一分钟跑一次,或者每次跑都强制 --force-renewal,很容易在短时间内反复申请,把额度耗掉后直接拿不到新证书。所以正确的做法是交给定时任务低频执行,比如每天跑一次,certbot 自己会判断「还没到续期窗口」就什么都不做。
另外一个细节:certbot renew 只认已经登记在册的证书配置,它读的是 /etc/letsencrypt/renewal/ 目录下的配置文件。如果你当初是用 certonly 申请的证书、后来把配置文件删了或者容器重建导致丢失,renew 就找不到对应的条目,永远是「0 renewed, 0 total」,看起来一切正常,其实什么都没做。
四、我的排查顺序,你可以照着走一遍
结合上面的链路,我现在碰到证书问题基本是按这个顺序查的:
第一步,certbot certificates 看 CA 侧记录的到期日,这是「应该」什么时候过期;第二步,openssl s_client 看线上实际吐出来的到期日,这是「实际」什么时候过期,两个一对就知道是续期没跑还是加载没生效;第三步,certbot renew --dry-run 跑一遍,把验证环节的错误抓出来;第四步,去翻 cron 或 systemd timer 的日志,确认调度器到底有没有执行;第五步,加一条监控,别等证书过期了才知道。
最后那一步其实是最值的。证书有效期是可以被监控的,把它接进你现有的告警体系,剩余天数低于 20 天就发一条通知,比任何排查都省事。顺带一提,监控本身也要留个心眼:服务器时间如果不同步,脚本判断「距离到期还有多久」就会算错,这一环我在另一篇讲时钟同步的文章里详细写过。

五、写在最后
证书自动续期这件事,配置的时候只要五分钟,但坏掉的时候通常是在没人值班的那个凌晨。真正让人安心的不是把 certbot 装上,而是把「定时任务触发、验证通过、证书加载、生效确认」这四个点都验证过一遍,并且给它配上一条到期提醒。
如果你也在用 certbot 或者 acme.sh,欢迎在评论区说说你的续期方式是什么,我是按 Debian 系 + Nginx 的组合在跑,CentOS 系的 systemd timer 那套以后再单独写一篇。


喜欢
顶
难过
囧
围观
无聊



