证书续签自动化静默挂了,站点现在不受信任 —— 完整修复指南

Certbot / 平台自动续签几个月前就坏了,没人发现。直到浏览器弹 NET::ERR_CERT_DATE_INVALID。查清续签为什么挂,把 cron 修回来。

浏览器一片红: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:rateLimitedtoo 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.timersystemctl list-timers --all | grep cert
  • crontab -lcat /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 天逐渐成为通用标准)。修自动化、别跟趋势对着干。

参见 绑了自定义域名但 SSL 没发CAA 记录挡了证书签发HTTPS 没强制

标签: #排查 #SSL #letsencrypt #automation #certbot