Let's Encrypt 证书为什么会静默过期
证书签发成功不等于会自动续期。梳理三个最常见的断点:定时任务没装、验证方式与凭证不匹配、续期后没有重载服务,以及一套可以照着跑的检查清单。
免费证书的有效期只有 90 天,靠的是自动续期把它变成”配好就不用管”。但自动续期是一条链,链上任何一环断了都不会立刻报错——它只会在几十天后、证书真的过期的那一刻,把整站 HTTPS 一起带走。
更糟的是,过期前没有任何提示。浏览器不会提前警告,服务端日志一片安静,监控如果只探 HTTP 状态码,也照样是 200。
断点一:定时任务根本没装
acme.sh 在安装时会往 crontab 里写一条每日任务,但前提是安装过程正常走完。用 curl | sh 装的时候如果中途报错、或者是在容器里装完又把镜像层丢掉,cron 就不会留下。certbot 走的是 systemd timer,同样可能处于 enabled 但 inactive 的状态——enabled 只说明开机会启动,不代表现在在跑。
两条命令就能确认:
crontab -l | grep acme
systemctl list-timers --all | grep -E 'certbot|acme'
如果两条都是空的,那么无论当初证书签得多成功,都不会有人去续它。
补装很简单:
~/.acme.sh/acme.sh --install-cronjob
装完记得确认 cron 守护进程本身是活的(systemctl is-active crond)。定时任务写进了 crontab、但 crond 没在跑,是另一个同样安静的失败。
断点二:续期时用的验证方式和签发时不一样
这是最隐蔽的一种。签发证书时可以在命令行里临时传 DNS 服务商的 AccessKey,用 DNS-01 完成验证;但 acme.sh 记录续期配置时,如果凭证没有写进 account.conf,续期那一刻它只知道”要用 DNS 验证”,却拿不到任何密钥,于是直接失败。
看一下每个域名自己的配置就能发现:
grep Le_Webroot ~/.acme.sh/example.com_ecc/example.com.conf
Le_Webroot='dns' 意味着续期要走 DNS API,必须有持久化的凭证;Le_Webroot='/var/www/certbot' 则是 HTTP-01 webroot 模式,续期时只要 nginx 还能对外提供那个目录就行,不依赖任何第三方密钥。
只要 80 端口对公网可达,HTTP-01 是更省心的选择——它没有外部凭证这个额外的失效点。切换方式是重新签一次:
~/.acme.sh/acme.sh --issue --force \
-d example.com -w /var/www/certbot --keylength ec-256
--force 用于覆盖还没到期的证书。签成功后原来的 Le_Webroot 就被改写成 webroot 路径,之后的自动续期不再需要 DNS 凭证。
断点三:证书续了,服务没重载
续期只是把新证书写到 ~/.acme.sh/ 下面。如果 nginx 的 ssl_certificate 指向的是 /etc/nginx/ssl/ 里的副本,而你没有配置安装动作,那么 nginx 会一直用内存里加载的旧证书,直到某次重启——通常也就是过期之后。
--install-cert 负责把这件事固化下来:
~/.acme.sh/acme.sh --install-cert -d example.com --ecc \
--fullchain-file /etc/nginx/ssl/example.com.crt \
--key-file /etc/nginx/ssl/example.com.key \
--reloadcmd "nginx -t && nginx -s reload"
这条命令会把路径和 reloadcmd 一起记进该域名的配置里,以后每次自动续期都会重新执行一遍安装和重载。
reloadcmd 里前置一个 nginx -t 是值得的:万一配置在这几个月里被人改坏了,nginx -t 会失败、reload 不会执行,服务继续用旧证书跑,而不是被一个语法错误直接掀翻。
顺带说清楚 SAN
经常有人问:已经有一张 api.example.com 的证书,能不能给裸域 example.com 复用?
不能。浏览器校验的是证书扩展里的 Subject Alternative Name,不是 CN。查一下就知道这张证书到底覆盖哪些域名:
openssl x509 -in example.com.crt -noout -text | grep -A1 "Subject Alternative Name"
只列了 DNS:api.example.com,那它就只对这一个名字有效,用在裸域上会得到 NET::ERR_CERT_COMMON_NAME_INVALID,这是硬失败,配置层面绕不过去。通配符证书 *.example.com 也帮不上忙——通配符只覆盖一级子域,不包含裸域本身。
正确做法是要么单独签一张,要么签一张 SAN 里同时列出所有名字的证书。
检查清单
给一台机器做证书体检,按这个顺序过一遍就够了:
# 1. 每张证书还剩多久
for f in /etc/nginx/ssl/*.crt; do
echo -n "$f "; openssl x509 -in "$f" -noout -enddate
done
# 2. ACME 客户端知道哪些证书、下次续期是什么时候
~/.acme.sh/acme.sh --list
# 3. 定时任务在不在
crontab -l | grep acme
# 4. 每张证书的续期方式(dns 还是 webroot)
grep -H Le_Webroot ~/.acme.sh/*/*.conf
# 5. 续期后会不会自动重载
grep -H Le_ReloadCmd ~/.acme.sh/*/*.conf
第 4 步出现 dns 而 account.conf 里没有对应凭证,第 5 步是空的,第 3 步查不到任务——这三种情况都属于”现在看起来一切正常,但到期必挂”。
别只靠清单
清单能查出问题,但查不出”三个月后才发生的问题”。真正稳妥的做法是把证书剩余天数变成监控指标:openssl s_client 从外部探测,剩余天数低于 20 天就告警。这样即使自动续期又因为某个新原因失败了,你也会在证书过期前收到通知,而不是等用户来告诉你网站打不开。