服务器迁移后Let‘s Encrypt证书自动续期失效的排查与修复
2026/9/16 3:57:31 网站建设 项目流程

1. 迁移后的故障现场:该续的没续,该换的没换

1.1 浏览器先炸还是服务先炸:两种典型报错形态

服务器迁移这件事,最折磨人的不是迁移当天,而是迁移后的一到两周。迁移当天所有服务看起来都是通的,SSH能连,Nginx能起,网站能开,你以为万事大吉。结果过了几天,浏览器开始弹大红叉,NET::ERR_CERT_DATE_INVALID,紧接着用户开始反馈"网站不安全""打开是拦截页"。

我第一次在迁移后遇到Let's Encrypt证书问题,就是这个套路。当时我把整套业务从旧机器平移到新机器,Nginx配置、数据库、静态文件都搬完了,唯独下意识觉得"证书这东西有自动续期,不用管"。结果就是这种"自动续期"的错觉,让我在迁移后的第十天被线上事故打脸。

这类故障通常分两种形态,很多人一开始分不清。

第一种是浏览器直接报证书过期。这种情况其实反而好排查,坏得明显。你打开https://example.com,浏览器明确告诉你"此网站的安全证书已过期"或者"证书无效",错误码可能是NET::ERR_CERT_DATE_INVALID,也可能是ERR_CERT_AUTHORITY_INVALID。前者是时间问题,后者是信任链问题。时间问题大概率是证书文件本身就过期了,或者服务器系统时间不准;信任链问题则可能牵扯到根证书、中间证书没配全。

第二种更隐蔽:服务一切正常,但证书的到期时间保持在迁移前的旧日期上。你用一个第三方SSL检测工具去查,发现证书剩余天数越来越少,但Nginx没有任何报错。因为你新机器上跑的其实是一份"从旧机器拷过来的、已经过期"的证书文件,Nginx照样加载,浏览器照样拦截,但Web服务进程自己完全无感。这就像门锁已经锈死了,但业主还不知道,每天照样假装锁了门。

1.2 为什么"迁移后一周"才暴露问题

这里有个时间差的学问。Let's Encrypt证书有效期是90天,certbot默认的策略是"剩余时间不足30天才真正触发续期"。所以哪怕旧服务器已经停止续期动作了,只要证书还有31天以上有效期,一切看起来风平浪静。等你搬完服务器,慢慢做完DNS切换、数据校验、配置调整,一两个星期过去了,证书剩余天数跌破30天,此时新机器上的续期机制没接上,问题正式爆发。

也就是说,迁移后你大概有一个"窗口期",在这个窗口里不处理证书迁移,系统的隐患不会立刻爆炸。但它一定会爆炸,只是早晚问题。这也就解释了为什么那么多人在迁移半个月后才开始排查证书问题——因为在第14天之前,检测工具显示证书还没过期,等它真过期了,一切都已经来不及优雅处理。

2. 先搞清楚一件事:"自动续期"实际是谁在干活

2.1 自动续期不是Let's Encrypt干的,是你的本地客户端干的

很多人的误解根源在于"Let's Encrypt证书会自动续期"这句话。准确地说,Let's Encrypt这个CA(证书颁发机构)只负责签发和吊销证书,它不会追着你家的服务器说"哎,你证书快到期了,我帮你换一个"。签发完证书那一刻,CA和你的服务器就再无瓜葛了,"续期"完全是你服务器上那个客户端程序的责任。

社区里最常见的客户端是certbot,也有acme.sh、lego这类工具。以certbot为例,安装时它会往系统里注册一套定时任务。Debian/Ubuntu系通常是systemdcertbot.timer,CentOS 7系可能直接写在crontab里,具体看版本和安装方式。这套定时任务才是"自动续期"的真正执行者。

所以当你问"Let‘s Encrypt证书到底会不会自动续期"时,准确答案是:如果你安装certbot时正确注册了定时任务,并且证书目录、配置文件、验证端口都保持正常,那么它确实能全自动续期;一旦其中任何一个环节被破坏——比如服务器迁移——这个"自动"就名存实亡。

2.2 certbot续期的三个硬性前提

certbot的续期逻辑看似简单,但触发成功需要满足几个前提,缺一个就失败。

第一个是定时任务必须活着。你可以用systemctl status certbot.timer看状态,用systemctl list-timers | grep certbot看它下次触发时间。如果这条链路断了,certbot这辈子都不会主动跑一次。

