
1. 一次服务器迁移把证书续期问题全炸出来了上周帮一位朋友处理生产环境的服务器迁移原本预计两个小时搞定结果从下午一直忙到半夜。罪魁祸首不是应用配置不是数据库连接就是一张差点过期的 Lets Encrypt 证书。当时的情况是服务器从旧机迁到新机域名解析切换过去之后一开始一切正常。结果第二天早上监控告警HTTPS 访问大面积报证书错误。登上去一看证书还在有效期内但新机器上的续期定时任务根本没跑起来。更麻烦的是旧机器的 cron 还在继续执行续期两边互相干扰搞得我一时间都分不清哪个证书才是“真正在用”的那份。这个问题太典型了。很多人一听到 Lets Encrypt脑子里第一反应就是“免费”“自动续期”好像装上之后就一劳永逸了。但“自动续期”这四个字跟很多人理解的无缝衔接完全不是一回事。它不是一个真正的自治系统而是一套依赖环境、依赖配置、依赖定时任务的自动化流程。任何一环断了证书到期就是必然结果。这篇文章我想把 Lets Encrypt 自动续期的原理讲透同时把这次迁移后排查证书问题的完整过程记录下来。不管你是刚接触 HTTPS 的初级运维还是已经用 certbot 跑了两三年证书的老手只要你打算做服务器迁移、域名切换、容器化改造这篇文章应该能帮你少踩几个坑。2. 先搞清楚 Lets Encrypt 自动续期的底层逻辑2.1 续期不是“到期自动换”而是“周期检测到期前刷新”Lets Encrypt 证书默认有效期是 90 天。90 天这个设计本身就是为了让证书生态保持“短周期、勤更新”的节奏。正是因为有效期短自动续期才成为刚需而不可靠的续期流程会在运维里暴雷。但“自动续期”到底是怎么实现的以最常见的 certbot 客户端为例它并不是在证书“到期”那一刻做刷新而是通过系统定时任务通常是 cron 或 systemd timer定期调用certbot renew。这个命令会检测机器上所有由 certbot 管理的证书并判断要不要续证书剩余有效期是否低于 30 天以及客户端版本、配置是否与签发时的状态一致。换句话说只要你安装 certbot 时把定时任务配好了通常每 12 小时它会检测一次全部证书。如果证书距离过期还有超过 30 天就直接跳过什么都不做如果少于 30 天就执行续期。所以严格来说它叫“定期体检到期前自动更换”而不是“到期自动续”。这个差异看似只是文字游戏实际操作中影响特别大。比如你迁移完服务器证书还有 60 天有效期跑certbot renew会直接跳过——不是它坏了而是它认为没必要续。如果这时候你察觉不到问题等 40 天后再来看发现一切正常再过 10 天证书突然就过期了。你要么每天手动盯日历要么一开始就做一次强制续期测试没有第三条路。2.2 自动续期必须满足的三个前提条件从这次排查的经验来看真正的坑往往不在 renew 命令本身而在它依赖的三个前提条件。第一个定时任务必须存在且正常运行。certbot 安装脚本默认会在/etc/cron.d/certbot写入一条 cron 配置或者安装一个 systemd timer。但如果你在服务器迁移时只复制了证书文件没复制定时任务配置或者新系统里 certbot 是后装的但 cron 服务没启用、Docker 里压根没有 cron 进程——续期自然就断了而且你短时间内很难察觉。第二个续期时的验证通道必须通。Lets Encrypt 续期不是“你说你有这个域名我就给你续”它要重新验证你对域名的控制权。HTTP-01 或 DNS-01 挑战都必须在你配置的路径或 DNS 服务商上走通。迁移服务器后如果防火墙端口没开、webroot 目录不存在、DNS 记录还暂时指到旧 IP验证都会失败。第三个配置不能发生“漂移”。certbot 会把每个证书的签发配置记录在/etc/letsencrypt/renewal/下renew 时它按原来的配置重新走一遍。如果你迁移后改了 nginx 的根目录路径、换了 webroot、或者改了验证方式renew 就会报配置不一致错误甚至完全跑不起来。这三个条件像是一根链条上的三个环。迁移服务器这个动作比日常更新更容易同时打断多个环——你换了机器、换了系统、改了配置迁移前的证书状态瞬间就变得不可信了。3. 迁移后证书失效的常见原因拆解3.1 最典型的三种“搬新家”现场我这次遇到的情况属于第一种——定时任务没搬过去。迁移服务器通常是个多步骤过程备份代码、导出数据库、同步静态文件、切换解析。证书这玩意在“备份”环节特别容易漏因为很多人觉得它是搭在服务器上的“基础设施”搬过去也就过去了。但新机器从系统到应用都是全新的环境任何东西都不会自动出现在它该在的位置。第二种常见现场是“证书文件搬过去了但私钥和证书链没配对”。Lets Encrypt 证书文件一般分成证书本体和私钥两类。迁移时只拷贝了其中一个或者从旧机器导出 nginx 用的 PEM 时没把 fullchain 带上新机器上 HTTPS 会直接报证书链不完整。这种问题的排查难度在于浏览器上显示的错误五花八门有的报“不安全连接”有的直接拒绝访问但本质都是证书链缺了中间证书。第三种常见现场是“容器化改造后路径全变了”。现在很多迁移都顺便容器化nginx 或 Caddy 封装到 Docker 里certbot 跑在宿主机证书目录通过 volume 挂载。挂载路径没对齐、文件权限不对容器里读到的是空目录。这种情况非常隐蔽因为从宿主机的 certbot 状态看一切正常从容器外看证书文件也都在但容器进程实际上根本读不到私钥。3.2 域名解析和验证路径的变化最容易忽略迁移完成后域名解析切换是最后一步但也是影响续期最大的变量。如果你用 HTTP-01 验证Lets Encrypt 的验证服务器会通过 80 端口去访问你的域名下某个挑战路径比如http://你的域名/.well-known/acme-challenge/一串随机字符串。这个请求会经过 DNS 解析到达你的服务器。如果解析切换前你就提前跑续期请求会落到旧服务器上如果解析切换后旧服务器还在运行、占用了 80 端口也可能出现请求被旧机器响应的情况此时续期校验的“控制权”其实落错了对象。DNS 生效还有个传播时间尤其一些解析服务商改了 TTL 之后不会立刻全局生效。我在迁移时习惯提前把域名 TTL 降到 60 秒等全部切换完再恢复。如果没做这个操作续期验证期间 DNS 还在飘失败率会陡增。更麻烦的是这种失败不是“一次性的”而是断断续续——某个区域的解析到了新机器验证成功另一个区域还指向旧机器验证失败最终表现就是你看到 certbot 输出一堆随机时段的报错。另外 webroot 路径也容易改出问题。假设旧机器上 nginx 的根目录是/var/www/htmlcertbot 配置里也写的是这个路径新机器上你把静态文件放到了/srv/www但配置还沿用旧的验证请求就找不到对应的挑战文件续期自然失败。这类问题在迁移后很常见因为很少有人会把 nginx 的文档根目录也写进迁移方案。4. 证书排查实操从症状到根因的完整路径4.1 第一步先确认证书到底过期没有排查证书问题第一步永远是“看清楚事实”。很多人一看到浏览器报“证书无效”就猜是证书过期其实不一定。证书无效可能是因为域名不匹配、证书链不完整、私钥不匹配、服务器时间不对等等。不要凭感觉修先查。最直接的方式是用 openssl 命令查看远程服务器的证书状态echo | openssl s_client -connect 你的域名:443 -servername 你的域名 2/dev/null | openssl x509 -noout -dates -issuer -subject这条命令会连接 443 端口拉取证书再交给 x509 解析出关键信息。你会看到notBefore和notAfter两个时间分别代表证书生效时间和过期时间同时还能看到 subject证书归属域名和 issuer签发机构。如果 issuer 显示的不是 Lets Encrypt 的中间证书或者 subject 里域名跟你访问的域名对不上问题就不是续期而是证书配置本身错了。如果服务器在本机直接看证书文件也可以openssl x509 -enddate -noout -in /etc/letsencrypt/live/你的域名/cert.pem看到有效期之后记下距离过期还有多少天。如果已经过期直接跳到续期如果还在有效期内说明问题不在“过期”本身而在证书链、域名匹配或续期机制。提示这条命令如果卡住没输出多半是 443 端口没通先检查防火墙和安全组别急着怀疑证书。4.2 第二步查看证书链是否完整浏览器报“证书无效”很多时候不是证书过期而是证书链缺了中间证书。Lets Encrypt 签发的证书链通常由两层组成最上层的根证书ISRG Root X1不会直接签叶子证书而是先签一个中间证书再由中间证书签你的域名证书。服务器返回给客户端的握手数据里必须包含中间证书因为客户端本地只信任根证书需要用中间证书来搭一条从叶子证书到信任锚的路径。我看证书链有个习惯直接看服务器实际返回的证书栈openssl s_client -connect 你的域名:443 -servername 你的域名 -showcerts 2/dev/null /dev/null输出末尾会有证书栈。正常的 Lets Encrypt 证书栈应该至少有两层你域名的叶子证书 一个中间证书。如果输出里只有一层叶子证书说明 nginx 或 Web 服务器没有正确配置ssl_certificate可能只指定了 cert.pem 而不是 fullchain.pem。这是个非常经典的配置错误。certbot 会生成四个主要文件cert.pem、chain.pem、fullchain.pem、privkey.pem。正确做法是在 nginx 里ssl_certificate /etc/letsencrypt/live/你的域名/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/你的域名/privkey.pem;有些人在网上看到教程写 cert.pem照抄之后服务器只给客户端发叶子证书。桌面 Chrome 会“智能”地从网上拉取中间证书补齐所以短时间看不出问题但很多移动端 App、命令行客户端、老版本软件、嵌入式设备都不会做这种补齐直接报证书链错误。这就是为什么我在排查时会用 openssl 而不仅仅是浏览器——浏览器会帮你掩盖问题openssl 不会。4.3 第三步检查自动续期任务是否健在确认证书本身没问题之后再排查自动续期为何失效。先看 certbot 自己管理的证书状态certbot certificates输出里会列出每个证书的域名、到期时间和证书名称。如果列表为空说明当前机器上的 certbot 不知道任何证书——你刚才查的那份文件可能是手动拷贝的如果列表里有证书但路径跟我们前面查的实际路径不一致说明两份并存很容易混淆。然后是检查定时任务。不同安装方式对应的位置不一样Debian/Ubuntu 用 apt 安装配置在/etc/cron.d/certbotsnap 安装通常用 systemd timer用systemctl list-timers | grep certbot查看源码安装或 Docker 镜像看 entrypoint 脚本或容器内 cron 是否被触发检查完定时任务还要看 cron 服务本身有没有运行。一些精简系统镜像默认没装 cron或者容器里压根没有 cron 进程定时任务文件写了等于没写。这个问题在迁移后尤其常见因为很多运维的“迁移”是把应用文件拷过去但系统的服务编排不会自动带过来。4.4 第四步翻日志找续期的真实执行记录不要光看配置日志会告诉你真正发生了什么。Lets Encrypt 客户端的日志默认在/var/log/letsencrypt/letsencrypt.log。这个日志会记录每次 renew 的调用时间、检查结果、跳过原因或失败原因。grep -i renew /var/log/letsencrypt/letsencrypt.log | tail -n 50正常运维中你会看到两类常见输出。一类是“Certificate not yet due for renewal”说明续期任务执行了但证书剩余天数超过 30 天自动跳过这是健康状态另一类是“Attempting to renew cert (你的域名) from /etc/letsencrypt/renewal/你的域名.conf produced an unexpected error”说明续期尝试过但失败了。失败原因一般在日志下面几行比如验证超时、连接被拒、文件找不到等。如果日志里连 renew 相关的记录都没有那基本可以断定定时任务没跑过。这时候别去折腾证书先去修 cron 或 systemd timer。5. 服务器迁移后的正确姿势与抢救方案5.1 迁移前的备份清单证书不能只备份文件迁移服务器最稳的做法是把整个 certbot 状态目录打包带走而不只是 live 目录里的几个 pem 文件。Lets Encrypt 客户端的工作目录一般包括/etc/letsencrypt主状态目录含证书、账号密钥、renewal 配置/var/lib/letsencrypt签发相关的缓存状态/var/log/letsencrypt客户端的日志如果只复制/etc/letsencrypt/live/目录certbot 就不知道这个证书对应哪个 ACME 账号、用的哪种验证方式、历史续期记录如何。后面续期很容易出问题。所以我在迁移时会做一次完整打包tar -czf letsencrypt-backup.tar.gz /etc/letsencrypt /var/lib/letsencrypt这个包拿到新机器之后先安装相同大版本的 certbot再解压到对应目录。需要注意的是解压时最好先停掉 cron避免恢复一半时定时任务开始执行造成状态错乱。但有个关键点必须说清楚迁移状态目录不等于迁移完整的证书“控制权”。你虽然把账号密钥复制过去了域名验证环节照样得重新走一遍因为 ACME 协议要求续期时重新证明你有域名的控制权。只要验证通道正常这只是多花几秒的事但如果你原封不动把旧机器上的配置拿过来新机器上的 webroot 路径对不上那照样失败。5.2 迁移后的第一件事做一次手动续期测试很多人迁完服务器习惯把所有操作列成清单逐个执行但总把证书续期放到很后面“等它自动续”。我强烈建议迁移完之后第一件事就是对证书做手动续期测试而不是等定时任务自己去跑。certbot renew --dry-run这个命令是续期的“排练”走完整套验证流程但不会真正签发新证书。如果 dry-run 失败趁着你人还在服务器旁边赶紧处理如果 dry-run 通过自动续期大概率不会再出什么大问题。为什么非要手动跑一遍而不是信定时任务因为定时任务只在固定时间触发下一轮可能是 12 小时之后。你迁移完当天发现证书有问题那还行但如果是第二天早上监控炸了再处理影响面就大了。手动跑一遍 dry-run等于把问题从被动等监控变成主动排查。5.3 证书如果已经过期抢救流程是什么证书过期不可怕可怕的是过期了你不处理导致服务长时间不可用。如果已经确认过期抢救流程通常如下。第一步直接续期。只要域名验证条件还成立80 端口能访问或者 DNS 解析正常跑certbot renew如果提示“Certificate not yet due for renewal”说明它认为还没到续期时间这时可以用--force-renewal强制续certbot renew --force-renewal第二步重新加载 Web 服务器。证书文件更新后nginx 或 Apache 不会自动重新读取要手动执行nginx -s reload或systemctl reload nginx。这一步漏掉很常见证书文件是新的但运行中的 Web 进程用的还是内存里缓存的旧证书等于白续。第三步如果续期一直失败不要在同一份配置上空转果断换验证方式或重新签发。比如原来用 HTTP-01现在 80 端口被其他服务占了那就临时改用 DNS-01 或 standalone 模式重新签发certbot certonly --standalone -d 你的域名这个方法会在 80 端口起一个临时服务来完成验证需要你暂时停掉占用 80 端口的应用。签发成功后别忘了把 nginx 配置指到新证书路径。5.4 验证自动续期真的会跑日志是个好东西手动测试通过以后还要验证定时任务确实会跑而不是靠手动刷出来的“假健康”。我的做法是第二天早上回来看日志tail -n 50 /var/log/letsencrypt/letsencrypt.log正常情况能看到类似“No renewals were attempted”的记录说明定时任务执行了只是证书还在有效期内所以什么都没动。这一条日志就是“自动续期机制还活着”的最好证明。如果日志里连一行都没有再去查 cron 服务是否在运行、systemd timer 是否启用。有些系统上 certbot 包的 cron 文件写的是0 */12 * * *它的执行时间是固定的如果你刚好在整点前检查日志就会造成“没执行”的假象。所以看日志时别只看有没有还要结合 cron 的执行周期和时间点判断。6. 常见问题与排查技巧整理6.1 证书类故障排查速查表症状可能原因排查命令/位置解决方向浏览器报 NET::ERR_CERT_AUTHORITY_INVALID证书链不完整openssl s_client -showcerts配置 fullchain.pem 而不是 cert.pem证书在有效期内但浏览器仍报错服务器时间不对date / timedatectl启用 NTP 同步certbot renew 报域名验证失败80 端口被占用或防火墙阻断curl -I http://域名/.well-known/acme-challenge/test检查 webroot 和端口监听certbot certificates 无任何输出certbot 状态目录丢失ls /etc/letsencrypt/live从备份恢复状态目录定时任务存在但日志沉默cron 服务未启动或 timer 异常systemctl status cron; systemctl list-timers启用并重启对应服务迁移后证书文件在但续期失败renewal 配置路径漂移cat /etc/letsencrypt/renewal/*.conf修正配置文件中的目录路径老设备访问报证书不受信任旧系统根证书库过期检查服务器证书链是否完整启用兼容链或手动安装 ISRG Root X1这个表里最容易被忽略的是“证书链不完整”因为桌面浏览器会自动补链让你误以为一切正常。真正的生产环境、移动 App、API 调用很多都不会做补链操作。凡是给外部客户提供 HTTPS 服务的人都应该在部署后用 openssl 抽查一遍链的完整性。6.2 独家避坑细节第一别把“证书还有效”当成万事大吉。Lets Encrypt 证书是 90 天有效期但自动续期在剩余少于 30 天时才会动。也就是说你在证书第 60 天时查看“一切正常”是没意义的因为续期逻辑还没触发。真正有效的检查方式是看续期任务的执行记录或者做一次 dry-run而不是只看证书有效期。第二服务器时间漂移是隐形杀手。证书验证本质上是一个时间戳博弈客户端验证证书时会检查有效期ACME 验证时会检查签名时间如果新机器上 NTP 没配好系统时间比实际时间慢了几分钟或快了几分钟握手阶段就可能出现各种诡异错误。这类问题很难从证书文件本身找到线索所以迁移服务器后第一件事就是把时间同步好。第三迁移后旧服务器不要立刻销毁。我遇到过一种最头疼的情况旧服务器还开机cron 任务还在跑它会在证书到期前尝试续期并且因为旧机器的验证通道没问题它居然能续成功——但新服务器上配置的还是旧证书。于是新旧两台机器都有证书你新机器上发的请求和旧机器上发的请求互相搅局配置改来改去根本分不清哪份是真正生效的。所以迁移完成后要么把旧机器的 certbot 定时任务停掉要么直接关机下线不要留一个“僵尸续期者”在后面捣乱。第四验证方式选择上做个取舍。HTTP-01 简单但依赖 80 端口和 webroot 目录的写权限DNS-01 通过域名解析服务商的 API 添加一条 TXT 记录完成验证不依赖 Web 服务器特别适合域名后面挂 CDN、负载均衡、多节点部署的场景。如果经常做服务器迁移我推荐优先考虑 DNS-01因为验证的是 DNS 记录而不是某台服务器上的文件迁移后只要 DNS 解析正常续期基本不受影响。6.3 监控证书续期最简单的实现方式把证书续期纳入监控是运维里基础且重要的动作。最简单的做法在 crontab 里写一个脚本每天检查所有证书的剩余天数低于阈值就发告警。我通常写一个 python 或 shell 脚本核心逻辑就是用 openssl 解析过期时间然后拿当前时间和过期时间做差。sort 一下剩余天数最小的那个就是最紧急的。如果低于 20 天就通过 webhook 发到网关群或短信平台。脚本跑完之后顺手再做一次 dry-run把输出追加到日志certbot renew --dry-run /var/log/certbot-dry-run.log 21这样既能提前发现快过期的证书又能验证续期逻辑本身是否健康。比单纯看有效期更能反映真实情况。提示监控脚本的时间阈值要设置合理别设成 30 天因为 certbot 自己在 30 天时才会主动续期你的告警阈值应该比它更早比如 45 天或 20 天才能提前暴露问题。6.4 关于 Lets Encrypt 根证书兼容性的提醒聊到证书信任机制有一个点必须提醒Lets Encrypt 当前的根证书 ISRG Root X1 已经全面接管了信任链。现在的中间证书签发的叶子证书在绝大多数现代操作系统、浏览器里都没有问题。但是一些老设备、旧版 JDK、嵌入式系统根证书库还停留在好几年前里面没有预置 ISRG Root X1访问你的 HTTPS 站点就会报“证书不受信任”。这种问题跟你的证书配置没关系是客户端信任库太旧。解决办法有两个方向一是给老系统手动安装 ISRG Root X1 根证书二是使用保留了旧兼容交叉链的中间证书。现在 certbot 默认签发的证书链里很多已经用上了交叉签名机制目的就是兼容那些旧信任库。不过这种事在普通 Web 站点上遇到的不多大多数出现在企业内网、工业设备、老手机 App 这些特殊环境里。如果你服务的用户群体里有这类设备迁移后一定要用老设备实测一下别等到用户报障再处理。7. 写在最后我的实际体会经历了这台服务器的迁移之后最大的体会是Lets Encrypt 的自动续期确实可靠前提是你把“自动”两个字背后的环境条件都伺候好了。它不是一个开箱即用的黑盒而是一个依赖 cron 或 systemd、依赖域名验证通道、依赖配置不漂移的自动化体系。服务器迁移这种大动作天然会破坏这三样里的至少一样所以迁移后必须专门针对续期做一次体检而不是等它自己跑。最后分享一个我养成的习惯每次做完服务器迁移都会把证书相关的验证写进迁移验收清单顺序是——证书文件就位、私钥匹配、证书链完整、域名解析生效、dry-run 续期通过、定时任务正常运行、日志输出正常。全部走完一遍才敢说这次迁移真的完成了。证书续期这种小事平时不显山不露水出问题时往往已经影响线上访问。提前多花十分钟后面几个月都能睡得安稳。