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

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

chinaz
chinaz 初级会员萌新站长
发布于 2026-10-28 16:53 ·0 浏览 ·9 回复
内容摘要

迁移 SSL 证书应整体打包 `/etc/letsencrypt` 及续期配置并保持目录结构和权限,而非只拷贝证书与私钥;迁移后需校验证书私钥匹配、续期链路和证书链完整性。

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

全部回复 9

域名注册
域名注册 版主萌新站长 👑黑卡会员 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 安全。