第二个是证书目录必须完整。certbot续期不是凭空去CA那边拿一张新证书,它要基于本地的/etc/letsencrypt目录里的账户信息、域名配置、私钥文件来发起申请。目录结构大概是:

/etc/letsencrypt/ ├── accounts/ # ACME账户密钥 ├── archive/ # 历史证书归档 ├── live/ # 当前生效证书的软链接 └── renewal/ # 每个域名独立的续期配置文件

renewal/里每个*.conf文件记录了这个域名用哪种验证方式、证书路径在哪、需要包含哪些域名。迁移时如果你只拷贝了Nginx配置,没拷/etc/letsencrypt整个目录,那certbot即使跑起来也找不到任何"可以续期的证书"。

第三个是验证通道必须通畅。Let's Encrypt续期和首次签发一样,需要证明你确实拥有这个域名。HTTP-01挑战会通过你的域名去访问一个临时路径,例如http://你的域名/.well-known/acme-challenge/xxxx。这意味着你的域名DNS必须指向当前服务器,而且80端口必须能正常响应这个路径。新服务器上如果没装Nginx,或者Nginx配置里没把80端口监听起来,或者防火墙拦了80端口,验证就会失败,续期自然失败。

2.3 续期阈值:"还剩30天"才是换证时机

certbot每次触发定时任务后,并不会无脑给所有证书续期。它的逻辑是检查证书剩余有效期,只有少于30天时才会真正动手。所以你在日志里经常看到这样的内容:

Certbot is not due for renewal of certificate example.com yet

这句不是报错,是正常的跳过。Let's Encrypt把证书有效期定成90天,加上30天续期阈值,等于每张证书在生命周期内大概会续期两次到三次。这样设计有两个用意:一是短效证书即使私钥泄露,影响面也有限;二是强迫使用者把续期做成自动化,不能靠人手去记到期时间。

理解了这个机制,你就能明白为什么迁移后不要干等"自动续期":如果旧证书还剩下35天,而新服务器的定时任务没接上,certbot确实不会主动做任何事,因为还没到30天临界点。但如果你不去检查,永远不会知道"系统在静默地等一个永远等不到的续期动作"。这也是这类故障最可怕的地方。

3. 服务器迁移动了哪些"续期命脉"

3.1 systemd服务状态跟着老系统一起归零

服务器迁移不是简单的文件复制,它本质上是"把你原来那套运行环境在另一台机器上重新搭建一遍"。而certbot的续期能力,恰恰是绑定在运行环境上的。

最常见的丢失项是certbot.timercertbot.service的systemd单元文件。很多人在迁移时备份了网站目录、数据库、Nginx配置,但很少有人会想到备份systemd的服务定义。到了新机器上,你重新安装了certbot,却忘了systemctl enable certbot.timer,于是定时器根本没有启用。

更隐蔽的一种情况是:新机器的系统版本和旧机器不一致。比如旧机器是Ubuntu 18.04,新机器是Rocky Linux 9,或者反过来。不同发行版里certbot的安装路径、插件机制、默认定时器命名都有差异。Ubuntu 18.04的certbot可能自带certbot.timer,而某些从EPEL装的certbot则默认只放/etc/cron.d/certbot。你把旧机器的运维习惯带到新机器,就会漏看这些差异。

3.2 证书目录没搬全,等于白搬

迁移时的第二个重灾区就是/etc/letsencrypt目录。我见过不少同行,迁移时只把Nginx配置里的ssl_certificatessl_certificate_key指向的.pem文件拷过去,完全没管/etc/letsencrypt

这种做法短期看没问题,Nginx能启动,HTTPS能访问。但问题在于renewal/配置目录是空的,certbot不知道有哪些证书需要管理,也不会去更新live/下的软链接。等证书真过期了,你只能手动重新签发,而且由于账户密钥没迁移,新签发的证书和你原来那批证书在ACME账户维度上就断开了关系。

正确做法是把整个/etc/letsencrypt目录打包带走,最好连/var/log/letsencrypt的日志目录一起。这样certbot的账户、证书、续期配置全部原样恢复,新机器上跑certbot renew --dry-run就能无缝衔接。

3.3 DNS切换、IP更换与验证时机的错位

