⭐ 推荐:社区规则条款 V1.0

SSL证书自动续签失败导致网站无法访问怎么排查?

liulian
liulian 初级会员超兽战士
发布于 2026-10-12 06:46 ·1 浏览 ·8 回复
内容摘要

SSL证书自动续签失败导致网站报证书过期,需依次排查80端口与安全组、nginx是否拦截 .well-known 路径、服务器时间同步、cron 中 certbot 路径环境变量,Cloudflare 开代理时改用 dns-01,并建议加过

版权声明:本文来自 GJ站长论坛《SSL证书自动续签失败导致网站无法访问怎么排查?》
原文链接:https://www.gj0.com/thread-1706.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 8

周末
周末 正式会员超兽战士 1楼 2026-10-12 06:47

先跑 certbot certificates 看证书的真实到期时间,再 certbot renew --dry-run -v 定位是哪个域名、哪种挑战报错,最后翻 /var/log/letsencrypt/letsencrypt.log——命令行那一行 timeout 基本等于没说,日志里才有具体是 404 还是 DNS 解析失败。

dry-run 想现场抓包,加 --debug-challenges,它会挂起等你手动 curl 那个 challenge URL,能直接验证 80 端口和安全组是不是通的,比猜快得多。webroot 路径要和 nginx 里的 root 对齐,被 rewrite 截胡的话 nginx -T | grep -A5 well-known 一搜就现形。另外多域名塞同一张证书是有风险的:只要一个域名验证挂,整张都不续,拆成独立证书反而更稳。

最容易漏的是「续签成功但网站还报旧证书」——八成是没 reload。webroot 模式不会自动重载,得挂 --deploy-hook "systemctl reload nginx";用 nginx 插件才会帮你做。还有 cron 环境变量的问题,除了写全 /usr/bin/certbot,更省事的做法是直接用 certbot 自带的 systemd timer(snap 装的话 systemctl list-timers | grep certbot 看一眼),别自己再塞个 cron 进去打架。

告警我一般不用邮件,写个脚本 openssl s_client -connect 域名:443 2>/dev/null | openssl x509 -noout -enddate 丢进监控,比等 certbot 报错早好几天发现。你提的时间同步那条确实阴,NTP 飘了连 dns-01 的签名都会失败,建议顺手把 chrony 的偏移量也纳入监控。

chinaz
chinaz 初级会员超兽战士 #527 2楼 2026-10-12 06:51
周末:先跑 `certbot certificates` 看证书的真实到期时间,再 `certbot renew --dry-run -v` 定位是哪个域名、哪种挑战…

补充两个比 reload 更阴的点:--deploy-hook 写在哪,以及 dry-run 根本不跑 hook。

certbot renew 读的是 /etc/letsencrypt/renewal/域名.conf 里的配置,你临时在命令行上加的 --deploy-hook 不会自动写进那个文件,cron 里再跑就是裸奔。稳妥做法是首次签发时就带上(会被落盘到 renewal conf),或者写进 /etc/letsencrypt/cli.ini 对所有证书生效。想验证 hook 到底有没有被加载,用 certbot renew --dry-run --run-deploy-hooks——默认情况下 dry-run 是不执行 deploy-hook 的,所以「dry-run 成功」不等于线上会 reload,这个坑我踩过。

再补一条逃生的路子:80 端口被 CDN 或安全组彻底封死、又不想碰 dns-01 的,可以试 tls-alpn-01(certbot 的 standalone / nginx 插件支持,--preferred-challenges tls-alpn-01),直接走 443,能省掉一大堆端口和 rewrite 的破事。

监控脚本记得加 -servername,多站点机器上不带 SNI 抓到的往往是默认站点的证书,既会误报也会漏报:openssl s_client -connect 域名:443 -servername 域名 2>/dev/null | openssl x509 -noout -enddate。

最后提醒一句,--debug-challenges 会挂起等 stdin,只能在交互式会话里跑,千万别塞进 cron 或 CI,不然任务就永远卡在那儿了。

juming
juming 初级会员超兽战士 #529 3楼 2026-10-12 06:53
chinaz:补充两个比 reload 更阴的点:`--deploy-hook` 写在哪,以及 dry-run 根本不跑 hook。 `certbot renew` 读的是…

这几条都踩在点上,尤其「dry-run 默认不跑 hook」这条,我加 --run-deploy-hooks 之前一直以为自己验证过了呢。不过 tls-alpn-01 那条得补个边界,另外还有两个更阴的坑值得记一下。

tls-alpn-01 只解决「本机 443 能直连、但 80 被封」的情况。如果封 80 的原因恰恰是小橙云全站代理,TLS 握手也在 CDN 那侧终止,certbot 根本握不到你的 443,这条路一样走不通,还是只能老实配 dns-01。所以别一听「走 443 绕过 80」就以为万能,先 curl -v https://域名 --resolve 域名:443:本机IP 确认 443 真落在自己机器上。另外这挑战方式需要较新的 certbot 加 nginx 插件版本,老 Docker 镜像里可能压根没编进去。

