用群晖给 ESXi 自动续期 Let‘s Encrypt 证书:三个官方文档没写的坑

用群晖给 ESXi 自动续期 Let‘s Encrypt 证书:三个官方文档没写的坑
目录环境为什么是「群晖跑 acme.sh SSH 推送」遇到的坑坑一ESXi 不认 ed25519 密钥坑二MULTI_CALL 不够必须开 USE_SCP这个坑真正危险的地方坑三auto-backup.sh 会把私钥打进日志另外几个小提醒最终脚本群晖任务计划配置验证小结参考家里那台 ESXi 6.7 的 HTTPS 证书过期一年多了每次打开管理页面都得点继续访问不安全的网站。这次把它换成 Let’s Encrypt并且让群晖每天自动检查续期。整个过程踩了三个坑都不在 acme.sh 的官方文档里其中一个还差点让主机在重启后彻底连不上 Web 界面。这篇把过程和坑一起记下来。环境ESXi6.7.0内网192.168.1.111群晖DSM 7.2.1DS918负责跑 acme.sh 和定时任务域名esxi.example.comNS 托管在 Cloudflare为什么是「群晖跑 acme.sh SSH 推送」ESXi 本身跑不了 acme.shbusybox 环境且不该往 hypervisor 上塞东西所以必须有一台机器负责签发再推过去。群晖 7×24 开机、有任务计划、能出网是现成的选择。推送方式上一开始我打算自己写脚本签发、传文件、重启服务。后来查文档才发现acme.sh 官方就有 ESXi 的部署配方用通用的sshdeploy hook文档里明确标注tested with 6.7u3exportDEPLOY_SSH_USERrootexportDEPLOY_SSH_SERVEResxi.example.comexportDEPLOY_SSH_KEYFILE/etc/vmware/ssl/rui.keyexportDEPLOY_SSH_FULLCHAIN/etc/vmware/ssl/rui.crtexportDEPLOY_SSH_REMOTE_CMD/etc/init.d/hostd restartexportDEPLOY_SSH_MULTI_CALLyesacme.sh--deploy-desxi.example.com --deploy-hookssh用上游钩子的好处很实在分块传输、旧证书备份轮转、续期后自动重新部署这些都是上游维护的我只需要写一层很薄的入口脚本。遇到的坑下面是这个配方直接用会遇到的三个问题。坑一ESXi 不认 ed25519 密钥部署钩子要求先交换 SSH 密钥。我图公钥字符串短生成了 ed25519装到/etc/ssh/keys-root/authorized_keys之后死活连不上root192.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-fipsFIPS 验证的算法集里不含 Ed25519所以编译期就把它从PubkeyAcceptedKeyTypes里排除了。这条在sshd_config里看不到只能从日志发现。结论ESXi 只能用 RSA 密钥。ssh-keygen-trsa-b2048-N-Cdsm-acme-esxi-f~/.ssh/esxi_deploy顺带一提证书本身也建议用 RSA 2048acme.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_CALLyes就是为 busybox 准备的——文档原话是command line buffer is not long enough。但它只是把多个文件拆成多次 SSH 调用单个文件仍然是一整条echo命令塞进去的。1.7KB 的私钥能过5.6KB 的 fullchain 就超了。解法是文档里另一个参数改用 scp 传文件而不是塞命令行exportDEPLOY_SSH_USE_SCPyesexportDEPLOY_SSH_SCP_CMDscp -q -i /path/to/key -o BatchModeyes -o StrictHostKeyCheckingnoESXi 自带 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 21 /etc/init.d/hostd restart /dev/null 21 /sbin/auto-backup.sh /dev/null 21补充两点官方配方只重启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-pacmesrctarxzf acme.tar.gz-Cacmesrc --strip-components1cdacmesrc./acme.sh--install--home~/.acme.sh--accountemailyouexample.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 现在支持 ARIACME 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_USERrootexportDEPLOY_SSH_SERVER192.168.1.111exportDEPLOY_SSH_KEYFILE/etc/vmware/ssl/rui.keyexportDEPLOY_SSH_FULLCHAIN/etc/vmware/ssl/rui.crtexportDEPLOY_SSH_MULTI_CALLyesexportDEPLOY_SSH_USE_SCPyesexportDEPLOY_SSH_BACKUP_PATH/vmfs/volumes/datastore1/.acme_bakexportDEPLOY_SSH_CMDssh -i /var/services/homes/YOURUSER/.ssh/esxi_deploy -o BatchModeyes -o StrictHostKeyCheckingnoexportDEPLOY_SSH_SCP_CMDscp -q -i /var/services/homes/YOURUSER/.ssh/esxi_deploy -o BatchModeyes -o StrictHostKeyCheckingnoexportDEPLOY_SSH_REMOTE_CMD/etc/init.d/rhttpproxy restart /dev/null 21 /etc/init.d/hostd restart /dev/null 21 /sbin/auto-backup.sh /dev/null 21exportCF_Token你的_Cloudflare_API_TokenexportCF_Zone_ID你的_Zone_IDexportCF_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 DOMAINesxi.example.com ESXI192.168.1.111 MIN_DAYS20 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.sh21)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$DOMAIN2/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:4432/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:44321|grepVerify 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 wikiacme.sh 项目主页Configuring CA signed certificates for ESXi hostsBroadcom KB