这个坑在云上迁移时尤其容易踩。很多迁移流程是:先把新服务器所有服务配好,然后改DNS解析让它指向新IP,最后再处理证书。问题就出在时间顺序上。

如果你在DNS还没切换之前就尝试跑certbot renew,ACME验证服务器访问你的域名时,请求会打到旧服务器IP上。此时旧服务器可能已经关了,或者旧服务器的HTTP服务已经停了,验证自然失败。反过来,如果你切了DNS之后跑续期,但新服务器的80端口被其他服务占用、或者安全组没放行80端口,同样会失败。

所以迁移过程中的证书操作,应该严格放在"DNS已切换、新服务器防火墙已放行、80端口已有HTTP服务响应"这三个条件全部满足之后。顺序反了,你会在排查时被这些变量绕晕。

3.4 时间同步问题:一个容易被忽略的"伪证书过期"

新装好的服务器如果没配置NTP或者chrony,系统时间可能是错的。有时候云镜像里刻的时间还是几个月前,有时候时区没设对导致本地时间和UTC差了好几个小时。

证书校验对时间极其敏感。客户端和服务器比对证书有效期时,都基于各自本地的系统时间。服务器时间慢了15天,一张本来还剩20天有效的证书,会被客户端判定为"只剩5天",进而触发不必要的告警;服务器时间快了10天,一张还有效的证书可能直接显示过期。

排查证书问题时,第一件事永远是先date看一眼服务器时间,确认时区和实际时间都正确。这个习惯能帮你省掉大量无意义的证书检查。

4. 从证书报错反向定位:一次完整的排查链路

4.1 第一步:用一行命令确认证书真实到期时间

不管浏览器报什么错,第一步永远是先看服务器上的证书文件实际到期时间。使用openssl直接读,最干净利落:

openssl x509 -enddate -noout -in /etc/letsencrypt/live/example.com/fullchain.pem

输出里会有notAfter=字段,格式类似Sep 12 12:00:00 2025 GMT。拿到这个时间,立刻就能判断问题是"证书确实过期了"还是"系统时间错乱"。

如果你不确定Nginx实际加载的是哪个证书文件,先看nginx.confssl_certificate指向的路径,再跟着路径读文件。常见误区是看了一个与实际情况无关的证书文件,比如自己手动拷贝的/etc/ssl/example.com.pem,结果真正生效的却是/etc/letsencrypt/live/下的软链接指向的那份。

4.2 第二步:手动跑一次续期,让错误直接暴露

排查续期问题最有效的手段就是手动触发续期,不经过定时任务,直接在前台跑一次,把所有错误输出尽收眼底:

certbot renew --dry-run

--dry-run是试运行,不会真正换证书,但会完整走一遍ACME验证流程,用来确认整条链路是否通畅。如果这条命令能顺利跑完并显示Congratulations,说明验证通道、证书目录、ACME账户全部正常,问题大概率出在定时任务上;如果报错,错误信息会直接告诉你问题在哪:

  • Domain not foundNo valid IP address found,说明DNS解析有问题,或者域名没指向当前机器。
  • Connection refusedTimeout during connect,说明80端口不通,检查防火墙和安全组。
  • The certificate failed to validate,说明HTTP-01验证时,ACME服务器没能访问到.well-known/acme-challenge/路径,检查Nginx是否配置了对应location路由。
  • Could not find a usable config或者Unrecognized arguments,说明renewal配置目录有问题,或者certbot版本和插件不匹配。

4.3 第三步:从日志里找定时任务的执行痕迹

手动跑完不出错,那就把怀疑对象转向定时任务本身。先看timer状态:

systemctl list-timers | grep certbot

如果有输出,说明timer已注册;如果没有,说明定时器根本不存在或者没启用。再看上一次执行日志:

journalctl -u certbot.service -n 50

也可以直接翻开certbot自己的日志文件/var/log/letsencrypt/letsencrypt.log,搜索最近的Certbot is not due for renewal或者Congratulations字样。如果日志里连最近一两周的执行记录都没有,那就坐实了"定时任务压根没跑"的判断。

4.4 修复:重建续期链路并恢复certbot管理权

根据排查结果,修复方案基本是固定的几个动作。

先恢复证书目录。如果你迁移时带了/etc/letsencrypt备份,直接解压覆盖回去:

tar -xzpf letsencrypt-backup.tar.gz -C /