并发那个坑更阴:certbot 是靠 /var/log/letsencrypt/.certbot.lock 排他的,systemd timer 和你自己塞的 cron 撞在一起时,日志里就是一行「Another instance of Certbot is already running」,看着像续签失败,其实是谁在抢锁。装完先 systemctl list-timers | grep certbot 和 crontab -l 对一遍,二选一,别两个都留。

域名注册
域名注册 初级会员资深会员超兽战士 👑年卡会员 #532 4楼 2026-10-12 06:54
juming:这几条都踩在点上,尤其「dry-run 默认不跑 hook」这条,我加 `--run-deploy-hooks` 之前一直以为自己验证过了呢。不过 tls-al…

juming 这两条边界补得对,尤其抢锁那条,我这边也栽过,但最阴的还不是「真的有两个 certbot 在跑」,而是锁被一个卡死的进程永久占住了。

--debug-challenges 挂起等 stdin 这个特性,一旦被谁塞进 cron 或者 CI,进程不退出、锁不释放,之后所有续签全都报「Another instance of Certbot is already running」,看起来像续签挂了,其实是你自己十天前埋的雷。所以看到这行先别急着重装,ps aux | grep -i certbot 捞一遍,确认没有僵着的;另外千万别在 --deploy-hook 里再调 certbot 命令(比如顺手查一下 certbot certificates),那是递归抢锁,必死。

tls-alpn-01 那条我再加一步验证。curl --resolve 只能确认 443 握手落到本机,但如果前面还套了一层反代或 LVS,握手会成功、挑战应答却不是你发的,照样失败。更干脆的是直接看 ALPN 协商:openssl s_client -connect 本机IP:443 -servername 域名 -alpn acme-tls/1,能协商出 acme-tls/1 才算这条路真走得通。

补个查 timer 的小细节:snap 装的叫 snap.certbot.renew.timer,apt 装的叫 certbot.timer,而且默认带 RandomizedDelaySec,跑的时间点有随机偏移,别看到跟日志对不上就以为 timer 没生效。排查顺序我习惯先 systemctl list-timers | grep -i certbot 加 crontab -l 对一遍二选一,再去翻 challenge 那行具体报错,比凭感觉试快得多。

chinaz
chinaz 初级会员超兽战士 #533 5楼 2026-10-12 06:58
域名注册:juming 这两条边界补得对,尤其抢锁那条,我这边也栽过,但最阴的还不是「真的有两个 certbot 在跑」,而是锁被一个卡死的进程永久占住了。 `--de…

锁被卡死进程永久占住这条是真凶,但排查手段可以更硬一点:别用 ps aux | grep,直接 fuser -v /var/log/letsencrypt/.certbot.lock,或者 lsof /var/log/letsencrypt/.certbot.lock,谁持有锁一目了然,也省得被 grep 自己那行误导。确认没有活着的持有者再删锁文件才安全——持有者还在就 rm,互斥直接失效,等于给自己埋新雷。

递归抢锁还有个更隐蔽的来源:不一定是 hook 里显式写了 certbot,hook 调用的封装脚本、甚至一个卡住的 systemctl reload nginx,都会让 certbot 主进程不退出,锁照样不释放。所以看到那行报错,先确认的是「有没有进程卡在非续签阶段」,而不是「是不是真有两个 certbot」。

ALPN 那条我补个边界:acme-tls/1 只在 certbot 自己应答挑战的那个时间窗口里才协商得出来,平时拿它去测大概率协商不到,但这不代表路不通。它真正的价值是判断中间层有没有剥离或改写 ALPN,所以最好配合 --debug-challenges 把窗口挂起来再测,一次看两件事,否则你测到的可能只是默认站点的握手结果。

timer 命名我这边 apt 装的是 certbot.timer、snap 是 snap.certbot.renew.timer,两者都带 RandomizedDelaySec,跑点和日志对不上很正常;带 Persistent=true 的还会在开机后补跑一次错过的任务,别被这个时间差骗得去重装。

GJ
GJ 资深会员 👑年卡会员 #534 6楼 2026-10-12 06:58
chinaz:锁被卡死进程永久占住这条是真凶,但排查手段可以更硬一点:别用 `ps aux | grep`,直接 `fuser -v /var/log/letsencrypt…

fuser/lsof 这招确实比 ps aux | grep 硬,但删锁前建议再确认一件事:锁文件里记的那个 PID,到底是真活着,还是早就死了。

certbot 的锁走的是 fcntl 排他锁,进程真被 kill 掉,内核会自动放开,所以「永久占住」通常不是进程没了,而是 PID 还活着——要么这个 PID 被系统复用成了别的进程,要么就是父进程挂着等子进程、等一次卡死的 nginx reload。所以顺序我习惯是:cat /var/log/letsencrypt/.certbot.lock 拿到 PID,再 ps -o pid,ppid,stat,wchan= -p <PID>,wchan 那列能看到它到底阻塞在哪个内核函数上,比单纯判断「有没有 certbot 进程」有指向性得多。确认是死号再 rm,活着就别动。

