Let's Encrypt 签发九十天的证书并自动续期,正因如此人们在配置好一周后就不再想它了。麻烦在于自动续期是悄无声息地坏掉的:没人看到报错,而三个月后站点用浏览器警告迎接访客。从 2025 年起 Let's Encrypt 也不再发送到期通知邮件,于是最后一个来自外部的提醒也没了。
下面是续期停止工作的各种原因,按出现频率递减排列。
1. 证书续了,而服务没有重新读取它
最常见的一种。certbot renew 跑过了,新文件在磁盘上,而 nginx 或 Apache 仍把旧的留在内存里——并且在配置被重载之前继续把它发给访客。形式上一切正常;实际上站点用的是一张过期证书。
解法是在续期成功之后运行的一个 hook:
sudo certbot renew --deploy-hook "systemctl reload nginx"
它只需写进续期文件一次(/etc/letsencrypt/renewal/example.com.conf,[renewalparams] 段,renew_hook 一行),此后就自己起作用了。
2. Web 服务器变了,而 challenge 没变
HTTP-01 challenge 要求通过普通 HTTP 提供 /.well-known/acme-challenge/ 下的一个文件。它通常这样被弄坏:
- 配置里加了从 HTTP 到 HTTPS 的无条件跳转——challenge 跟着跳转走了,于是失败;
- 出现了带
deny all的location ~ /\.规则,把所有带点的目录都关上了,其中包括.well-known; - 站点根目录挪了位置,而 certbot 的设置里仍留着旧的
--webroot-path; - 打开了按国家的封锁或基本认证,把验证服务器连同所有人一起拦住了。
配置里正确的顺序是:先给 /.well-known/acme-challenge/ 单独的 location,然后才是通用跳转。
3. 计时器被关了
续期由 systemd 计时器或 cron 任务运行,而两者都可能在排障时被误关,或者在迁移服务器时丢失。
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
第二条命令会在测试模式下走完整个续期流程,而不消耗你的配额。这是唯一一项能提前回答「一个月后它还会不会续」的检查,而不是事后才知道。
4. 某个域名不再指向这里
证书覆盖了五个域名,其中一个搬去了别的主机,DNS 变了。那个域名的验证不再通过,于是 certbot 不会续整张证书——连同那四个还在工作的域名一起。多余的域名应当从证书里去掉,而不是连同提示一起被无视。
5. 你撞上了限额
Let's Encrypt 限制每个域名每周的证书数量。这通常是排障时「没成,再试一次」这个循环的结果。做实验有 staging 服务器(--dry-run 或 --test-cert),而它应当在配额耗尽之前用,而不是之后。
检查被发出去的东西,而不是被存起来的东西
任何基于磁盘文件的检查都有一个缺陷:文件可以是新的,而访客拿到的却是旧证书。请从客户端那一侧看:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer
-servername 这个选项是强制的:没有它,在一台有多个站点的服务器上你拿到的会是排在最前面的那个虚拟主机的证书,而不是你要的那个。输出里的 notAfter 就是真正的到期日期。
顺便把证书链也检查一下:
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
如果只有一张证书,那说明中间证书没有被发出去。浏览器通常会原谅这种配置——它们自己会补出链条——但 curl、移动应用和 Java 客户端会验证失败。这正是经典的「我这儿打得开,客户那儿就是不行」。
提前多少天预警
三十天是合理的第一次预警:certbot 开始尝试续期时剩下的也正是这么多,所以如果到那时还什么都没发生,说明机制已经坏了。十四天是第二次预警,那已经要求今天就动手。七天则是紧急状态。
要紧的是把所有证书一起盯着,包括那些不是来自 Let's Encrypt 的:按年买的那些被遗忘得最彻底,因为一年之内所有人既忘了它们存在,也忘了通知发到谁的邮箱。一个列出域名、日期和剩余天数的页面,能把这个问题彻底了结——就像下面的演示那样。