覆盖之后,用certbot certificates确认certbot能看到证书列表,并且路径和Nginx配置引用一致。

然后恢复定时任务。Debian系certbot包一般自带certbot.timer,但可能需要手动启用:

systemctl enable --now certbot.timer systemctl status certbot.timer

如果你用的是CentOS系,certbot的cron脚本可能位于/etc/cron.d/certbot,查看里面的内容确认cron配置存在即可。

接着重新拉一次证书,确保当前文件不是旧的:

certbot renew

这条命令只有在证书剩余天数低于30天时才会真正换新。如果剩余时间还多,可以用--force-renewal强制续期,不过日常不建议没事就强制续,能用--dry-run验证链路就足够了。

最后重载Web服务,让新证书生效:

nginx -t && nginx -s reload

4.5 踩坑备注:迁移时别只拷证书文件

这次事故之后,我每次迁移服务器都会特别注意一个点:证书迁移不只是搬运文件,更是搬运"管理状态"。/etc/letsencrypt目录里不仅有.pem证书,还有ACME账户密钥、renewal配置、archive历史归档。这三样东西缺一样,certbot的自动化管理能力就打折扣。

我现在的迁移习惯是连/etc/letsencrypt/var/log/letsencrypt/etc/systemd/system/certbot.*一起打包备份,最多再带一份crontab -l的输出。宁可多带,不可少带。因为在新环境里重建这些东西虽然不难,但排查过程消耗的时间成本,远比多打一个压缩包要多得多。

5. 验证和长效巡检:别等浏览器报错才想起证书

5.1 用dry-run作为迁移后的必检动作

迁移完成后,我建议把certbot renew --dry-run当作和"测试数据库连接"同级别的验收步骤。它不需要等证书临近过期,不管剩余天数多少都能完整走一遍验证流程,是最快确认续期链路正常的办法。

整个迁移Checklist做下来大概长这样:

检查项命令预期结果
证书文件是否完整ls -l /etc/letsencrypt/live/域名/存在fullchain.pem和privkey.pem
证书到期时间openssl x509 -enddate -noout -in /etc/letsencrypt/live/域名/fullchain.pem到期时间在未来30天以上
certbot能否识别证书certbot certificates域名列表和实际一致
定时任务是否注册systemctl list-timers | grep certbot显示下次触发时间
续期链路是否通畅certbot renew --dry-run显示Congratulations或成功提示
Nginx配置是否引用正确nginx -T | grep ssl_certificate路径指向/etc/letsencrypt/live/
系统时间是否准确date与实际时间一致,时区正确

5.2 一个简单的剩余天数检查脚本

除了certbot自己的dry-run,我还会在服务器上放一个简单的巡检脚本,每周跑一次,检查所有证书的剩余天数。脚本逻辑很直接:用openssl读到期时间,转成时间戳,减去当前时间戳,得到剩余天数,低于阈值就告警。

#!/bin/bash CERT_DIR="/etc/letsencrypt/live" THRESHOLD=40 for dir in "$CERT_DIR"/*/; do CERT="$dir/fullchain.pem" if [ ! -f "$CERT" ]; then continue fi EXPIRE_DATE=$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2) EXPIRE_TS=$(date -d "$EXPIRE_DATE" +%s) NOW_TS=$(date +%s) REMAIN_DAYS=$(( (EXPIRE_TS - NOW_TS) / 86400 )) DOMAIN=$(basename "$dir") echo "$DOMAIN 剩余 $REMAIN_DAYS 天" if [ "$REMAIN_DAYS" -lt "$THRESHOLD" ]; then echo "$DOMAIN 将在 $REMAIN_DAYS 天后过期,请及时处理" # 这里可以接告警,比如curl一个Webhook fi done

阈值我习惯设40天。因为还剩30天是certbot的续期触发点,我在40天就告警,给自己留出10天的缓冲时间去排查为什么certbot没成功续期。这个缓冲在线上环境里非常重要,它把"紧急事故"变成了"常规工单"。

5.3 云厂商免费证书和Let's Encrypt的区别要分清

排查过程中还有一个容易混淆的点,就是你手头的证书到底是哪来的。阿里云、腾讯云这类云厂商也提供免费SSL证书,很多站点历史上混用过几家的证书。它们的续期逻辑完全不同:云厂商的免费证书通常有效期1年,不提供自动续期,你需要在控制台手动申请新证书再重新部署到服务器上。

