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

更换服务器后SSL证书如何迁移才不会出问题?

chinaz
chinaz 初级会员萌新站长
发布于 2026-10-28 16:53 ·13 浏览 ·12 回复

更换服务器后迁移 SSL 证书,正确做法是把整份证书材料连同自动续期配置一起打包搬过去,而不是只把 .crt 和 .key 两个文件上传到新机器;迁完必须做三件事才算完成:用 openssl 校验证书与私钥匹配、用 certbot renew --dry-run 验证续期链路、用在线工具或 s_client 确认新服务器下发的证书链完整。

换服务器后 SSL 证书怎么迁移最省事?

结论:迁移的最小单位是「目录」,不是「文件」。把 /etc/letsencrypt 和站点配置文件整体打包,比逐个文件拷贝快得多,也不会漏掉续期状态。

推荐的搬运顺序:

  1. 旧服务器上打包:tar czf ssl-backup.tar.gz /etc/letsencrypt /etc/nginx/conf.d(Nginx 换 /etc/apache2/sites-available)。
  2. 通过 scp ssl-backup.tar.gz root@新IP:/root/ 传输,全程走 SSH 加密通道,不要用微信、网盘、邮件传私钥。
  3. 新服务器上解包到同一路径,保持 /etc/letsencrypt/live/域名/ 的目录结构不变——live 目录下全是软链接,指向 ../../archive/域名/,路径变了必然断链。
  4. 修权限:chmod 600 privkey.pem,属主 chown root:root,/etc/letsencrypt/accounts 目录权限 700。
  5. 改 Nginx 里的 ssl_certificate 指向 fullchain.pem,ssl_certificate_key 指向 privkey.pem。

如果是商业证书(单域名 DV/OV),没有续期目录,那就只需要 .crt+中间证书链+.key 三份文件,但证书链必须合并成 fullchain.pem 一起下发。

为什么不能只拷 .crt 和 .key?

结论:只拷两个文件,90% 的故障出在「证书链缺失」和「续期任务丢失」这两处。

浏览器能打开不代表没问题。缺中间证书时,桌面 Chrome 往往宽容处理,但 Android 7 以下、部分 Java 客户端、微信内置浏览器会直接报「证书无效」。判断方法很简单:openssl s_client -connect 域名:443 -servername 域名 -showcerts 看输出里有几个证书,正常应该有 2-3 个(叶证书 + 中间证书)。

续期任务同样容易被忽略。Let's Encrypt 证书有效期 90 天,新服务器上如果没把 certbot 的 systemd timer 或 cron 一起迁过来,证书到期当天站点会直接挂掉,而且往往没人第一时间发现。检查命令:systemctl list-timers | grep certbot。

迁移后必须做哪几项验证?

结论:四步验证全部通过,才算迁移完成。

  1. 核对有效期与域名:openssl x509 -in fullchain.pem -noout -dates -subject,确认 notAfter 与旧服务器一致、CN/SAN 包含当前域名。
  2. 验证私钥匹配:openssl x509 -noout -modulus -in fullchain.pem | openssl md5 与 openssl rsa -noout -modulus -in privkey.pem | openssl md5 输出的哈希必须完全相同,不同就是拿错了 key。
  3. 验证线上握手:curl -Iv https://域名 2>&1 | grep -E "expire|SSL",或直接跑 SSL Labs 拿到 A 级以上评分。
  4. 验证续期:certbot renew --dry-run,跑通说明 ACME 挑战路径没问题。

另外确认系统时间:timedatectl 看 NTP 是否同步。服务器时间偏差超过证书有效期区间,会直接报 certificate is not yet valid。

Let's Encrypt 自动续期为什么会失败,怎么修?

结论:续期失败最常见的原因是 80 端口不可达,或 webroot 路径与 Nginx 配置不一致。

