ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Let‘s Encrypt证书迁移后自动续期失效?完整排查步骤与体检清单

Let‘s Encrypt证书迁移后自动续期失效?完整排查步骤与体检清单 1. 迁移后的第60天证书到期告警打破了自动续期的安心感事情发生在上个月。一台跑了两年多的业务服务器准备退役我把 nginx、PHP、数据库全部迁到了新机器域名解析也切过去了SSL 证书文件用rsync原样拷了过去。搬完之后页面正常、接口正常、后台正常我以为这件事就这么结束了。直到第 60 天左右监控邮箱收到一封告警邮件证书将在 14 天后过期。我第一反应是误报吧Lets Encrypt 不是会自动续期吗。登服务器一查/etc/letsencrypt/live/下面证书的实际到期时间确实就在两周后而且上次成功续期的日期恰好是迁移之前。也就是说迁完服务器之后自动续期就再也没成功过。带着到底哪里断了的疑问我把整个 Lets Encrypt 自动续期的链路从头捋了一遍。这篇博文就是把当时的排查过程、背后的原理、以及现在每次迁机后我必做的一套检查清单整理出来。不管你是自己跑 VPS 的独立开发者还是公司里管着一批服务器的运维只要你的 HTTPS 证书用的是 Lets Encrypt这篇文章应该能帮你少踩几个我踩过的坑。先说结论避免你读完还悬着心Lets Encrypt 证书本身是会尝试自动续期的但它依赖的自动不是你想象的那种魔法而是由一套链条组成的——定时任务、certbot 配置、验证端口、DNS 解析、Web 服务器 reload 钩子。任何一个环节在迁移时被无意破坏续期都会静默失败。而且最坑的是失败的头一个月几乎没有任何告警因为证书还没到紧急续期窗口。2. 自动续期不是魔法90 天有效期和三个默认前提2.1 为什么 Lets Encrypt 把证书有效期压到 90 天要理解自动续期先得理解为什么 Lets Encrypt 的证书只有 90 天有效期。相比传统付费证书动辄一年、两年90 天看起来像是给自己和用户找麻烦。但这是有意为之的设计核心逻辑就两个字降低损失。证书本身是一个信任凭证一旦私钥泄露攻击者就可以用它伪装你的网站。有效期越长泄露后的危害时间窗就越长。90 天设计配合自动化续期让证书哪怕泄露了最坏也就是在几十天内失效。这本质上是把安全成本和运维自动化做了交换——前提是你的自动化要真的跑得起来。之所以说这个是因为我看到太多人把Lets Encrypt 自动续期当成一个默认存在的功能就像服务器开机自动启动一样理所当然。实际上它更接近系统自带了一个定时提醒但闹钟响了之后能不能成功续上取决于你手机有没有电、闹钟有没有关、网络有没有断。2.2 certbot 的续期齿轮renew 命令和定时任务Lets Encrypt 官方推荐的客户端是 certbot。当你用certbot certonly或certbot --nginx签发出第一张证书时certbot 并不只是生成证书文件它还会在/etc/letsencrypt/renewal/下写入一个针对该域名的续期配置文件在系统里注册一个定时任务定期执行certbot renew记录 ACME 账号私钥到/etc/letsencrypt/accounts/定时任务的方式取决于操作系统。Debian/Ubuntu 上通常是 systemd timerCentOS/RHEL 上则一般是/etc/cron.d/certbot。你可以用下面这些命令确认自己的定时任务长什么样# Debian/Ubuntu systemctl status certbot.timer systemctl list-timers | grep certbot # CentOS/RHEL 等 cat /etc/cron.d/certbot定时任务本身并不做每天续期这种激进操作。certbot renew 每次运行时会检查每张证书的剩余有效期只对距离到期不足 30 天的证书执行续期。也就是说如果你证书刚签发没几天运行certbot renew会很快结束并输出类似Cert not yet due for renewal的提示这属于正常现象。这个不到 30 天不续期的窗口是理解很多续期异常的关键。比如你在迁移后第 10 天检查定时任务发现它在跑日志也没报错但你不会意识到它每次其实都是因为还没到窗口而直接跳过。等到真正进入 30 天窗口后如果验证环节有问题才会开始暴露失败。这就是为什么问题会拖延很久才显现。2.3 一次续期涉及到的文件比你想象的多很多初学迁移的人以为把证书文件拷过去就行了但实际上一次续期涉及到的远不止/etc/letsencrypt/live/下那几个 pem 文件。要理解这一点得先弄清楚 certbot 的目录结构/etc/letsencrypt/ ├── accounts/ # ACME 账号私钥和注册信息 ├── archive/ # 历次签发的证书版本归档 ├── live/ # 当前生效证书的符号链接目录 └── renewal/ # 每个域名的续期配置live/目录里的fullchain.pem、privkey.pem等其实是指向archive/下具体版本文件的符号链接而不是实体文件。很多人用scp或rsync同步时如果没有加-L或没有连同archive/一起同步新服务器上拿到的就是一堆指向不存在的破链接。这是一个非常容易踩的坑。renewal/下的配置文件同样重要它记录了该证书是用什么方式验证的HTTP-01 还是 DNS-01、验证目录在哪、续期成功后要执行什么钩子命令。如果迁移后 Web 服务器根目录变了而这个配置文件里写的还是旧路径那么 HTTP-01 验证就会在最后一步失败。3. 服务器迁移中最容易断掉的续期三件套把自动续期抽象成一个简单模型可以拆成三件套定时任务有没有跟着过来、验证用的入口是否还通、续期成功后的重载钩子是否还能生效。我在这次排查中发现这三样东西各自都埋着坑。3.1 第一件certbot 的整体目录没有完整地迁移先说我最开始的失误。当时图省事只把/etc/letsencrypt/live/里的证书文件用scp拉了过去以为能用就行。结果新服务器的 nginx 虽然加载了证书、HTTPS 也能访问但 certbot 的renewal/配置文件、accounts/账号信息全都不在。这种情况下即使定时任务存在certbot renew 也会因为找不到对应的续期配置而直接略过这张无主证书或者更糟——把它当成一张全新证书去重新申请从而触发速率限制。正确的迁移方式是直接打包整个/etc/letsencrypt目录# 在旧服务器上打包 sudo tar czf letsencrypt-backup.tar.gz /etc/letsencrypt # 传到新服务器后解压 sudo tar xzf letsencrypt-backup.tar.gz -C / # 如果之前已经安装过 certbot需要先停止相关服务再覆盖注意解压后要检查一下权限/etc/letsencrypt下的文件必须属于root:root。我见过有人解压后所有文件变成1000:1000导致 certbot 报 Permission denied。另外如果你迁移前使用的 certbot 版本和迁移后不一致也要留意配置兼容性。比如旧版本用tls-sni-01验证方式新版本已经移除了对它的支持这种情况在 2020 年前后特别常见。3.2 第二件定时任务不会自动跟着你搬家如果你的迁移方式是旧服务器直接宕机新服务器从零装系统那定时任务十有八九是丢的。我之前犯的另一个错就是以反正我都用 docker 部署了为由在新机器上重新装 certbot然后纯粹手动签了一张证书完全没配置任何定时任务。检查方法其实很简单# 看 systemd timer systemctl list-timers | grep certbot # 看 cron ls /etc/cron.d/certbot crontab -l | grep certbot # 最直接的方式——手动跑一次 dry-run sudo certbot renew --dry-runcertbot renew --dry-run会进入模拟模式真实地去 Lets Encrypt 服务器发起验证请求但不会实际替换证书文件。如果这一条命令输出Congratulations或者success说明续期链路整体是通的如果报错那错误信息往往就是根因。这也是我最推荐的第一步排查手段。3.3 第三件验证不通、重载不了才是大多数失败的根源定时任务和目录都在不代表续期就一定成功。HTTP-01 验证的本质是Lets Encrypt 的服务器从公网访问http://你的域名/.well-known/acme-challenge/xxx如果拿到预期内容就证明你对域名有控制权。这个机制决定了以下三个条件缺一不可域名的 DNS 解析指向当前这台服务器且解析已经生效服务器防火墙/安全组放行了 80 端口Web 服务器nginx/apache没有拦截/.well-known/acme-challenge/路径迁移场景下最容易出问题的是第一种。如果你是在旧服务器还活着的时候切了解析那么 DNS 的 TTL 会导致全球各地的验证服务器在一段时间内仍然访问旧服务器恰好旧服务器上的 certbot 可能也在尝试续期造成新服务器日志报错、旧服务器签到一半的混乱局面。另外certbot 在续期配置里通常会有一个renew_hook或deploy_hook最常见的就是--deploy-hook systemctl reload nginx。如果迁移后你的 nginx 服务名变了比如从nginx变成nginx-mainline或容器化的docker exec nginx nginx -s reload这个钩子就会执行失败。症状是证书续期其实成功了新证书文件也落盘了但 Web 服务器还在读旧证书。这种情况下 HTTPS 不会立刻报错而是会继续用旧证书服务直到它过期。4. 完整排查链路从 dry-run 到日志再到手动续期4.1 第一步跑一次 dry-run看它到底卡在哪回到我这次的排障现场。发现证书快到期后我先在新服务器上执行了sudo certbot renew --dry-run命令很快就返回了失败关键输出大致是Attempting to renew cert (example.com) from /etc/letsencrypt/renewal/example.com.conf produced an unexpected error: Problem binding to port 80: Could not bind to IPv4 or IPv6.这个报错直接定位了问题80 端口绑不上。当时我就意识到很可能是新服务器的 nginx 配置里把 80 端口的请求全部 301 跳转到 HTTPS 了而重新绑定端口失败是因为 certbot 在验证时需要一个 80 端口的临时监听或者它检测到端口被其他进程占用且没有正确的代理转发规则。这里要区分两种情况很多人容易搞混如果 certbot 使用webroot 方式在renewal/*.conf中配置authenticator webroot它需要 Web 服务器能正常处理/.well-known/acme-challenge/下的请求并不需要 certbot 自己监听 80 端口。如果使用standalone 方式配置里是authenticator standalonecertbot 会自己短暂占用 80 端口来做验证。这时候如果 nginx 也在监听 80就会冲突。我这次遇到的就是第二种。旧服务器的 80 端口其实一直是交给 nginx 处理的所以旧机器的 certbot 是通过 webroot 做的验证。但迁移后我重装 certbot 时不知怎么的续期配置变成了 standalone 模式导致它和 nginx 抢端口。4.2 第二步翻阅 certbot 日志找到最底层的原因dry-run 的报错只是冰山一角想要完整的上下文还得看日志。certbot 的日志放在tail -n 100 /var/log/letsencrypt/letsencrypt.log里面能看到从启动、加载配置文件、发起 ACME 请求到最终失败的完整链路。我切换 webroot 方式后重新 dry-run发现新的报错变成了Invalid response from http://example.com/.well-known/acme-challenge/xxx: 404这就很有意思了——说明 certbot 把验证请求代理到 nginx 了但 nginx 返回的是 404。我检查了 nginx 的配置才发现新服务器的 root 目录和旧服务器不一样旧配置里 webroot 写的是/var/www/html但新机器上我用的是/usr/share/nginx/html。续期配置里的webroot_path却还是旧的。修正方式很简单在续期配置文件或命令行里更新 webrootsudo certbot renew --dry-run --webroot-path /usr/share/nginx/html如果确认没问题把续期配置文件/etc/letsencrypt/renewal/example.com.conf里的webroot_path一并改掉否则下次定时任务还是会用老路径去验证、继续失败。4.3 第三步手动真实续期一次但别手滑触发速率限制dry-run 通过之后我决定手动真实续期一次确保万无一失。这里有个操作纪律先 dry-run再真实执行真实执行后立刻检查证书文件时间戳和 Web 服务器是否 reload。最好不要一上来就certbot renew --force-renewal因为--force-renewal会无视续期时间窗口强制重新签发。反复执行它很容易触碰到 Lets Encrypt 的速率限制比如每个域名每周最多签发 5 张重复证书。普通情况下手动续期只需要sudo certbot renew由于此时证书已经进入距离到期不足 30 天的窗口renew会真正执行续期而不是跳过。如果你只是想立刻刷新证书比如你改了域名配置或换了私钥那就用sudo certbot renew --force-renewal执行完我会按顺序验证三件事# 1. 看证书文件的修改时间 ls -l /etc/letsencrypt/live/example.com/fullchain.pem # 2. 看证书实际到期时间 openssl x509 -enddate -noout -in /etc/letsencrypt/live/example.com/fullchain.pem # 3. 确认 nginx 等 Web 服务器真的重载了新证书 sudo systemctl status nginx顺便提一句检查线上 HTTPS 证书被实际使用哪个到期时间可以这样echo | openssl s_client -connect example.com:443 -servername example.com 2/dev/null | openssl x509 -noout -dates这个命令走一遍公网路径能避免服务器本地证书是新的但线上访问还是旧证书这种误导。4.4 一个值得记住的细节live 目录是符号链接在手动续期验证过程中我还发现一个问题/etc/letsencrypt/live/example.com/fullchain.pem实际是指向../../archive/example.com/fullchain1.pem的符号链接。如果你在迁移时只复制了live目录没有把archive目录一起复制那么即使新服务器上这些文件看起来存在openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem也会报错no such file or directory。这也是为什么我特别强调迁机时不要只挑看起来有用的文件拷贝直接把整个/etc/letsencrypt目录打包带走才是正解。如果你已经在只有 live 目录而没有 archive 目录的残缺状态下最简单的办法是删掉 live 目录下失效的符号链接让 certbot 重新签发但那样的话你其实丢掉了原有私钥需要重新走一遍域名验证别怕麻烦。5. 两种让人困惑的续期假象与一个老旧环境兼容问题排查过程中我还遇到了几个更隐蔽的坑单独拎出来说说因为它们比端口没开路径写错更容易让人绕圈。5.1 假象一新服务器没续期旧服务器却还在续如果你迁移时没有立刻停掉旧服务器仅仅是切了解析可能出现一种诡异现象新服务器上怎么看都正常但证书就是不更新。我当时一度怀疑解析没切干净拿dig example.com一查新 IP 已经生效了但 Lets Encrypt 的验证服务器可能还带着旧的 DNS 缓存。更头疼的是如果旧服务器上的 certbot 定时任务还在运行它会检测到域名解析虽然改了但我还有配置文件那我继续续期——实际上一旦解析完全生效旧服务器的 HTTP-01 验证就会失败。但它会一直试探而新服务器则在等待验证请求。这就造成新机器日志一片寂静旧机器日志一堆报错的假象。处理办法是迁移后立即在旧服务器上停止 certbot 定时任务不管旧服务器是否还会运行一段时间因为 DNS 的 TTL 最长可能有几小时甚至几天这段时间内验证流量依然有可能打到旧机器。# 旧服务器上禁用 certbot 定时任务 sudo systemctl stop certbot.timer sudo systemctl disable certbot.timer # 或者直接注释掉 /etc/cron.d/certbot 里的配置5.2 假象二通配符证书的 DNS-01 验证在迁移后静默失败如果你的域名证书是*.example.com这种通配符证书那它只能通过 DNS-01 验证无法用 HTTP-01。DNS-01 的原理是Lets Encrypt 要求你在域名的_acme-challenge.example.com下添加一条 TXT 记录内容是一段随机 token验证服务器去查询这条记录是否匹配。我帮朋友排查过一个案例他自己用 acme.sh 脚本托管在 Cloudflare 上迁移完服务器之后手动跑acme.sh --renew -d *.example.com一切正常但定时任务里就是一直失败。后来发现他在新服务器上没有复制 acme.sh 的 cloudflare API 凭据文件导致脚本在尝试自动添加 TXT 记录时被 Cloudflare 拒绝了。如果你是用 certbot 某种 DNS 插件做通配符续期迁移后一定要检查三样东西DNS 插件的凭据文件是否已经复制到新服务器凭据对应的 API Token 权限是否仍然有效很多 Token 会因账号变动或安全策略被撤销新服务器上的 Python 版本/依赖库和旧服务器是否一致DNS 插件经常会被版本问题卡住如果用的是手动添加 TXT 记录的方式那就更要注意Lets Encrypt 验证时要求 TXT 记录在验证期间持续存在你手动加完后不要很快删掉。而且_acme-challenge记录的 TTL 建议设置短一点否则验证会有延迟。5.3 让人误以为证书坏了的老旧 JDK 兼容问题还有一个和续期关系不大、但迁移后容易被误判为证书故障的问题就是老版本的 Java 或者其他老应用内置的信任库不认识 Lets Encrypt 的根证书 ISRG Root X1。Lets Encrypt 在早期依赖 IdenTrust 的 DST Root CA X3 交叉签名来兼容老设备但后来交叉签名的兼容期结束现在新签发的证书链主要锚定在 ISRG Root X1 上。对于较老的环境比如 Java 8u101 之前的 JDK或者某些嵌入式设备自带的 CA 列表它们可能没有内置 ISRG Root X1于是访问 HTTPS 站点时会抛出PKIX path building failed之类的信任错误。如果你迁到的新服务器上恰好跑着一个旧版本 JDK 的 Java 应用遇到 HTTPS 调用失败不要一上来就觉得是 Lets Encrypt 证书文件坏了先检查一下 JDK 的cacerts里有没有 ISRG Root X1。如果确认是老 JDK通常的解法是手动把根证书导入cacerts或者升级 JDK 到 8u101 以上版本。这个问题在从新环境迁移到更旧环境时特别容易踩到。6. 迁机后的续期体检清单我现在的固定操作流程经过这次教训我把服务器迁移后的证书检查做成了一套固定动作每次迁完机、甚至每次动完防火墙或 DNS 配置都会按这个列表走一遍。这里分享出来你可以直接复制成自己的 check-list。6.1 迁移之前先备份整个 certbot 目录打包前先确认 certbot 是正常运行的并且先做一次之前的续期状态检查sudo certbot certificates sudo tar czf letsencrypt-backup-$(date %Y%m%d).tar.gz /etc/letsencrypt把备份文件传到新服务器上之后解压前最好先停掉新服务器上的 certbot 相关服务避免它在你解压的过程中写入文件造成冲突。如果你用的是容器化方案注意 certbot 挂载的卷也要一起备份。6.2 迁移之后逐项核对以下命令我把它拆成了四步第一步确认目录完整sudo ls -l /etc/letsencrypt/live/example.com/ sudo cat /etc/letsencrypt/renewal/example.com.conf sudo ls /etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory/注意看一下live目录下是不是符号链接链接是不是指向了archive里存在的文件。第二步确认定时任务存在且启用systemctl status certbot.timer systemctl list-timers | grep certbot如果没有 timer就手动创建一个或者把 cron 配置补上。Debian/Ubuntu 下最简单的方式是重新安装 certbot它通常会自动注册 timer。也可以直接手动写sudo systemctl enable certbot.timer sudo systemctl start certbot.timer第三步确认验证链路通sudo certbot renew --dry-run这一步是核心。只要它通过了就说明 80 端口HTTP-01或者 TXT 记录添加流程DNS-01是通的定时任务即使现在什么都不做等进入 30 天续期窗口时也大概率能成功。第四步做一次真实续期可选但推荐如果迁移时证书剩余时间已经不多或者你想逼系统把新证书生成一遍来测试完整链路可以执行一次sudo certbot renew --force-renewal --deploy-hook systemctl reload nginx执行完重复 4.3 里的验证命令确认线上证书到期日期是新的。6.3 加一道人为巡检之外的保险证书到期监控定时任务、配置、验证这些都正常不代表以后不会出问题。比如某天你不小心改了 nginx 配置导致 80 端口不再转发验证请求证书到期前 30 天自动续期就会开始失败但你在到期前一两周可能完全感知不到。我的做法是加了两层监控第一层用一个定时脚本检查本地证书到期时间少于 30 天就在日志里告警#!/bin/bash cert_file/etc/letsencrypt/live/example.com/fullchain.pem expires$(openssl x509 -enddate -noout -in $cert_file | cut -d -f2) expires_epoch$(date -d $expires %s) now_epoch$(date %s) days_left$(( (expires_epoch - now_epoch) / 86400 )) if [ $days_left -lt 30 ]; then echo Certificate expires in $days_left days fi你可以把它写进 cron每天跑一次配合邮件或者 IM 群机器人推送告警。第二层用一个外部监控服务从公网角度检查证书有效期比如 Uptime Kuma 或者各种 SaaS 监控。这层监控的价值在于能发现服务器本地证书正常但线上不被信任的诡异问题比如前面提到的旧 JDK 场景、或者 CDN 上还在用旧证书的场景。6.4 迁移场景里的最后一个建议先 dry-run再切解析如果你有条件在切 DNS 解析之前先让新服务器预演一次续期尽量这么做。具体玩法是在新服务器上把 certbot 配好但在确认解析切换之前不要运行真实续期而是跑 dry-run——dry-run 会模拟验证过程但不会真的改证书所以你不需要担心验证服务器找不到新 IP 的问题。此时即使失败也只是报错不影响任何正常服务。等解析切完之后再手动跑一次 dry-run确认验证链路连通之后就可以放心让定时任务接管了。这套切解析前 dry-run、切解析后 dry-run、到期前再跑一次真实 renew的三步节奏我用了很久几乎没再遇到过证书悄然过期的尴尬。写在最后的个人体会这次排查给我最大的触动不是某个命令有多难而是自动续期这四个字太容易让人放松警惕了。Lets Encrypt 把证书生命周期缩短到 90 天初衷是逼着运维自动化但自动化本身也需要被纳入运维的范围。定时任务、验证方式、webroot 路径、DNS 解析、reload 钩子每一环都是独立的变量任何一环在迁移时被意外改动整个链条就会悄悄断掉。我现在每迁一台服务器都会强迫自己把certbot renew --dry-run这个命令跑到Congratulations再往下走。这个习惯帮我避免了很多一个月后才爆雷的隐患。如果你也有自己的迁机流程希望这篇博文里提到的那些细节能帮你把证书这块的坑提前填平。
返回列表