所以当你发现"服务器上明明有自动续期任务,证书还是过期了"的时候,先确认一下证书链的签发者是谁。用openssl看一下:

openssl x509 -issuer -noout -in /etc/letsencrypt/live/example.com/fullchain.pem

如果签发者是Let's Encrypt,那就是certbot的职责范围;如果是其他CA,别指望certbot来管,老老实实按云厂商的流程去手动换证。我见过太多人拿着Let's Encrypt的排查思路去查云厂商证书,查半天毫无结果。

6. 几个容易忽略的连带问题和运维细节

6.1 老版本JDK和ISRG根证书的兼容性

聊到Let's Encrypt证书,有一类问题不发生在服务器上,而发生在客户端。这正好是近期很多人搜"ISRG Root X1"的原因。

Let's Encrypt的证书链现在主要依赖ISRG Root X1这个根证书。如果你的Java程序跑在比较老的JDK上,比如JDK 8u141之前的版本,默认信任库(cacerts)里没有内置ISRG Root X1,那么这些Java程序访问Let‘s Encrypt签发的HTTPS站点时,会直接报PKIX path building failed,因为它不认识这个根证书。

解决方案通常是两个方向:要么升级JDK版本到一个内置了新根证书的版本,要么把这个根证书手动导入JDK的cacerts信任库。如果是一些没法升级的老系统,还要注意证书链里是否包含了跨签的中间证书,某些老客户端需要靠中间证书里的交叉签名来补齐信任路径。这也是为什么我建议在排查"浏览器能打开但Java程序报证书错误"这类问题时,先去确认客户端环境的信任库,而不是一上来就怀疑服务器证书配置有问题。

6.2 有条件续期机制背后的工程理念

Let's Encrypt把证书有效期定90天、续期阈值定30天,这个设计在运维上是有深意的。短有效期意味着即使私钥泄露,事故影响面被限定在90天内;也意味着你必须把事情自动化,不能靠人肉记日期。续期阈值30天则是给自动化留了足够的重试窗口——一次定时任务失败,还有将近一个月的时间去修复,不至于第二天就挂掉。

理解了这套设计逻辑,你对"自动续期"的态度就会有转变:不要神化它,也不要轻视它。它就是一套"定时器 + 条件判断 + 自动申请"的脚本组合。它的可靠性来源于你服务器环境的稳定性,一旦环境变了,脚本不会像人一样主动适应,只会静静地失败、跳过、等待。

6.3 迁移后还有一类证书问题,和Let's Encrypt完全无关

排查过程中,我经常被问到一个看起来很像的问题:为什么Charles、Fiddler这类抓包工具突然报证书过期?

这类工具使用的证书是自己动态生成的根证书,用于解密HTTPS流量。抓包工具的证书过期,其实是指"你手机或电脑上安装的那个抓包根证书"过期了,跟网站服务器上的Let's Encrypt证书没有半点关系。处理方式也完全不同:你需要去抓包工具的设置里生成一个新的根证书,然后重新安装到设备上,旧的删掉。

把这两类证书问题混为一谈是新手最容易犯的错。前者是服务端证书,后者是客户端代理证书,两者无论是证书格式、信任链建立方式,还是更新路径都不一样。排查线上证书问题前,先想清楚"这个证书是装在服务器上的,还是装在用户设备上的",方向对了,效率就上来了。

6.4 迁移这件事,最值钱的经验是Checklist

回头再看这次迁移排查,我最大的体会是:迁移本身不难,难的是迁移后的环境一致性。你在一台机器上精心配置的自动续期机制,换一台机器如果不显式恢复,它就跟没存在过一样。这也是为什么我现在做迁移,第一件事永远是列Checklist,把certbot、logrotate、系统定时器、防火墙规则这些"看不见的运维基础设施"全部列进去,和数据库、网站文件一样作为一等公民对待。

以后再遇到服务器迁移,关于证书部分我会直接这样操作:备份整个/etc/letsencrypt和相关systemd配置,迁移完成后恢复目录、检查路径、systemctl enable --now certbot.timer、跑一次certbot renew --dry-run,全部绿灯后才宣布迁移完成。整个过程十分钟,但这十分钟能省掉未来半个月的线上排查时间。你可能会说,这些环节看起来都太基础了,但恰恰是这些基础环节,在迁移的高压环境下最容易被忽略。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询