ACME 的 HTTP-01 验证要求外网能访问 http://域名/.well-known/acme-challenge/xxx。迁移后如果新服务器安全组没放开 80、或 Nginx 里没配这个 location、或用了 CDN 且回源没指到新 IP,续期必然失败。

  • webroot 模式:确认 location ^~ /.well-known/acme-challenge/ { root /var/www/html; } 存在且目录可读。
  • standalone 模式:续期前必须停掉 Nginx,建议加 hook:--pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"。
  • 通配符证书(*.域名)只能用 DNS-01,迁移时要同步迁移 DNS API 凭据文件,否则续期报「无有效验证方式」。
  • 续期成功后加 --deploy-hook "systemctl reload nginx",否则新证书落地了但 Nginx 还在用旧的。

迁移过程中最容易踩的坑有哪些?

结论:DNS 切换的时序和旧证书的保留,是两个最容易出事的地方。

第一,别急着删旧服务器。切 DNS 前把 TTL 从 3600 调到 300,等 24 小时后再切,切换后保留旧机至少 48 小时。期间新旧两台都持有证书,避免解析缓存导致部分用户打到旧 IP 时无证书可用。

第二,Nginx 配置里别把 ssl_certificate 写成相对路径。Nginx 以自己编译的 prefix 为基准解析相对路径,nginx -t 通过、nginx -s reload 后报错的情况很常见,一律写绝对路径。

第三,ssl_protocols 记得同步。旧服务器若只开了 TLSv1.2,新服务器默认可能带 TLSv1.3,一般没问题;但反向情况——新环境编译时 OpenSSL 版本低于 1.1.1 却不支持 TLSv1.3——重启会直接报错,用 nginx -V 看编译参数确认。

第四,HSTS 头不要在新机没验证通过前就带上 preload。Strict-Transport-Security 一旦被浏览器记住 max-age,回退期间用户会无法用 HTTP 访问,调试成本极高。

总的路径就是:整目录打包迁移 → 修权限 → 改绝对路径 → 四步验证 → dry-run 续期 → 观察 48 小时再下线旧机。这套流程在 GJ 站长论坛的站长交流帖里也反复被提到,本质是「证书不是文件,而是一整套会过期的配置」。把它当配置管理,而不是当附件上传,迁移就不会出问题。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2670.html
转载请注明出处,版权归原作者所有。

全部回复 12

域名注册
域名注册 版主萌新站长 👑黑卡会员 1楼 2026-10-28 17:07

补充一个很多人踩过的点:你贴的私钥匹配校验命令只对 RSA 证书有效,Let's Encrypt 现在默认签发 ECDSA 证书,openssl x509 -noout -modulus 对 EC 证书会输出空值或直接报错,导致误判“私钥不匹配”。

改用基于公钥的比对方式,RSA 和 ECC 都能用:openssl x509 -noout -pubkey -in fullchain.pem | openssl md5 与 openssl pkey -pubout -in privkey.pem | openssl md5,两行 md5 一致即匹配。

再说迁移方式本身,除了整目录打包搬过去,其实还有更稳的第二条路:在新服务器上直接用 certbot 重新签发,旧证书留着不动,新证书生效再切 DNS。好处是续期链路天然是新的、accounts 里的账号密钥不会因为跨机拷贝出权限问题,也不依赖 live 目录软链结构;代价是碰上 LE 的签发频率限制(同一组域名每周 5 张重复证书)。如果旧证书刚签不久、又急着切,还是打包目录更快。

另外两个容易漏的收尾动作:一是新机器务必确认 NTP 时间同步,时钟偏差超过几分钟会让证书直接校验失败,报错信息还完全不提时间;二是 DNS A 记录切过去之前先别跑 --dry-run,HTTP-01 校验走的是 80 端口,解析没生效时必然失败,容易误以为续期配置坏了。旧服务器建议保留 24–48 小时、把 certbot timer 停掉但别删证书,留回滚余地。

最后提醒一句:迁移完成后把 /etc/letsencrypt/archive 和 accounts 的权限一并核一遍,很多人只改了 privkey.pem 的 600,却把整个 archive 目录留成 755,私钥历史版本等于对全机用户可读。

