本文从一次多服务器证书过期故障出发,分析多台服务器各自运行 acme.sh 的风险,并给出以单台证书机统一签发、通过 rsync over SSH 分发证书并远程重载 Nginx 的落地方案。
背景
当一个域名(尤其是带泛解析的 *.example.com)同时指向多台服务器时,很多人的第一反应是:每台服务器都装一个 acme.sh,各自续期,各自安装。看起来简单直接,实际上埋下了不少隐患。
笔者最近就遇到了这个问题:两台服务器都托管 lovehistory.top 及其子域名,各自运行 acme.sh 自动续期。某天客户端突然报 CERTIFICATE_VERIFY_FAILED: certificate has expired,排查后发现是其中一台服务器的证书续期失败,但 Nginx 仍在提供旧证书。顺着这条线往下挖,发现”各自为政”的模式存在四个真实风险。
这篇文章记录问题分析和最终采用的单一签发 + 证书分发方案,适用于任何使用 acme.sh、多台服务器共享同一域名的场景。
一、为什么不能让每台服务器各自续期
风险 1:并行续期损坏配置文件
acme.sh 的续期过程会读写 ~/.acme.sh/<domain>/<domain>.conf。如果两台服务器的 cron 恰好同时触发,并行进程可能同时写同一个配置文件,导致文件损坏。虽然概率不高,但一旦发生,该服务器上这个域名的后续续期都会失败,而且报错往往很隐晦。
风险 2:Let’s Encrypt 的重复证书速率限制
Let’s Encrypt 对”完全相同的一组域名”有一个 Duplicate Certificate 限制:每 7 天最多 5 份。这个限制是全局的,不管你用了多少个 ACME 账户、多少台服务器,所有申请合并计算。
正常自动续期通常不受此限,但前提是续期请求来自签发上一份证书的同一个 ACME 账户。如果两台服务器的 acme.sh 使用不同账户,一台的”续期”在 Let’s Encrypt 看来可能是”新申请”,从而消耗配额。反复 --force 测试时尤其容易触发。
风险 3:DNS TXT 记录的竞争
使用 DNS-01 验证时,一个常见的担心是两台服务器同时写入 _acme-challenge 的 TXT 记录会互相覆盖。DNS 协议本身允许一个名称下存在多条 TXT 记录,验证时只要任意一条匹配即可,所以”各自添加、各自删除”理论上可以共存。
真正的风险在清理环节:如果某台服务器的脚本错误地删除了整个 _acme-challenge 名称下的所有记录,另一台正在验证的服务器就会失败。
风险 4(最隐蔽):证书不同步
这是”各自续期”模式最典型的后果。两台服务器各自生成不同的私钥和证书,独立续期,互不知晓。
可能出现的情况是:服务器 A 续期成功,服务器 B 因网络波动或 DNS 延迟续期失败,仍停留在旧证书上。当流量被负载均衡或 DNS 轮询分配到 B 时,用户就会看到证书错误。笔者遇到的正是这个问题——A 的证书已经更新,B 的还是过期的。
二、目标架构:单一签发者 + 证书分发
社区公认的最佳实践是 **”集中签发,统一分发”**:
┌─────────────────────────────────────────────┐ |
优势很直接:
- 只有一台机器持有 DNS API Token,暴露面更小;
- 速率限制只需关注一台机器的行为;
- 所有服务器上的证书始终一致;
- 排查问题只看一台机器的日志。
三、实施步骤
以下以两台服务器为例,服务器 A 作为证书机,服务器 B 为分发目标。假设:
- 域名:
example.com+*.example.com - DNS 托管:阿里云(钩子
dns_ali) - Web 服务:Nginx
- 证书路径:
/etc/nginx/ssl/example.com/
第 1 步:摸清服务器 B 的现状
登录服务器 B,记录关键信息:
# acme.sh 的续期任务 |
重点确认 B 的 Nginx 证书路径是否与 A 一致。如果不同,后续 rsync 目标路径要相应调整,或在 B 上做软链接。
第 2 步:停掉服务器 B 的 acme.sh 自动续期
注意:不要卸载 acme.sh,只删 cron 任务,保留已签发的证书文件,避免 Nginx 立刻报错。
# 备份 crontab |
如果 B 的 ~/.acme.sh/account.conf 里还存着 DNS API Token,建议一并移除,减少暴露面。
第 3 步:在服务器 A 上配置到 B 的 SSH 免密登录
# 生成专用密钥(如果还没有) |
能直接输出 B 的主机名和时间,说明免密成功。
第 4 步:在 B 上准备证书目录
# 在服务器 B 上执行 |
如果 B 的 Nginx 配置引用的路径不同,以 nginx -T 查到的实际路径为准。
第 5 步:在服务器 A 上重新安装证书,扩展 reloadcmd
把 --reloadcmd 从”只重载本地”扩展为”本地重载 + rsync 到 B + 远程校验并重载 B”:
acme.sh --install-cert -d example.com --ecc \ |
这条命令在每次续期成功后会依次执行:
- 重载 A 的 Nginx;
- 把 A 的证书目录同步到 B;
- 在 B 上先
nginx -t校验配置,通过后再重载 B 的 Nginx。
nginx -t &&不能省。如果 B 的配置因证书路径或权限有问题,nginx -t会失败,reload 不会执行,避免 B 直接中断服务。
确认 --reloadcmd 已记录:
acme.sh --info -d example.com | grep -i reload |
应输出:
Le_ReloadCmd=systemctl reload nginx && rsync ... |
第 6 步:手动触发一次分发,验证 B 已更新
不用等续期,直接在 A 上跑一遍 reloadcmd 里的逻辑:
systemctl reload nginx && \ |
然后在 B 上验证:
openssl x509 -in /etc/nginx/ssl/example.com/fullchain.cer -noout -dates -issuer |
期望 B 的证书与 A 完全一致(相同的 notAfter 和 issuer)。
四、验证端到端链路
建议在正式上线前强制续期一次,观察分发是否自动完成:
# 在服务器 A 上 |
然后看日志:
tail -50 /var/log/acme.sh.log |
确认里面有 systemctl reload nginx、rsync、ssh 相关输出且无报错。
注意:
--force不要频繁使用。Let’s Encrypt 对”完全相同域名组”有每周 5 份重复证书的限制,验证一次即可,之后靠自动续期。
五、运维建议
1. 给 acme.sh 的 cron 加日志
默认 cron 输出被重定向到 /dev/null,续期失败时无日志可查。建议改成:
crontab -e |
51 15 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" --log /var/log/acme.sh.log > /dev/null 2>&1 |
这样续期失败时,直接看 /var/log/acme.sh.log 就能定位。
2. 保留 B 上的旧证书备份
在第一次分发成功前,不要删除 B 上的旧证书文件。如果 rsync 因网络问题失败,B 的 Nginx 至少还能用旧证书启动,不会直接 502。
3. SSH 端口不是 22 时
如果 B 的 SSH 端口不是默认的 22,rsync 和 ssh 命令要加 -p <端口>,或写进 ~/.ssh/config:
Host server-b |
之后命令里直接用 root@server-b 即可。
4. CDN 场景要单独处理
如果域名前面还有 CDN(Cloudflare、阿里云 CDN 等),rsync 只解决源站之间的同步。CDN 边缘节点的证书需要在 CDN 控制台单独更新,或开启”回源 HTTPS 跟随源站证书”。
5. 定期检查证书一致性
可以写一个简单的巡检脚本,在多台服务器上分别执行,对比 notAfter 是否一致:
for host in <A_IP> <B_IP>; do |
如果有服务器日期不一致,说明分发链路断了,需要检查 A 的日志和 B 的 SSH 连通性。
六、做完后的状态
| 项目 | 服务器 A(证书机) | 服务器 B(分发目标) |
|---|---|---|
| acme.sh 续期 | ✅ 唯一运行 | ❌ 已停 cron |
| 证书来源 | 本地签发 | 从 A rsync 同步 |
| 自动重载 | ✅ 本地 + 远程触发 B | 由 A 远程触发 |
| 证书一致性 | ✅ 始终一致 | ✅ 始终一致 |
| 排查入口 | /var/log/acme.sh.log |
看 A 的日志 |
总结
多台服务器共享同一域名时,”各自为政”地运行 acme.sh 看似省事,实际会引入并行续期损坏配置、速率限制、DNS 记录竞争、证书不同步四类风险。其中”证书不同步”最隐蔽,往往在流量切换到某台续期失败的服务器时才暴露。
单一签发 + 证书分发的方案并不复杂:选定一台证书机,其余服务器停掉续期,通过 --reloadcmd 中的 rsync over SSH 把新证书推送到所有目标服务器并远程重载。整个改动集中在一条 acme.sh --install-cert 命令里,却能从根本上消除上述所有风险。
如果你也在多台服务器上跑着 acme.sh,建议花半小时按本文步骤切换一次。之后 90 天的续期周期里,你只需要看一台机器的日志。