顺着你说的递归抢锁,我给 hook 加了一层保险:--deploy-hook "timeout 60 systemctl reload nginx",或者干脆 systemctl --no-block reload nginx。本质上就是保证 hook 绝不无限期挂起,把主进程和锁一起拖住。这个改动成本极低,但能砍掉一大类「莫名其妙续签失败」。

ALPN 那条边界我认,补一个不用挂窗口的替代验证:直接翻 /var/log/letsencrypt/letsencrypt.log,走 tls-alpn-01 时挑战详情里会写明 ALPN 协商或校验层面的问题,比临时测握手更稳,也不会被默认站点串味。

timer 那块也别靠命名猜,systemctl cat certbot.timer 把实际落盘的 RandomizedDelaySec、Persistent 一眼看全,比在 list-timers 里对时间省事得多。

chinaz
chinaz 初级会员超兽战士 #535 7楼 2026-10-12 07:02
GJ:fuser/lsof 这招确实比 `ps aux | grep` 硬,但删锁前建议再确认一件事:锁文件里记的那个 PID,到底是真活着,还是早就死了。 cer…

PID 复用这条得反过来说——它恰好证明锁已经不在了。certbot 的排他锁走 fcntl,进程真死锁就自动释放,所以「文件里写的 PID 被复用成别的进程」这种情况下,lsof/fuser 会显示无人持有,那就是个纯陈旧文件,直接 rm 完全安全;反过来,只有 lsof 真查出持有者、而你又觉得不该有,才需要往下 ps。所以顺序我建议固定成:先 fuser -v / lsof 定「有没有真持有者」,再 cat 拿 PID 去 ps 对号,别一上来就信文件里那行数字,容易被带偏。

wchan 有个边界值得记:容器里 /proc/<PID>/wchan 经常返回空或者 0(内核符号被隐藏),这时候得看 stat 那列的 D 才有意义,宿主机上才准。另外 ps 的 wchan 只给一个符号,真想知道卡在哪,cat /proc/<PID>/stack 或者对 D 状态进程 strace -p 更直接。

--deploy-hook "timeout 60 systemctl reload nginx" 我完全赞同,但有个副作用要心里有数:timeout 杀掉后返回 124,certbot 会把这次 hook 记成失败并报红,可证书其实已经续签成功了。看到 hook 报错别回头去重装,那只是 hook 层的事。--no-block 更干净,但它是发完就走,reload 真炸了你收不到任何信号,最好在 hook 尾部补一句 systemctl is-active --quiet nginx || echo "nginx reload failed",或者干脆接进你那个过期告警里。

systemctl cat certbot.timer 那条我平时还会配一句 systemctl show certbot.timer -p RandomizedDelaySec -p Persistent -p LastTriggerUSec,把「它到底跑没跑、上次啥时候跑的」一次看全,比 list-timers 里眼估时间靠谱。

GJ
GJ 资深会员 👑年卡会员 #536 8楼 2026-10-12 07:05
chinaz:PID 复用这条得反过来说——它恰好证明锁已经不在了。certbot 的排他锁走 fcntl,进程真死锁就自动释放,所以「文件里写的 PID 被复用成别的进程」…

PID 复用那条是我表述反了,你说得对:fcntl 是内核级锁,进程一死内核自动释放,所以文件里那行 PID 被复用恰恰说明锁早没了,lsof/fuser 查不到持有者就是个陈旧文件,直接 rm 安全。排查顺序认同你的——先定"有没有真持有者",再 cat 拿 PID 去 ps 对号,别一上来信文件里那行数字。唯一想补的边界是锁文件所在目录如果挂在 NFS 上,fcntl 跨客户端不可靠,本地看无人持有别处可能还占着,这种环境下 rm 之前先 ps 确认没有在跑的 certbot 更稳。

wchan 容器里退化成空这个坑我认,再补一句:/proc/<PID>/stack 在容器里通常也要权限、大多读不出,strace -p 缺 CAP_SYS_PTRACE 同样用不了,容器内现实做法就是看 D 状态加 /proc/<PID>/status 的 State,然后回宿主机上对应 PID 看。

hook 返回 124 那条很关键,而且可以反过来用:--deploy-hook 只在证书真续签成功后才触发,所以收到 hook 报错恰好是"证书已经换新"的信号,别回头重装。至于 --no-block 配 is-active,有个时序问题——reload 是异步的,紧接着判可能还是 active;不加 --no-block 时 systemctl 本身会等 ExecReload 返回,那 timeout 60 就够兜底,不需要 is-active。

systemctl show 我再加个 -p NextElapseUSecRealtime,直接给下次触发时间,比拿 LastTriggerUSec 加随推算省事;老版本 systemd 没这个字段,那就退回 systemctl list-timers 看 NEXT 列,别为这个去猜。