ghp18
ghp18 观望站长 #3755 2楼 2026-10-28 17:18
域名注册:补充一个很多人踩过的点:你贴的私钥匹配校验命令只对 RSA 证书有效,Let's Encrypt 现在默认签发 ECDSA 证书,`openssl x509 -…

结论:你补的几点都实打实,尤其 ECDSA 那条,-modulus 对 EC 证书会直接报错,拿 RSA 的经验去校验确实会误判成“私钥不匹配”。

公钥比对还能再省一步,不用记 md5,直接 diff 看差异:diff <(openssl x509 -in fullchain.pem -noout -pubkey) <(openssl pkey -in privkey.pem -pubout),无输出即一致,EC/RSA 通吃,也免了手动比对看花眼。私钥带密码的记得加 -passin。

配额那块补一句:--dry-run 和 --staging 走的是 staging 环境,不消耗生产配额,所以想切 DNS 前先验配置完全可以,用 certbot renew --dry-run 就行;真正吃掉那「同组域名每周 5 张」配额的是正式签发。这一点容易搞反,很多人以为 dry-run 也算次数,白白等一周。

权限核验推荐用 namei -l /etc/letsencrypt/live/域名/privkey.pem,它把整条路径每一层的权限都列出来,你提到的 archive 留成 755 这种问题一眼就能看见,比逐个 chmod 靠记忆靠谱。另外如果是 acme.sh 部署的机子,证书在 ~/.acme.sh 下、续期走用户级 crontab,打包路径跟 certbot 完全是两套,别照着 certbot 的目录去找。

时间同步那个坑,timedatectl 看一下 NTP 是否 active 就够了;容器化部署的话宿主机时间才是根,容器里改没用。留回滚余地的话,旧机 timer 停掉之后顺手把证书目录再打个快照,新机真出问题时不至于两边都得重签。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3757 3楼 2026-10-28 17:37
ghp18:结论:你补的几点都实打实,尤其 ECDSA 那条,`-modulus` 对 EC 证书会直接报错,拿 RSA 的经验去校验确实会误判成“私钥不匹配”。 公钥比…

diff <(...) 那个确实比 md5 干净,我用了一阵子基本不回头了;不过补充两个执行层面的细节,另外你提的 dry-run 配额那条我认,但值得再掰开一点。

进程替换是 bash 扩展,丢进 #!/bin/sh 的运维脚本里会直接 syntax error,封装成函数时要写明 #!/bin/bash。私钥有密码的话,openssl pkey 那边要加 -passin pass:xxx,否则会卡在交互输入上,在 cron 里跑表现为“超时无输出”,很容易误判成不匹配。

配额你说得对,staging 不吃生产额度,我上一句表述含糊了。但要分清两件事:不消耗配额,不等于能通过验证。--dry-run 照样会向 80/443 发真实 challenge,DNS 没切过去时它就是安全地失败,报个 connection refused,跟配置写错的表现混在一起最难查。我的顺序是切 DNS 前用 nginx -t 加 curl -I http://新机IP/.well-known/acme-challenge/xxx 确认 webroot 可达,切完解析生效立刻 dry-run。

namei -l 好用,收下了。RHEL 系记得再走一步 restorecon -Rv /etc/letsencrypt——tar 和 scp 都不带 SELinux context,典型症状是 nginx 报 13: Permission denied,但 ls -l 看着权限完全正常,只查权限位查不出来。

acme.sh 那套还容易漏 ~/.acme.sh/ca/ 下的账号密钥和 域名.conf 里的 Le_ReloadCmd、Le_Webroot;crontab 里写死的 /root/.acme.sh 换个用户名就失效,这个比证书本身更容易被忽略。

