目录
- 环境
- 为什么是「群晖跑 acme.sh + SSH 推送」
- 遇到的坑
- 坑一:ESXi 不认 ed25519 密钥
- 坑二:`MULTI_CALL` 不够,必须开 `USE_SCP`
- 这个坑真正危险的地方
- 坑三:`auto-backup.sh` 会把私钥打进日志
- 另外几个小提醒
- 最终脚本
- 群晖任务计划配置
- 验证
- 小结
- 参考
家里那台 ESXi 6.7 的 HTTPS 证书过期一年多了,每次打开管理页面都得点"继续访问不安全的网站"。这次把它换成 Let’s Encrypt,并且让群晖每天自动检查续期。
整个过程踩了三个坑,都不在 acme.sh 的官方文档里,其中一个还差点让主机在重启后彻底连不上 Web 界面。这篇把过程和坑一起记下来。
环境
- ESXi:6.7.0,内网
192.168.1.111 - 群晖:DSM 7.2.1,DS918+,负责跑 acme.sh 和定时任务
- 域名:
esxi.example.com,NS 托管在 Cloudflare
为什么是「群晖跑 acme.sh + SSH 推送」
ESXi 本身跑不了 acme.sh(busybox 环境,且不该往 hypervisor 上塞东西),所以必须有一台机器负责签发再推过去。群晖 7×24 开机、有任务计划、能出网,是现成的选择。
推送方式上,一开始我打算自己写脚本:签发、传文件、重启服务。后来查文档才发现acme.sh 官方就有 ESXi 的部署配方,用通用的sshdeploy hook,文档里明确标注"tested with 6.7u3":
exportDEPLOY_SSH_USER="root"exportDEPLOY_SSH_SERVER="esxi.example.com"exportDEPLOY_SSH_KEYFILE="/etc/vmware/ssl/rui.key"exportDEPLOY_SSH_FULLCHAIN="/etc/vmware/ssl/rui.crt"exportDEPLOY_SSH_REMOTE_CMD="/etc/init.d/hostd restart"exportDEPLOY_SSH_MULTI_CALL="yes"acme.sh--deploy-desxi.example.com --deploy-hookssh用上游钩子的好处很实在:分块传输、旧证书备份轮转、续期后自动重新部署,这些都是上游维护的,我只需要写一层很薄的入口脚本。
遇到的坑
下面是这个配方直接用会遇到的三个问题。
坑一:ESXi 不认 ed25519 密钥
部署钩子要求先交换 SSH 密钥。我图公钥字符串短,生成了 ed25519,装到/etc/ssh/keys-root/authorized_keys之后死活连不上:
root@192.168.1.111: Permission denied (publickey,keyboard-interactive)文件内容对、权限对(-rw------Troot:root)、sshd_config里PermitRootLogin yes、AuthorizedKeysFile路径也对。查了半天配置文件毫无线索。
答案在 ESXi 的/var/log/auth.log里:
userauth_pubkey: key type ssh-ed25519 not in PubkeyAcceptedKeyTypes [preauth]ESXi 的 sshd 是用 FIPS 版 OpenSSL 编译的(OpenSSL 1.0.2s-fips),FIPS 验证的算法集里不含 Ed25519,所以编译期就把它从PubkeyAcceptedKeyTypes里排除了。这条在sshd_config里看不到,只能从日志发现。
结论:ESXi 只能用 RSA 密钥。
ssh-keygen-trsa-b2048-N""-C"dsm-acme-esxi"-f~/.ssh/esxi_deploy顺带一提,证书本身也建议用 RSA 2048(acme.sh --issue加--keylength 2048)。ESXi 6.7 的 hostd 对 ECC 证书支持有问题。
排查提示:遇到公钥登录失败,别在客户端反复试,直接看服务端的auth.log,一行日志顶半小时猜测。
坑二:MULTI_CALL不够,必须开USE_SCP
密钥通了,部署还是失败:
[...] will copy fullchain to remote file /etc/vmware/ssl/rui.crt /bin/sh: File too large [...] Error code 1 returned from ssh [...] Error deploying for domain: esxi.example.com官方配方里的DEPLOY_SSH_MULTI_CALL="yes"就是为 busybox 准备的——文档原话是"command line buffer is not long enough"。但它只是把多个文件拆成多次 SSH 调用,单个文件仍然是一整条echo命令塞进去的。1.7KB 的私钥能过,5.6KB 的 fullchain 就超了。
解法是文档里另一个参数,改用 scp 传文件而不是塞命令行:
exportDEPLOY_SSH_USE_SCP="yes"exportDEPLOY_SSH_SCP_CMD="scp -q -i /path/to/key -o BatchMode=yes -o StrictHostKeyChecking=no"ESXi 自带 scp,直接可用。
这个坑真正危险的地方
失败信息看着只是"部署没成功",但看一眼时间戳会发现问题严重得多:
-rw-r--r-- 1 root root 5649 Jul 19 09:33 /etc/vmware/ssl/rui.crt ← 旧证书 -rw-r--r-- 1 root root 1675 Jul 19 12:01 /etc/vmware/ssl/rui.key ← 新私钥钩子先写成功了私钥,然后卡在证书上——主机被留在了 cert/key 不匹配的状态。
此时 HTTPS 表面完全正常,因为运行中的 hostd 用的是内存里已加载的旧证书对。但这是个定时炸弹:ESXi 的 crontab 每小时会跑一次auto-backup.sh把/etc固化进 bootbank,之后任何一次 hostd 重启或主机重启,HTTPS 就直接起不来了。而这种时候你往往正需要 Web 界面去救。
所以动完 ESXi 证书,一定要验证配对,别只看服务还活着:
c=$(openssl x509-noout-modulus-in/etc/vmware/ssl/rui.crt|md5sum)k=$(openssl rsa-noout-modulus-in/etc/vmware/ssl/rui.key|md5sum)["$c"="$k"]&&echoMATCH||echoMISMATCH好在 acme.sh 覆盖前会自动备份(DEPLOY_SSH_BACKUP默认开),把备份里的rui.key还原回去就恢复配对了。建议把DEPLOY_SSH_BACKUP_PATH指到 datastore 上,默认位置在 ESXi 的内存盘里,一重启备份自己就没了:
exportDEPLOY_SSH_BACKUP_PATH="/vmfs/volumes/datastore1/.acme_bak"坑三:auto-backup.sh会把私钥打进日志
ESXi 的/etc是内存文件系统,改动要靠/sbin/auto-backup.sh固化到 bootbank。很自然地会想把它加进部署后的远程命令:
exportDEPLOY_SSH_REMOTE_CMD="/etc/init.d/hostd restart && /sbin/auto-backup.sh"但auto-backup.sh会把/etc的变更 diff 打到 stdout,其中包含rui.key的完整内容。
+MIIEowIBAAKCAQEA... +... -----END RSA PRIVATE KEY----- Saving current state in /bootbank意味着每次自动续期,主机私钥都会被完整写进任务日志。日志文件权限往往比密钥文件宽松得多,等于白白降低了私钥的保护等级。
修复很简单,但必须记得:
exportDEPLOY_SSH_REMOTE_CMD="/etc/init.d/rhttpproxy restart >/dev/null 2>&1 && /etc/init.d/hostd restart >/dev/null 2>&1 && /sbin/auto-backup.sh >/dev/null 2>&1"补充两点:
- 官方配方只重启
hostd,但监听 443 的实际是rhttpproxy,我这边实测两个都重启才稳定生效,所以加上了。 auto-backup.sh其实不是必须的。ESXi 的 crontab 里本来就有1 * * * * /sbin/auto-backup.sh,每小时自动跑。显式调用只是让持久化立即完成,把窗口从"最多一小时"缩短到"立即"。装 SSH 公钥后同理。
另外几个小提醒
装 acme.sh 别只下单个脚本。只curl那个acme.sh文件的话,dnsapi/和deploy/两个目录不会跟过来,表现为Cannot find DNS API hook for: dns_cf。要下整个仓库:
curl-sL-oacme.tar.gz https://github.com/acmesh-official/acme.sh/archive/refs/heads/master.tar.gzmkdir-pacmesrc&&tarxzf acme.tar.gz-Cacmesrc --strip-components=1cdacmesrc&&./acme.sh--install--home~/.acme.sh--accountemailyou@example.com--nocron群晖没有crontab命令,所以用--nocron,调度交给 DSM 的任务计划。另外 DSM 的/tmp是noexec,脚本别放那儿执行。
DEPLOY_SSH_SERVER填内网 IP,不要填域名。我这个域名对外解析到一台公网反向代理,填域名会把证书推到错误的机器上。用192.168.1.111直连。
CF 凭据用 API Token,不要用 Global API Key。Token 可以限定成只有Zone / DNS / Edit且只对单个域名生效;Global API Key 等同于账号全权限,能改所有 zone、账单甚至删域名。两者在 acme.sh 里都能用,没有任何理由选后者。
acme.sh 现在支持 ARI(ACME Renewal Information),续期时机由 CA 动态给出,不再是固定的"到期前 30 天":
ARI suggestedWindow: 2026-09-16T13:55:25Z to 2026-09-18T09:06:14Z Next renewal time picked from ARI window: 2026-09-17T07:40:54Z最终脚本
放在群晖上,由任务计划每天调用一次。
配置文件acme-esxi.env(权限 600,含凭据):
exportDEPLOY_SSH_USER=rootexportDEPLOY_SSH_SERVER=192.168.1.111exportDEPLOY_SSH_KEYFILE=/etc/vmware/ssl/rui.keyexportDEPLOY_SSH_FULLCHAIN=/etc/vmware/ssl/rui.crtexportDEPLOY_SSH_MULTI_CALL=yesexportDEPLOY_SSH_USE_SCP=yesexportDEPLOY_SSH_BACKUP_PATH=/vmfs/volumes/datastore1/.acme_bakexportDEPLOY_SSH_CMD="ssh -i /var/services/homes/YOURUSER/.ssh/esxi_deploy -o BatchMode=yes -o StrictHostKeyChecking=no"exportDEPLOY_SSH_SCP_CMD="scp -q -i /var/services/homes/YOURUSER/.ssh/esxi_deploy -o BatchMode=yes -o StrictHostKeyChecking=no"exportDEPLOY_SSH_REMOTE_CMD="/etc/init.d/rhttpproxy restart >/dev/null 2>&1 && /etc/init.d/hostd restart >/dev/null 2>&1 && /sbin/auto-backup.sh >/dev/null 2>&1"exportCF_Token="你的_Cloudflare_API_Token"exportCF_Zone_ID="你的_Zone_ID"exportCF_Account_ID="你的_Account_ID"入口脚本acme-esxi-renew.sh(权限 700):
#!/bin/bash# ESXi HTTPS 证书自动续期,由 DSM 任务计划调用# 退出码非 0 时任务计划会发邮件告警set-u# D = 脚本自身所在目录,配置和日志跟着脚本走,整个目录可随意搬动D="$(cd "$(dirname"$0")"&&pwd)" # H = acme.sh 的家目录 H="/var/services/homes/YOURUSER" ENV_FILE="$D/acme-esxi.env" ACME="$H/.acme.sh/acme.sh" LOG="$D/acme-esxi.log" DOMAIN="esxi.example.com" ESXI="192.168.1.111" MIN_DAYS=20 log() { echo "[$(date'+%F %T')]$*" >>"$LOG"; } die() { log "FAIL:$*"; exit 1; } # 日志超过 1MB 时轮转 [ -f "$LOG" ] && [ "$(wc-c<"$LOG")" -gt 1048576 ] && mv "$LOG" "$LOG.old" log "===开始===" [ -f "$ENV_FILE" ] || die "配置文件缺失:$ENV_FILE" . "$ENV_FILE" # --cron 只在 ARI 建议的窗口内才真正续期,平时空跑 out=$("$ACME" --cron --home "$H/.acme.sh"2>&1)rc=$?# 过滤证书/私钥正文,防止敏感内容落进日志 echo "$out" | grep -viE 'BEGIN |END |^\+|^[A-Za-z0-9+/=]{40,}$' >>"$LOG" [$rc-eq 0 ] || die "acme.sh 退出码$rc" # 独立验证:直连 443 抓线上真实证书,而不是相信 acme.sh 的返回值。 # 能抓到「续期成功但没部署上去」和「部署了但服务没重启」两类静默故障。 live=$(echo|openssl s_client-connect"$ESXI:443"-servername"$DOMAIN"2>/dev/null|openssl x509-noout-serial2>/dev/null)mine=$(openssl x509-in"$H/.acme.sh/$DOMAIN/fullchain.cer"-noout-serial2>/dev/null)[ -n "$live" ] || die "无法从$ESXI:443 取得证书,ESXi 可能不可达" [ -n "$mine" ] || die "本地证书文件读取失败" [ "$live" = "$mine" ] || die "线上证书与本地不一致(线上$live/ 本地$mine),部署未生效" # 剩余天数兜底:即使上面都过,天数不足也说明续期逻辑没在按预期工作 end=$(echo|openssl s_client-connect"$ESXI:443"2>/dev/null|openssl x509-noout-enddate|cut-d=-f2)days=$(((($(date-d "$end"+%s)-$(date+%s))/ 86400))) [ "$days" -ge "$MIN_DAYS" ] || die "线上证书仅剩$days天(阈值$MIN_DAYS),续期未按预期工作" log "===正常:$live,剩余$days天==="exit0这个脚本的重点不是调 acme.sh,而是后面那两段独立校验。
acme.sh --cron返回 0 只说明它自己没报错,说明不了证书真的在对外服务。上面坑二里那种"私钥写了、证书没写、服务还活着"的状态,acme.sh 报的是失败,但如果失败发生在重启服务之后呢?所以脚本跑完会直连 443 抓真实握手的证书序列号,和本地文件比对,再查一遍剩余天数。任何一步不过就退出码 1,让任务计划发邮件。
群晖任务计划配置
控制面板 → 任务计划 → 新增 → 计划的任务 → 用户定义的脚本:
- 用户:普通用户即可,不需要 root(脚本只需要出网和一把 SSH 密钥)
- 计划:每天一次
- 命令:脚本的绝对路径
- 勾选「异常时通过电子邮件发送运行详情」——这是唯一的告警出口
验证
别只看"没报错"就认为成了。至少验证这几项:
# 1. 线上实际提供的证书(直连 443 抓真实握手,不是看文件内容)echo|openssl s_client-connect192.168.1.111:443-servernameesxi.example.com2>/dev/null\|openssl x509-noout-subject-issuer-dates-serial# 2. 证书链是否完整可信echo|openssl s_client-connect192.168.1.111:4432>&1|grep"Verify return code"# 期望:Verify return code: 0 (ok)# 3. 序列号与本地签发的是否一致openssl x509-in~/.acme.sh/esxi.example.com/fullchain.cer-noout-serial比对序列号而不是只看有效期——有效期相同的两张证书完全可能是不同的两张。
另外建议测一次失败路径。复制一份脚本、把ESXI改成一个不可达的 IP 再跑,确认它确实退出码 1 并记录了错误。
小结
三个坑按踩到的顺序:
- ESXi 只认 RSA 公钥,ed25519 会被 FIPS 编译的 sshd 拒绝,证据在服务端
auth.log - 必须开
DEPLOY_SSH_USE_SCP,只靠MULTI_CALL会在 fullchain 上失败,并留下 cert/key 不匹配的主机 auto-backup.sh的输出要重定向掉,否则每次续期都把主机私钥写进日志
真正花时间的不是配置本身,而是第一个坑——公钥文件内容、权限、路径全都正确却连不上,光看配置文件永远找不到原因。远程服务的日志比本地的猜测有用得多。
参考
- acme.sh deployhooks wiki
- acme.sh 项目主页
- Configuring CA signed certificates for ESXi hosts(Broadcom KB)