浏览器一片红:NET::ERR_CERT_DATE_INVALID。你的 Let’s Encrypt 证书两天前过期了,但你”几个月前配过自动续签、之后再没碰过”。挂法就是这样:截至 2026 年 6 月,Let’s Encrypt 默认证书还是 90 天有效期,自动续签任务在某次循环里静默挂了,没人看日志,到晚上 11 点你才开始救火。
最快路径: SSH 上机器,跑 sudo certbot renew --force-renewal --dry-run。这一条命令会拿 Let’s Encrypt 的 staging 服务器复现失败(不消耗任何限速配额),并打印出续签失败的确切原因:challenge 被挡、DNS token 过期、磁盘满、或者 hook 没重载 web server。读那段输出,只修它指出的那一个错,然后去掉 --dry-run 重跑一遍。本文剩下的部分就是按原因给修法,以及怎么让它别再静默地挂。
续签任务有三个独立的故障面(调度器、challenge 机制、deploy hook),任意一个坏了都够让证书悄悄过期。
你属于哪一类?
先跑下一节的诊断命令,再把现象对到原因上。绝大多数故障都是下面这六类之一。
| 你看到的现象 | 最可能的原因 | 跳到 |
|---|---|---|
systemctl list-timers 看不到 certbot.timer;journalctl 好几个月是空的 | 调度器根本没跑 | 原因 1 / 第 4 步 |
curl 访问 .well-known/acme-challenge/ 返回 301/401/403,不是 404 | HTTP-01 challenge 被重定向、WAF 或认证挡了 | 原因 2 / 第 2 步 |
curl 在 80 端口超时或连接被拒 | 防火墙 / 安全组关了 80 端口 | 原因 2 / 第 2 步 |
日志里有 DNS API 的 403 Forbidden 或 Authentication error | DNS-01 token 过期或被轮换 | 原因 3 / 第 3 步 |
日志里有 No space left on device,或文件系统是 ro | 磁盘满 / /etc 只读 | 原因 4 |
磁盘上证书是新的,但 s_client 还显示旧日期 | deploy hook 没重载服务 | 原因 5 / 第 5 步 |
日志里有 rateLimited 或 too many failed authorizations | 反复重试把限速撞了 | 原因 6 / FAQ |
常见原因
按事故复盘的频次排序。
1. cron / systemd timer 根本就没跑
Certbot 的续签一般跑在 cron、systemd timer,或包自带的 unit 里。重启过、升级过 OS、或者 systemctl mask 过之后,timer 被关了,几个月没人发现。
怎么判断:systemctl list-timers | grep certbot 看不到激活的 timer,或者 journalctl -u certbot.timer --since '90 days ago' 是空的。crontab -l 和 /etc/cron.d/certbot 没了或被注释掉。
2. HTTP-01 challenge 现在到不了 .well-known/acme-challenge/
你加了 CDN、WAF、auth_basic、或者一条”强制 HTTPS”的 rewrite 规则,把 /.well-known/acme-challenge/* 拦了——返回 401 / 403 / 重定向。一个很常见的静默版本::80 server 块上的 return 301 https://... 重定向,会在 challenge 到达 token 文件之前就把它弹走。Let’s Encrypt 是在 80 端口走纯 HTTP 验证的,所以这条路径上的任何重定向都会破坏它。另一个常见版本:云防火墙、安全组或 ufw 规则把入站 80 端口整个关了,验证请求根本连不上。两种情况下 Let’s Encrypt 都拿不到 token,续签就挂。
怎么判断:curl -I http://yourdomain.com/.well-known/acme-challenge/test 返回不是 404 的任何东西。401 / 403 / 301 说明前面有代理或重定向在拦截;连接超时或 “connection refused” 说明 80 端口被防火墙挡了。
3. DNS-01 challenge 的 API 凭据过期或轮换了
你用 DNS 插件(Cloudflare / Route 53 / Google Cloud DNS)做 challenge,那个 API token 有过期时间,或者你轮换了 key,或者 IAM 策略变了——但 certbot 配置里还是旧凭据。
怎么判断:/var/log/letsencrypt/letsencrypt.log 里有 DNS API 的 403 Forbidden 或 Authentication error。
4. 磁盘满 / 文件系统只读,续签写不了
Certbot 要写 /etc/letsencrypt/live/、/etc/letsencrypt/archive/,还要写临时文件。分区满了、或者系统进入降级模式 /etc 是只读,续签直接挂。
怎么判断:df -h /etc 显示 100%,或 mount | grep ' / ' 看到 (ro,...)。certbot 日志里有 OSError: [Errno 28] No space left on device。
5. 证书续了但 deploy hook 没重载 nginx / haproxy
磁盘上的证书是新的——openssl x509 -in fullchain.pem -noout -dates 看到新过期时间——但跑着的 web server 还在内存里抱着旧证书,因为 post-renewal hook(--deploy-hook 'systemctl reload nginx')没配或失败了。
怎么判断:fullchain.pem 的 mtime 是新的,但 openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 还在送旧证书。
6. 反复失败把 rate limit 撞了
如果坏掉的 cron 一遍遍重试,Let’s Encrypt 的限速就会启动。截至 2026 年 6 月,最容易撞到这里的两条是:每个 identifier 每小时 5 次授权失败(每 12 分钟回补 1 次)和 每个注册域名每 7 天 50 张证书(每约 202 分钟回补 1 次)。一旦撞上,连正确的续签尝试也会被挡,直到窗口清空。
一个重要细节:通过 ARI(ACME Renewal Information)对已有证书做的真正续签,截至 2026 年是 不受任何限速约束 的,所以配置正确的 Certbot 很少会撞到这些限制。只有当脚本配错、一直在请求全新订单而不是续签,或者反复死磕一个坏掉的 challenge 时,你才会撞上。
怎么判断:日志里有 urn:ietf:params:acme:error:rateLimited 或 too many failed authorizations recently。
开始前
- 记下证书过期了多少小时 / 天——决定用户影响和处置紧迫度。
- 看清楚证书工具是哪个:certbot、acme.sh、caddy 自带、cert-manager(k8s)、Vercel / Netlify / Cloudflare 托管。
- 在跑续签的机器上有 shell / sudo 权限。
- 准备一个 HTTPS 备用方案:Cloudflare 代理挂在前面(提供边缘证书),或手动单次签一张。
需要收集的信息
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -dates -subject输出。ls -la /etc/letsencrypt/live/yourdomain.com/输出。/var/log/letsencrypt/letsencrypt.log最后 200 行。systemctl status certbot.timer和systemctl list-timers --all | grep cert。crontab -l和cat /etc/cron.d/certbot(或等价文件)。- 前面挂了什么:CDN、WAF、负载均衡。
一步步修
顺序:先把 HTTPS 拉回来,再把自动化修对。
第 1 步:手动跑一次续签,拿到真实错误
sudo certbot renew --force-renewal --dry-run
--dry-run 走 Let’s Encrypt 的 staging 环境,不消耗你的限速配额。输出会清楚告诉你哪里挂了——challenge 拉取、DNS API、hook、文件系统。逐行读。
dry-run 通过:
sudo certbot renew --force-renewal
dry-run 失败:先修这个错,别先消耗生产配额。
第 2 步:HTTP-01 challenge 被挡的情况
直接测 challenge 路径:
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
echo "test123" | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/test
curl -I http://yourdomain.com/.well-known/acme-challenge/test
要的是 HTTP/1.1 200 OK,纯 HTTP。要是重定向到 HTTPS 或 401,找到对应的 nginx location / WAF 规则,把 /.well-known/acme-challenge/ 从认证和重定向里 exempt 出来:
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
allow all;
auth_basic off;
}
放在 :80 server 块里 return 301 https://... 之前。
要是 curl 反而超时或报 “connection refused”,问题就出在 80 端口本身,不是 nginx。检查每一层入站 TCP 80 是否放开:主机防火墙(sudo ufw status,找 80/tcp ALLOW)、云上的安全组 / 网络 ACL、以及上游负载均衡。Let’s Encrypt 的 HTTP-01 验证器是从公网连 80 端口的;如果某次安全组改动把它关了,即使你 443 上的 HTTPS 站点还正常,续签照样会挂。
第 3 步:DNS-01 challenge 凭据过期的情况
重新生成 API token(Cloudflare、Route 53……),更新凭据文件:
sudo nano /etc/letsencrypt/cloudflare.ini
# dns_cloudflare_api_token = NEW_TOKEN_HERE
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
重跑续签。这个 API token 要是同时给 Terraform / 监控用的,一起更新,免得修一个坏另一个。
第 4 步:把续签 timer / cron 修回来
大多数系统用的是 Certbot 默认装好的 systemd timer。重新启用它:
sudo systemctl enable --now certbot.timer
systemctl list-timers | grep certbot
应该能看到一个 12 小时以内的 Next 时间。certbot timer 一天跑两次;每次会续签任何距过期 30 天以内的证书。在 Certbot 4.1.0 或更新版本上,它还会查 ACME Renewal Information(ARI)端点,所以在 Let’s Encrypt 建议时可以更早续(比如遇到事故或证书 profile 切换)。用 certbot --version 看版本;低于 4.1 的话升级一下,这样才有 ARI 和不受限速约束的续签。
挂重载最干净的做法是 deploy hook 文件,不是行内 flag。timer 不认行内的 --deploy-hook 参数,所以应该往 hook 目录里放脚本:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/bash
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
/etc/letsencrypt/renewal-hooks/deploy/ 里的脚本会在任何一次成功续签之后自动跑,不管是 timer 触发还是手动跑。nginx -t 这道保险意味着配置坏了就中止重载,而不是把站点弄挂。
如果你确实是从 cron 而不是 timer 跑续签,行内写法在那里是有效的:
sudo crontab -e -u root
加:
0 3,15 * * * certbot renew --quiet --deploy-hook "nginx -t && systemctl reload nginx"
--deploy-hook 很关键:只有真签了新证书时才重载 nginx,不会无端 flap。
第 5 步:用新证书重载 web server
sudo systemctl reload nginx # 或 haproxy / apache2 / caddy
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -dates
默认 classic profile 下 notAfter= 应该到大约 90 天后(如果你切到了新的 tlsserver profile 大约 45 天,shortlived profile 则约 6 天)。磁盘是新的、s_client 还看到旧日期,说明 web server 还抓着文件——reload(不是 restart)应该会重读,不重读就 restart。
第 6 步:加主动监控,下次别再静默挂
sudo crontab -e -u root
加一条检查:证书距过期不到 25 天就告警:
0 9 * * * /usr/local/bin/cert-expiry-check.sh
cert-expiry-check.sh:
#!/bin/bash
DOMAIN="yourdomain.com"
END=$(echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
END_EPOCH=$(date -d "${END}" +%s)
NOW_EPOCH=$(date +%s)
DAYS=$(( (END_EPOCH - NOW_EPOCH) / 86400 ))
if [ "${DAYS}" -lt 25 ]; then
echo "Cert for ${DOMAIN} expires in ${DAYS} days" | mail -s "CERT WARN" you@example.com
fi
25 天提前 = 真正过期前还有 35 天缓冲,可以从容修。
怎么确认修好了
- 浏览器开
https://yourdomain.com无警告(硬刷新;Chrome 会把坏证书状态缓存几分钟)。 openssl s_client -connect yourdomain.com:443 -servername yourdomain.com返回的证书notAfter是个新日期(默认 profile 下距今约 90 天)。sudo certbot certificates把证书列为VALID、过期时间是新的,没有INVALID/EXPIRED行。systemctl list-timers | grep certbot显示 timer 激活、Next时间在未来约 12 小时内。sudo certbot renew --dry-run退出码 0(应打印 “Cert not yet due for renewal” 或 “Congratulations, all simulated renewals succeeded”)。- 监控(上面第 6 步的脚本、或 UptimeRobot / Better Stack 这类外部服务)已经在跟新的过期日期。
长期预防
- 续签一定要配 deploy hook(timer 用
/etc/letsencrypt/renewal-hooks/deploy/里的脚本,cron 用--deploy-hook)。否则证书在磁盘是新的、进程在送旧的。 - 把 Certbot 保持在 4.1.0 或更新,这样能用上 ARI 驱动的续签——它不受限速约束,并且在 Let’s Encrypt 建议时会提前续。证书有效期越短这点越重要:默认 90 天证书计划在 2027 年初降到 64 天、2028 年降到 45 天,续签频率翻倍。
- 加外部证书过期监控(UptimeRobot / Better Stack / Datadog / Pingdom),跑在签发机以外的地方,不要跑在签证书的同一台机器上。注意 Let’s Encrypt 已于 2025 年 6 月停发过期提醒邮件,所以不能再指望它的提醒了。监控现在是必需项,不是可选项。
- OS 升级之后立刻确认
certbot.timer还激活着——升级有时候会关掉三方 timer。 - 在 runbook 里写清续签流程,下一个值班的人 5 分钟修好、不是 5 小时。
常见坑
- 底层问题(challenge 被挡)没修,反复
certbot renew来”逼它过”——把限速配额烧光,一周内都签不出来。 - 以为平台自动续签,结果你早就关了托管证书自己签了。Vercel / Netlify 这类只续他们自己签的。
- 续了证书但忘了
systemctl reload nginx——磁盘新、送的旧。 - nginx 已经占了 :80 还在 cron 里用
--standalone,端口冲突直接挂。 - 续签监控跑在签证书的同一台机器上——机器挂了监控也挂了。永远外部监控。
FAQ
Q:证书刚过期,用户会丢数据 / 会话吗?
服务端会话不丢,但每个用户都看到浏览器警告。强证书 pinning 的移动 app 可能直接断。先恢复 HTTPS、再担心会话。
Q:能快速从别的 CA 签一张顶上吗?
可以——ZeroSSL、Buypass、Sectigo 都支持 ACME。配置里加第二个 issuer。也可以把站点挂到 Cloudflare 代理后面,几分钟(一个 DNS 改动)就拿到边缘证书顶住,同时再修 Let’s Encrypt。
Q:限速撞了,多久才能再签?
截至 2026 年 6 月,每个注册域名 50 张的限制是滚动 7 天窗口,每约 202 分钟回补 1 个名额。每个 identifier 5 次授权失败的限制每 12 分钟回补 1 个,所以单个错误修好后大约这么久就能放出来。在等生产限速清空时,用 staging(certbot renew --test-cert 或 --dry-run)确认你的修复。还要记住:对已有证书的 ARI 协调续签完全不受这些限制约束,所以 Certbot 4.1+ 正常续签起来后,你应该不会再撞到。
Q:certbot 续得够早吗?它到底什么时候触发?
timer 一天查两次,会续任何距过期 30 天以内的证书。配上 ARI(Certbot 4.1.0+)后,在 Let’s Encrypt 公布续签窗口时它能更早续。所以你今天把自动化修好,下一张有效证书会在新证书过期之前很久就续上。你不需要按计划去强制续签。
Q:换成 1 年期证书避免续签问题,行不行?
行不通。公共 CA 已经不签长于 200 天的证书(按 CA/Browser Forum 规则,2026 至 2029 年逐步收紧),而 Let’s Encrypt 在往反方向走:默认现在 90 天,2027 年初降到 64 天、2028 年降到 45 天,还有一个可选的 6 天 shortlived profile 已经正式可用。真正的解法是可靠的自动化加监控,不是更长的证书。