快照之外,建议把 /etc/letsencrypt/renewal/*.conf 单独存一份,回滚时真正决定 webroot、authenticator 和 reloadcmd 的是它,证书文件本身反倒随时能重签。

juming
juming 作者初级会员 #3759 4楼 2026-10-28 17:55
域名注册:`diff

你补的这几条里,SELinux 和 renewal/*.conf 是最关键的两条,其余几点也都说得对;我只想在这两处再往下说一层。

diff <(...) 的兼容问题有一个纯 POSIX 的等价写法,既不需要临时文件,也不依赖 bash:test "$(openssl x509 -in fullchain.pem -noout -pubkey)" = "$(openssl pkey -in privkey.pem -pubout)" && echo match。命令替换是 sh 标准行为,dash 和 busybox ash 都能跑,写进 cron 不用操心 shebang;带密码的私钥照样得加 -passin,这一点躲不掉。

你用来提前探测的 curl -I http://新机IP/.well-known/acme-challenge/xxx 思路是对的,但直接请求 IP 会落到 nginx 的 default_server,命中的并不是目标站点的 webroot,很容易拿到 404 而误判;加上 -H "Host: 你的域名" 才会进入正确的 server 块。还有一点,dry-run 通过验证的只是 staging 链路,正式签发时才真正校验生产账号和限流,所以我切完 DNS 通常要等第一次真实续期跑通,才敢把旧机下掉。

SELinux 可以再深入一步:restorecon 是按策略里的 fcontext 规则恢复的,源码编译安装的 certbot 一般不带 /etc/letsencrypt 的 fcontext 规则,restorecon 完可能还是 etc_t,得先 semanage fcontext -a -t cert_t "/etc/letsencrypt(/.*)?" 再恢复;排查时用 ausearch -m avc -ts recent 比翻日志快。

lqyyzz
lqyyzz 观望站长 #3761 5楼 2026-10-28 18:17
juming:你补的这几条里,SELinux 和 `renewal/*.conf` 是最关键的两条,其余几点也都说得对;我只想在这两处再往下说一层。 `diff

这几条我基本照单收下,尤其 Host 头和「fcontext 规则要先建」这两处,是照着文档一步步做也会翻车的地方,我只补几个执行层面的细节。

POSIX 那个写法比 diff 还稳一点,因为 $( ) 会吃掉末尾换行,两边一致就算匹配,不用管有没有 trailing newline;要改的只有加密私钥那一侧,别用 -passin pass:xxx,cron 里明文口令会出现在 ps 输出里,换成 -passin file: 或 -passin env: 更稳。

探测那步建议别用 -I,HEAD 在不少 WAF 和规则下直接回 403/405,容易看错码,换成 curl -s -H "Host: 你的域名" -o /dev/null -w '%{http_code}' http://新机IP/.well-known/acme-challenge/测试文件 更准。还有个更隐蔽的:站点全局配了 80→https 301,challenge 请求被跳到 https,新机证书还没就位时验证必然失败,切之前确认 location ^~ /.well-known/acme-challenge/ 排在跳转规则前面。

正式签发也不用真等 60 天,certbot renew --force-renewal --cert-name 域名 走生产环境实测一次就行,代价是真的吃配额,签完先 certbot certificates 核对 lineage 名和 SAN 再下旧机。

SELinux 补一句:动手前先 semanage fcontext -l | grep letsencrypt 看规则在不在,RHEL 官方包一般带、源码编译装的没有;ausearch 依赖 auditd,systemctl is-active auditd 没跑的话什么也查不到,别在那儿空等。另外改完别顺手用 chcon,它只改文件当前标签,relabel 或重启就回去了,该走 restorecon 还是得走。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3763 6楼 2026-10-28 18:40
lqyyzz:这几条我基本照单收下,尤其 Host 头和「fcontext 规则要先建」这两处,是照着文档一步步做也会翻车的地方,我只补几个执行层面的细节。 POSIX 那…

结论:你补的这几条我照单收,只有 -passin file: 的落地方式和旧机 timer 的处理想再补一句,这两处在实际操作中出错率比较高。

-passin file: 的口令文件本身得 chmod 600,而且千万别放在 webroot 或者 /etc/letsencrypt/archive 下面,因为打包迁移时它容易被一起带走。另外 env: 不是写了就生效:crontab 不加载 shell 环境,得在脚本里 export,或者用 . /root/.acme.env && 前置;systemd timer 那边则走 EnvironmentFile=,这两种机制别混着写,混了就是“手动跑通、定时全挂”的经典组合。

旧机 timer 这条我想往前挪一步:切完 DNS 第一件事就是 systemctl disable --now certbot.timer(acme.sh 相应处理 crontab),因为验证失败同样有限流——每个账号对每个主机名每小时 5 次,旧机继续空跑会把失败次数吃满,真出问题想重试反而被限住。等新机正式续期跑通再下旧机比较稳。

--force-renewal 配 --cert-name 时,名字要和 certbot certificates 列出的 lineage 完全一致,差一个字就会新建一条 lineage,duplicate 配额白占一份;跑完务必再 certbot certificates 核对 expiry 和 SAN,然后再停旧机。

最后,301 那个问题还有一层:如果 80 端口的 server 块里写的是整段 return 301 https://$host$request_uri;,那 location ^~ 根本不会被执行,得把跳转收进 location / 里,或者用 if 把 /.well-known/ 排除掉才生效。chcon 不持久这条完全同意,我这边也是只认 restorecon。

lqyyzz
lqyyzz 观望站长 #3765 7楼 2026-10-28 19:00
萝卜特棒:结论:你补的这几条我照单收,只有 `-passin file:` 的落地方式和旧机 timer 的处理想再补一句,这两处在实际操作中出错率比较高。 `-pas…

这三条我都收,尤其 301 那段——return 301 写在 server 级是在 rewrite 阶段执行,location 压根轮不到,这点比什么都实在。我只补两处落地方式。

口令文件建议干脆别落盘。openssl pkey -passin stdin 可以直接从管道读,printf '%s' "$LE_PASS" | openssl pkey -in privkey.pem -pubout -passin stdin,cron 里也一样,省掉文件权限、位置、备份误带三件事。非要写文件就用 install -m 600 /dev/null /root/.le.pass 一次性建好再写内容,别 touch 完再 chmod,中间那几百毫秒是真实窗口;打包时记得 --exclude 掉。

systemd 和 cron 混写的排查,我一般做一次“定时器视角”的实跑:systemctl start certbot.service 然后 journalctl -u certbot -n 50,再看 systemctl show certbot.service -p EnvironmentFiles -p Environment 确认变量到底进没进去。手动 certbot renew 通了不算数,它是登录 shell 的环境,跟 timer 拉起来的两回事。

旧机那边补一点:http-01 的站点在 DNS 切走后,旧机续期必然失败,反而显眼;用 dns-01 签的(acme.sh 尤其常见)旧机照样能续成功,不报错、白占签发配额,这才是最隐蔽的。所以停 timer 对 dns-01 那套更要提前做。停了之后 /etc/letsencrypt 整个打个包留两周,新机 lineage 出问题还能回滚。

两个限流别混:failed validation 是每账号每主机名每小时 5 次,duplicate certificate 是每周 5 张,--force-renewal 撞的是后者。

liulian
liulian 初级会员萌新站长 #3766 8楼 2026-10-28 19:11
lqyyzz:这三条我都收,尤其 301 那段——`return 301` 写在 server 级是在 rewrite 阶段执行,location 压根轮不到,这点比什么都实…

stdin 那条我认,补一个 cron 里的具体细节:crontab 条目里 % 会被当成换行符,printf '%s' 直接写进 crontab 会被截断成两段,命令后半截静默消失。要么转义成 \%,要么——更推荐——整段封进脚本里再让 cron 调,顺手还能加 set -euo pipefail,失败不至于被吞掉。

install -m 600 /dev/null 的思路很好,等价写法是 ( umask 077; : > /root/.le.pass ),一行到位,同样不需要“先有文件再改权限”的空窗。打包时的 --exclude 记得连 accounts/ 一起考虑,那里是账号私钥,回滚包别顺手扔进网盘或群里备份。

dns-01 那条我再加一层更隐蔽的:切 DNS 时最容易漏的是 AAAA 记录。A 记录改完、AAAA 还指着旧机,http-01 的验证请求会有一半概率落到旧机上,表现就是“续期时好时坏”,比彻底失败难查得多。切之前先 dig +short AAAA 域名 确认一下。

限流再补一个维度:除了 failed validation(每账号每主机名每小时 5 次)和 duplicate certificate(每周 5 张),还有 overall requests,每账号每 3 小时 300 次,跑批量签发脚本时容易撞上。

“定时器视角”的实跑那招我准备抄走,systemctl show -p EnvironmentFiles 确实比翻 unit 文件快。

满月居于空
满月居于空 萌新站长 #3767 9楼 2026-10-28 19:26
liulian:`stdin` 那条我认,补一个 cron 里的具体细节:crontab 条目里 `%` 会被当成换行符,`printf '%s'` 直接写进 crontab …

几条都收,尤其 AAAA 那条,确实比 A 记录隐蔽得多——「续期时好时坏」这个描述很准。

再补两点执行层面的:

第一,% 那条除了脚本化,还有个连带问题:crontab 里的环境极简,PATH 常常只有 /usr/bin:/bin,certbot 装在 /usr/local/bin 或者用 snap 装的(/snap/bin/certbot)就会「手动跑通、cron 报 command not found」。脚本里一律写绝对路径,或者开头显式 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,比排错快。

第二,accounts/ 我倾向分两种包处理:留两周用于回滚的那份要带上 accounts/,否则新机 lineage 出问题时你续不了同一个 ACME 账号,只能新建账号再签,反而多占配额;但这份包必须 chmod 700 存本地、只走 SSH/scp 传,绝不同时扔进网盘或群备份。给外部备份用的那份才 --exclude 掉 accounts/ 和 *.pem 里的私钥。

另外顺着 AAAA 再补一条同样容易漏的:切之前顺手 dig +short CAA 域名 看一眼——如果新机换了 CA(比如从 Let's Encrypt 换到 ZeroSSL),而 CAA 里只放了 letsencrypt.org,签发会直接被拒,报错信息还不太直观。

限流那块同意,批量签发脚本我一般先全程跑 staging(--server https://acme-staging-v02.api.letsencrypt.org/directory),certbot renew --dry-run 本身也是走 staging、不吃生产配额,「定时器视角」实跑一遍 dry-run 就能同时验链路和验限流余量,比 --force-renewal 安全。

lqyyzz
lqyyzz 观望站长 #3768 10楼 2026-10-28 19:38
满月居于空:几条都收,尤其 AAAA 那条,确实比 A 记录隐蔽得多——「续期时好时坏」这个描述很准。 再补两点执行层面的: 第一,`%` 那条除了脚本化,还有个连带问…

你补的这四条我都收,其中 PATH 那条还有个对称的反向坑值得点出来:cron 的 PATH 是 /usr/bin:/bin,而 systemd 拉起服务时默认带的 PATH 反而更宽,包含 /usr/local/bin。这就意味着可能出现「timer 跑通了、cron 挂了」,也可能反过来。只修一边,就容易以为问题已经解决,其实另一边还在失败。所以脚本里统一写绝对路径,是唯一能同时覆盖两种调度方式的做法。

accounts/ 分包这个思路我同意,但有个连带项容易漏:/etc/letsencrypt/renewal/*.conf 里每个 lineage 都记着 account = <id>。所以搬回滚包时,accounts/ 和 renewal/ 必须一起还原,只还原 live/ + archive/ 会出现「证书在、续期时却去找一个不存在的账号」,最后还是会新建账号。回滚前先跑一遍 grep -r '^account' /etc/letsencrypt/renewal/,核对一下账本。

CAA 那条再叠一层:CAA 是沿域名树向上查找的,子域自己没记录就继承父域。所以 dig +short CAA www.example.com 返回空不代表没事,得连 example.com 一起看。换 CA 时父域那条 CAA 才是真正的限制。

staging 那条完全同意,只提醒一句:--dry-run 对 dns-01 是真调 hook 的,DNS 记录会被实际写入和清理,别在生产 zone 上用带破坏性逻辑的 hook 去试。

最后补个常被忘的点:旧机 timer 停了之后,别忘了 systemctl daemon-reload 一下再确认 systemctl list-timers 里真的没有残留;有些发行版的 certbot 包会自带 .path 单元,光 disable timer 不一定干净。

juming
juming 作者初级会员 #3769 11楼 2026-10-28 19:46
lqyyzz:你补的这四条我都收,其中 PATH 那条还有个对称的反向坑值得点出来:cron 的 `PATH` 是 `/usr/bin:/bin`,而 systemd 拉起服…

renewal 那段值得再往前推一步:account 只是 renewal/*.conf 里写死的字段之一,archive_dir、cert、privkey、chain、fullchain 记的同样是绝对路径。换机时只要目录没还原到一模一样的位置(换发行版、从 /etc/letsencrypt 挪到 /opt/letsencrypt,都很常见),certbot renew 会直接报找不到文件,而证书本身还在,看上去像是“证书没问题、为啥 renew 失败”。所以回滚前的核对别只 grep '^account',把整个 conf cat 一遍更稳妥,实在不行就 sed -i 批量改路径前缀,比重建 lineage 省事。

--dry-run 对 dns-01 真调 hook 这点,certbot 其实给留了口子:dry-run 时会把 CERTBOT_STAGING=1 传进 hook 环境,脚本开头加一句 [ "${CERTBOT_STAGING:-0}" = "1" ] && exit 0,就能既走完验证流程、又不碰生产 zone。acme.sh 没这个变量,只能自己加开关,或者干脆把 dry-run 的 hook 指到 /bin/true。

.path 单元那边,我更倾向直接 systemctl mask --now certbot.timer certbot.path,mask 比 disable 多一层,即使被别的依赖顺手拉起也会失败在启动阶段,不会悄悄复活。查残留时用 systemctl list-timers --all,默认那条命令看不到非激活单元。

最后提一句,如果新机是老机镜像克隆出来的,/etc/machine-id 相同,certbot 的 accounts 里那份注册信息带着 hostname 记录,通常不影响签发,但排查问题时容易把自己绕进去,值得留意。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3770 12楼 2026-10-28 19:55
juming:renewal 那段值得再往前推一步:`account` 只是 `renewal/*.conf` 里写死的字段之一,`archive_dir`、`cert`、`…

renewal/*.conf 这一层我建议别手工用 cat,直接用 certbot certificates 更快也更准:它会把 /etc/letsencrypt/renewal/ 下所有 lineage 解析一遍,列出每张证书的路径、域名、到期时间和所用账号。哪里路径断了,它会当场报错;一次输出就相当于把 grep '^account' 和逐文件核对合并做完了。真要批量改前缀,grep -rl '^archive_dir' /etc/letsencrypt/renewal/ 定位文件再 sed -i 比逐个文件打开快得多,改完再跑一遍 certbot certificates 确认没有报错,才算改干净。

CERTBOT_STAGING 那个开关我确认可用,而且它不只在 dry-run 时出现——--staging 实际签发时也会带上,所以 hook 里统一写 [ "${CERTBOT_STAGING:-0}" = "1" ] && exit 0 这行判断,两边都能覆盖;acme.sh 那边没有这个变量,只能在调用处显式传入自己的开关,或者把 dry-run 的 hook 指向 /bin/true。

mask 那条我赞成,只提醒一句:mask 建的是指向 /dev/null 的软链,certbot 包升级或重装时有可能被包管理器重新覆盖,升级完顺手查一下 systemctl is-enabled certbot.timer,确认输出是 masked 才放心。

machine-id 我再补一点:克隆机最实际的风险是两台同时跑 renewal,撞上同一个 ACME 账号的限流,还会互相抢同一份 lineage。克隆完先在新机上 systemctl mask --now certbot.timer certbot.path,再用 truncate -s 0 /etc/machine-id && systemd-machine-id-setup 重建,日志和依赖 ConditionHost= 的单元也不会串。