浏览器一片红:NET::ERR_CERT_DATE_INVALID。你的 Let’s Encrypt 证书两天前过期了,但你”几个月前配过自动续签、之后再没碰过”。挂法就是这样——Let’s Encrypt 证书 90 天有效期,自动续签 cron 在某次循环里静默挂了,没人看日志,到晚上 11 点你才开始救火。续签链有三个独立的故障面(cron 本身、challenge 机制、deploy hook),任意一个坏了都够让证书悄悄过期。
常见原因
按事故复盘的频次排序。
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、或者一条 rewrite 规则,把 /.well-known/acme-challenge/* 拦了——返回 401 / 403 / 重定向。Let’s Encrypt 拿不到 challenge token,续签就挂。
怎么判断:curl -I https://yourdomain.com/.well-known/acme-challenge/test 返回不是 404 的任何东西。401 / 403 / 301 都说明前面有东西拦截。
3. DNS-01 challenge 的 API 凭据过期或轮换了
你用 DNS 插件(Cloudflare / Route 53 / Google Cloud DNS)做 challenge,那个 API token 有过期时间,或者你轮换了 key,或者 IAM 策略变了——但 certbot 配置里还是旧凭据。
怎么判断:/var/log/letsencrypt/letsencrypt.log 里有 403 Forbidden 或 DNS API 的 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 在一个窗口里重试了 50 次,Let’s Encrypt 的限速会启动(每个域名每小时 5 次失败验证、每周 50 次签发)。你修好了底层问题,签发还是会被挡住。
怎么判断:日志里有 urn:ietf:params:acme:error:rateLimited 或 too many failed authorizations。
开始前
- 记下证书过期了多少小时 / 天——决定用户影响和处置紧迫度。
- 看清楚证书工具是哪个: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://... 之前。
第 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 修回来
systemd:
sudo systemctl enable --now certbot.timer
systemctl list-timers | grep certbot
应该能看到一个 12 小时以内的 Next 时间。certbot timer 一般一天两次。
cron:
sudo crontab -e -u root
加:
0 3,15 * * * certbot renew --quiet --deploy-hook "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
notAfter= 应该到 89 天后。磁盘是新的、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无警告。 openssl s_client -connect yourdomain.com:443 -servername yourdomain.com返回的证书notAfter距今 89 天左右。systemctl list-timers | grep certbot显示 timer 激活、有未来运行时间。certbot renew --dry-run退出码 0。- 监控(上面的脚本、或 UptimeRobot / Better Stack 这类外部服务)已经在跟新过期日期。
长期预防
certbot renew一定要配--deploy-hook,否则证书在磁盘是新的、进程在送旧的。- 注册 Let’s Encrypt 账户邮箱要用真的会看的邮箱——他们会发过期提醒。
- 加外部证书过期监控(UptimeRobot / Better Stack / Datadog / Pingdom),跑在签发机以外的地方。
- OS 升级之后立刻确认
certbot.timer还激活着——升级有时候会关三方 timer。 - 在 runbook 里写清续签流程,下一个值班的人 5 分钟修好、不是 5 小时。
常见坑
- 底层问题(challenge 被挡)没修,反复
certbot renew来”逼它过”——把限速配额烧光,一周内都签不出来。 - 以为平台自动续签,结果你早就关了托管证书自己签了。Vercel / Netlify 这类只续他们自己签的。
- 续了证书但忘了
systemctl reload nginx——磁盘新、送的旧。 - nginx 已经占了 :80 还用
--standalone,端口冲突直接挂。 - 续签监控跑在签证书的同一台机器上——机器挂了监控也挂了。永远外部监控。
FAQ
Q:证书刚过期,用户会丢数据 / 会话吗?
服务端会话不丢,但每个用户都看到浏览器警告。强证书 pinning 的移动 app 可能直接断。先恢复 HTTPS、再担心会话。
Q:能快速从别的 CA 签一张顶上吗?
可以——ZeroSSL、Buypass、Sectigo 都支持 ACME。配置里加第二个 issuer。也可以把站点挂到 Cloudflare 代理后面,几分钟(一个 DNS 改动)就拿到边缘证书顶住,同时再修 Let’s Encrypt。
Q:限速撞了怎么办?
每域名每周 50 张是滚动 7 天窗口。每小时 5 次失败验证是 60 分钟窗口。在等待期间用 staging(--test-cert)测。
Q:换成 1 年期证书避免续签问题,行不行?
公共 CA 已经不签长于 397 天的证书(而且整体在变短——90 天逐渐成为通用标准)。修自动化、别跟趋势对着干。