UniFi Cloud Gateway — Let’s Encrypt 证书自动化手动安装教程
适用设备:UniFi Cloud Gateway Fiber / Ultra / UDM 系列
适用场景:国内网络无法访问 GitHub / Gitee,需要手动传输 acme.sh 安装包
验证环境:Debian 11 (bullseye) · aarch64 · UniFi OS 4.x
端口要求:无需开放 80 / 443,全程使用 DNS-01 验证
目录
- 前置准备
- 手动安装 acme.sh
- 切换 CA 并注册账号
- 配置 DNSPod API 凭据
- 签发 RSA-2048 证书
- 创建 UniFi 证书部署脚本
- 绑定 acme.sh 自动部署 Hook
- 配置 Cron 自动续签
- 配置开机自愈(on_boot.d)
- 验证证书是否生效
- 日常维护与故障排查
- 常见问题汇总
1. 前置准备
1.1 确认基础环境
SSH 登录设备后,先确认系统工具是否齐全:
which curl openssl
uname -a
预期输出:
/usr/bin/curl
/usr/bin/openssl
Linux Cloud-Gateway-Ultra 5.4.x ... aarch64 GNU/Linux
注意:UniFi Cloud Gateway 没有
java/keytool,不要尝试 keystore 方式,新版固件全部使用 PEM 格式证书,路径在/data/unifi-core/config/。
1.2 确认 UniFi 证书目录
ls /data/unifi-core/config/*.crt
预期看到以下文件:
unifi-core.crt
unifi-core-direct.crt
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.crt ← UUID 证书(每台设备不同)
UUID 文件名无需手动记录,部署脚本会自动扫描。
1.3 确认持久化存储目录
ls /data/
/data/ 目录在固件升级和重启后保持不变,所有脚本和配置均放在此处。
1.4 准备域名
需要一个由 DNSPod 管理的域名,例如 route.plnl.vip。
域名 A 记录是否指向当前 IP 不影响证书签发(DNS-01 验证只写 TXT 记录)。
2. 手动安装 acme.sh
2.1 在能访问 GitHub 的电脑上下载
在本地电脑操作(能访问 GitHub 的网络环境):
# 方法一:浏览器直接下载 zip 包
# https://github.com/acmesh-official/acme.sh/archive/refs/heads/master.zip
# 方法二:git clone
git clone https://github.com/acmesh-official/acme.sh.git
2.2 传输到 UniFi 设备
在本地电脑执行(替换为设备实际 IP):
# 上传 zip 包
scp acme.sh-master.zip root@192.168.x.x:/root/
# 或上传整个目录
scp -r acme.sh/ root@192.168.x.x:/root/acme-src/
2.3 在设备上解压并安装
回到 UniFi 设备 SSH 终端:
cd /root
# 如果上传的是 zip 包,先解压
unzip acme.sh-master.zip
mv acme.sh-master acme-src
# 进入源码目录执行安装
# 邮箱必须是合法格式(含 @),否则后续注册 Let's Encrypt 账号会报 invalidContact 错误
cd /root/acme-src
sh acme.sh --install -m admin@example.com --home /root/.acme.sh
2.4 验证安装成功
~/.acme.sh/acme.sh --version
看到版本号(如 v3.x.x)即安装成功。
3. 切换 CA 并注册账号
关键步骤,不可跳过。
acme.sh 默认 CA 是 ZeroSSL,在国内网络下无法自动获取 EAB 凭据,会报Cannot resolve _eab_kid错误。必须手动切换到 Let’s Encrypt。
3.1 切换默认 CA
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
成功输出:
[...] Changed default CA to: https://acme-v02.api.letsencrypt.org/directory
3.2 注册 Let’s Encrypt 账号
~/.acme.sh/acme.sh --register-account \
-m admin@example.com \
--server letsencrypt
成功输出:
[...] Registering account: https://acme-v02.api.letsencrypt.org/directory
[...] Registered / Already registered
[...] ACCOUNT_THUMBPRINT='xxxxxx...'
3.3 确认 CA 配置已写入
grep "DEFAULT_ACME_SERVER" ~/.acme.sh/account.conf
预期输出:
DEFAULT_ACME_SERVER='https://acme-v02.api.letsencrypt.org/directory'
如果之前曾安装过 acme.sh 且注册失败,执行以下命令彻底清理后重试:
rm -f ~/.acme.sh/account.conf rm -rf ~/.acme.sh/ca/acme-v02.api.letsencrypt.org # 然后重新执行 3.1 和 3.2
4. 配置 DNSPod API 凭据
4.1 获取 API 凭据
登录 DNSPod 控制台 → 用户中心 → API 密钥,获取 ID(纯数字)和 Token(字符串)。
安全建议:优先使用 DNSPod 域名级 API Token(仅授权该域名的 DNS 写入权限),而非账号级全局密钥。一旦路由器上的配置文件泄露,仅影响该域名,不会导致整个账号下所有域名失控。申请路径:DNSPod 控制台 → API 密钥 → 创建域名密钥。
4.2 写入配置文件
mkdir -p /data/unifi-cert-manager
cat > /data/unifi-cert-manager/config.conf << 'EOF'
DP_Id="你的API_ID"
DP_Key="你的API_Token"
DOMAIN="route.plnl.vip"
DNS_PLATFORM="dnspod"
BIND_INTERFACE="ppp0"
EOF
chmod 600 /data/unifi-cert-manager/config.conf
4.3 加载凭据到当前 Shell
. /data/unifi-cert-manager/config.conf
export DP_Id DP_Key
5. 签发 RSA-2048 证书
必须使用 RSA-2048,严禁使用 ECC(
--keylength ec-256)。
UniFi Core 启动时会校验证书类型,遇到 ECC 证书会误判为损坏,在服务启动后约 2 秒内用自签名 RSA 证书将其静默覆盖,且不报任何错误。
. /data/unifi-cert-manager/config.conf
export DP_Id DP_Key
~/.acme.sh/acme.sh --issue \
--dns dns_dp \
-d route.plnl.vip \
--keylength 2048 \
--server letsencrypt \
--force
--server letsencrypt 显式指定 CA,防止 acme.sh 内部配置漂移回 ZeroSSL。
5.1 正常签发过程输出说明
[...] Using CA: https://acme-v02.api.letsencrypt.org/directory ← CA 正确
[...] Creating domain key
[...] Adding TXT value: xxxx for domain: _acme-challenge.route.plnl.vip
[...] The TXT record has been successfully added. ← DNS API 调用成功
[...] Success for domain route.plnl.vip ← DNS 验证通过
[...] Cert success. ← 签发完成
[...] Your cert is in: /root/.acme.sh/route.plnl.vip/route.plnl.vip.cer
[...] The full-chain cert is in: /root/.acme.sh/route.plnl.vip/fullchain.cer
5.2 签发失败常见原因
| 错误信息 | 原因 | 解决 |
|---|---|---|
Using CA: https://acme.zerossl.com |
CA 未切换 | 重新执行第 3 节 |
Cannot resolve _eab_kid |
CA 是 ZeroSSL | 重新执行第 3 节 |
invalidContact |
邮箱格式无效 | 清理 account.conf 重新注册(见 3.3) |
DNS record not found |
API 凭据错误或域名不在该账号 | 检查 DP_Id / DP_Key |
error code: 35 / error code: 28 |
DNS 查询出口不通,acme.sh 自动重试 | 通常等待即可自动通过 |
Rate limit exceeded |
同域名短期签发超过限制(每周 5 次) | 等待一周,或用 --staging 测试 |
6. 创建 UniFi 证书部署脚本
6.1 创建 SSL 目录
mkdir -p /data/ssl
6.2 写入部署脚本
cat > /data/deploy-unifi-cert.sh << 'EOF'
#!/bin/sh
# 在 cron / acme.sh hook 等无完整环境的场景下,显式声明 PATH
# 确保 openssl、systemctl 等命令可以正常找到
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOMAIN="route.plnl.vip" # ← 修改为你的域名
SRC_CRT="/data/ssl/fullchain.pem"
SRC_KEY="/data/ssl/key.pem"
CONFIG_DIR="/data/unifi-core/config"
LOG="/data/cert_deploy.log"
echo "$(date) ===== 开始更新证书 =====" >> $LOG
# 1. 检查源文件存在
if [ ! -f "$SRC_CRT" ] || [ ! -f "$SRC_KEY" ]; then
echo "$(date) 错误:/data/ssl/ 中未找到证书文件" >> $LOG
exit 1
fi
# 2. 验证证书文件完整性(防止损坏的证书导致 unifi-core 无法启动)
if ! openssl x509 -in "$SRC_CRT" -noout 2>/dev/null; then
echo "$(date) 错误:fullchain.pem 证书文件损坏,已中止" >> $LOG
exit 1
fi
# 3. 验证是 RSA 证书,防止 acme.sh 意外使用 ECC 证书
KEY_TYPE=$(openssl pkey -in "$SRC_KEY" -text -noout 2>/dev/null | head -3)
if echo "$KEY_TYPE" | grep -qi "EC\|ecdsa"; then
echo "$(date) 错误:检测到 ECC 证书,UniFi 不支持,已中止" >> $LOG
exit 1
fi
echo "$(date) 证书验证通过(RSA 格式,文件完整)" >> $LOG
# 4. 动态扫描 UUID 证书(固件升级后 UUID 可能变化,避免硬编码失效)
UUID_CERTS=$(find "$CONFIG_DIR" -maxdepth 1 -name "*.crt" \
| grep -E "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.crt" \
| sed 's/\.crt$//' | xargs -I{} basename {} 2>/dev/null)
# 需要替换的证书组:固定名称 + 动态 UUID
CERT_NAMES="unifi-core unifi-core-direct $UUID_CERTS"
# 5. 备份并替换所有证书
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
for name in $CERT_NAMES; do
CRT="${CONFIG_DIR}/${name}.crt"
KEY="${CONFIG_DIR}/${name}.key"
[ -f "$CRT" ] && cp "$CRT" "${CRT}.bak.${BACKUP_DATE}"
[ -f "$KEY" ] && cp "$KEY" "${KEY}.bak.${BACKUP_DATE}"
cp "$SRC_CRT" "$CRT" && chmod 644 "$CRT"
cp "$SRC_KEY" "$KEY" && chmod 600 "$KEY"
echo "$(date) 已替换:$name" >> $LOG
done
# 6. 清理 7 天前的旧备份,防止长期积累占满 /data 空间
find "$CONFIG_DIR" -name "*.bak.*" -mtime +7 -delete 2>/dev/null
echo "$(date) 已清理 7 天前旧备份" >> $LOG
# 7. 只重启 unifi-core,严禁同时重启 nginx
# 原因:443 端口由 unifi-core 独占,两者同时重启会产生端口抢占冲突,
# 导致网页完全无法访问,且无法自动恢复
systemctl restart unifi-core.service
if [ $? -eq 0 ]; then
echo "$(date) ✓ 证书更新成功,unifi-core 已重启" >> $LOG
else
echo "$(date) ✗ unifi-core 重启失败,请手动检查" >> $LOG
exit 1
fi
# 8. 日志截断,只保留最近 1000 行,防止长期运行后日志文件过大
tail -n 1000 "$LOG" > "${LOG}.tmp" && mv "${LOG}.tmp" "$LOG"
echo "$(date) ===== 完成 =====" >> $LOG
EOF
chmod +x /data/deploy-unifi-cert.sh
6.3 脚本关键设计说明
| 设计点 | 原因 |
|---|---|
| 顶部声明 PATH | cron/hook 环境 PATH 不完整,openssl、systemctl 可能找不到 |
| 证书完整性校验 | 损坏的 fullchain.pem 会导致 unifi-core 启动失败,黑屏 |
| RSA 格式校验 | ECC 证书被 UniFi 静默覆盖,且不报错,极难排查 |
| 动态扫描 UUID | 固件升级后 Console UUID 可能变化,硬编码会失效 |
| 只重启 unifi-core | 同时重启 nginx 抢占 443 端口,网页彻底崩溃 |
| 自动清理备份 | 每次部署都备份,不清理会长期积累占满 /data |
| 日志截断 | 长期追加日志,文件无限增长 |
| 不操作 keystore | 新版固件无 Java,keystore 由 Network Application 独立管理 |
7. 绑定 acme.sh 自动部署 Hook
~/.acme.sh/acme.sh --install-cert \
-d route.plnl.vip \
--fullchain-file /data/ssl/fullchain.pem \
--key-file /data/ssl/key.pem \
--reloadcmd "/data/deploy-unifi-cert.sh"
绑定后立即手动触发一次完整部署验证:
/data/deploy-unifi-cert.sh
查看部署日志:
tail -20 /data/cert_deploy.log
预期输出:
2026-08-06 18:06:26 ===== 开始更新证书 =====
2026-08-06 18:06:26 证书验证通过(RSA 格式,文件完整)
2026-08-06 18:06:26 已替换:unifi-core
2026-08-06 18:06:26 已替换:unifi-core-direct
2026-08-06 18:06:26 已替换:f31f11c9-e8d9-4866-97ce-f0bedad947e8
2026-08-06 18:06:45 ✓ 证书更新成功,unifi-core 已重启
2026-08-06 18:06:45 ===== 完成 =====
8. 配置 Cron 自动续签
UniFi OS 重启后 crontab 可能被重置,需要手动确认并写入。
8.1 检查当前 cron 任务
crontab -l
8.2 写入 cron 任务
crontab -e
加入以下内容(保留已有的 DDNS 等任务):
# acme.sh 自动续签
# 说明:acme.sh 在第一次签发时已将 DP_Id/DP_Key 缓存到域名配置文件中,
# 续签时无需每次重新 source。保留 source 是为了防止缓存被意外清除时的兜底。
0 2 * * * . /data/unifi-cert-manager/config.conf && export DP_Id DP_Key && /root/.acme.sh/acme.sh --cron --home /root/.acme.sh >> /data/acme_cron.log 2>&1
# 每小时健康检查:检测证书是否被 UniFi 固件覆盖,若是则自动修复
0 * * * * /data/check-cert.sh
8.3 创建每小时健康检查脚本
cat > /data/check-cert.sh << 'EOF'
#!/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LOG="/data/cert_deploy.log"
ISSUER=$(openssl x509 -in /data/unifi-core/config/unifi-core.crt \
-noout -issuer 2>/dev/null)
# Let's Encrypt 签发,证书正常
if echo "$ISSUER" | grep -qi "Let's Encrypt"; then
exit 0
fi
# 被 UniFi 覆盖,触发修复
echo "$(date) ⚠ 证书被覆盖(${ISSUER}),自动修复中..." >> $LOG
/data/deploy-unifi-cert.sh
EOF
chmod +x /data/check-cert.sh
9. 配置开机自愈(on_boot.d)
UniFi OS 重启后 unifi-core 先于 cron 启动,会写入自签名证书,需要等 cron 下次触发才能修复(最长 1 小时)。通过 on_boot.d 可在开机后立即检查并修复(30 秒内完成)。
cat > /data/on_boot.d/10-restore-cert.sh << 'EOF'
#!/bin/sh
# 开机自愈:后台异步执行,不阻塞系统启动序列
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LOG="/data/cert_deploy.log"
(
# 等待 unifi-core 完全就绪
sleep 30
echo "$(date) ===== 开机证书检查 =====" >> $LOG
ISSUER=$(openssl x509 -in /data/unifi-core/config/unifi-core.crt \
-noout -issuer 2>/dev/null)
if echo "$ISSUER" | grep -qi "Let's Encrypt"; then
echo "$(date) 证书正常,无需修复" >> $LOG
exit 0
fi
echo "$(date) 检测到非 LE 证书(${ISSUER}),开始修复..." >> $LOG
/data/deploy-unifi-cert.sh
# 同时检查并补回 cron 续签任务
if ! crontab -l 2>/dev/null | grep -q "acme.sh"; then
/root/.acme.sh/acme.sh --install-cronjob >> $LOG 2>&1
echo "$(date) cron 续签任务已重新安装" >> $LOG
fi
) >> $LOG 2>&1 &
exit 0
EOF
chmod +x /data/on_boot.d/10-restore-cert.sh
10. 验证证书是否生效
10.1 检查本地证书文件
openssl x509 -in /data/unifi-core/config/unifi-core.crt \
-noout -issuer -dates
预期输出:
issuer=C = US, O = Let's Encrypt, CN = YR2
notBefore=Aug 6 09:06:30 2026 GMT
notAfter=Nov 4 09:06:29 2026 GMT
10.2 检查 HTTPS 实际返回的证书
echo | openssl s_client \
-connect route.plnl.vip:443 \
-servername route.plnl.vip 2>/dev/null \
| openssl x509 -noout -issuer -dates
10.3 查看 acme.sh 管理的证书列表及到期时间
~/.acme.sh/acme.sh --list
10.4 浏览器验证
用无痕模式访问 https://route.plnl.vip,地址栏显示绿色锁图标,点击后证书信息中可看到 Let's Encrypt 签发机构。
11. 日常维护与故障排查
手动续签(测试用)
. /data/unifi-cert-manager/config.conf
export DP_Id DP_Key
~/.acme.sh/acme.sh --renew -d route.plnl.vip --force
固件大版本升级后的恢复步骤
固件大版本升级可能同时重置证书和 cron,升级完成后执行以下两步即可完全恢复:
# 步骤 1:重新安装 cron 续签任务
~/.acme.sh/acme.sh --install-cronjob
# 步骤 2:手动触发一次证书检查和修复
/data/check-cert.sh
# 确认结果
openssl x509 -in /data/unifi-core/config/unifi-core.crt -noout -issuer
查看所有日志
# 证书部署日志(含健康检查和开机自愈记录)
tail -50 /data/cert_deploy.log
# acme.sh 自动续签 cron 日志
tail -50 /data/acme_cron.log
# acme.sh 详细调试(签发失败时使用)
~/.acme.sh/acme.sh --issue --dns dns_dp -d route.plnl.vip \
--keylength 2048 --server letsencrypt --debug 2
更新 acme.sh(推荐每月执行)
DNSPod / 阿里云等 DNS API 接口会随厂商变动而调整,建议定期更新:
# 有网络时直接升级
~/.acme.sh/acme.sh --upgrade
# 国内无法访问 GitHub:在本地电脑下载新版后 scp 传入,重复第 2 节安装步骤
# 安装程序会自动保留 account.conf 和已签发的证书数据
12. 常见问题汇总
Q1:签发成功但证书约 2 秒后又变回 unifi.local
原因:使用了 ECC 证书(--keylength ec-256)。UniFi Core 启动时误判 ECC 证书为损坏,静默覆盖为自签名 RSA 证书,全程不报错。
解决:
# 删除 ECC 证书目录
rm -rf ~/.acme.sh/route.plnl.vip_ecc/
# 重新用 RSA-2048 签发
~/.acme.sh/acme.sh --issue --dns dns_dp -d route.plnl.vip \
--keylength 2048 --server letsencrypt --force
Q2:重启 nginx 后网页完全无法访问
原因:443 端口由 unifi-core 独占,nginx 与 unifi-core 同时重启会产生端口抢占冲突,两个服务均无法正常绑定端口。
恢复:
# 删除自定义证书,让 UniFi 用自签名证书自我修复
rm -f /data/unifi-core/config/unifi-core*.crt
rm -f /data/unifi-core/config/unifi-core*.key
systemctl restart unifi-core
# 等待约 15 秒后刷新浏览器
预防:部署脚本中永远不要加 systemctl restart nginx。
Q3:固件升级后证书又失效
原因:大版本固件升级可能重置 /data/unifi-core/config/ 下的证书文件。
解决:执行第 11 节「固件大版本升级后的恢复步骤」,两条命令搞定。
Q4:UUID 证书文件名变了
原因:固件重置或大版本升级后 Console UUID 可能改变,旧 UUID 文件被新 UUID 文件替换。
解决:部署脚本使用 find 动态扫描,直接重新执行即可:
/data/deploy-unifi-cert.sh
Q5:cron 续签任务重启后丢失
原因:UniFi OS 某些固件版本重启会重置 crontab。
解决:开机自愈脚本(第 9 节)会在开机后自动检查并重装;也可手动恢复:
~/.acme.sh/acme.sh --install-cronjob
Q6:Cannot resolve _eab_kid
原因:acme.sh 默认 CA 是 ZeroSSL,国内网络无法获取其 EAB 凭据。
解决:重新执行第 3 节,切换到 Let’s Encrypt。
Q7:Account registration error: invalidContact
原因:安装 acme.sh 时邮箱为空或格式无效,account.conf 中存储了损坏的联系人信息。
解决:
rm -f ~/.acme.sh/account.conf
rm -rf ~/.acme.sh/ca/acme-v02.api.letsencrypt.org
~/.acme.sh/acme.sh --register-account \
-m admin@yourdomain.com \
--server letsencrypt
Q8:续签成功但 deploy 脚本没有被触发
原因:acme.sh hook 执行时 PATH 不完整,openssl 或 systemctl 找不到,脚本静默失败。
验证:
# 手动模拟 hook 环境测试
env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
/data/deploy-unifi-cert.sh
tail -10 /data/cert_deploy.log
预防:部署脚本顶部已声明 PATH(见第 6.2 节),此问题已规避。
附录:文件清单
/data/acme_cron.log
| 文件路径 | 用途 |
|---|---|
~/.acme.sh/ |
acme.sh 程序本体及证书数据 |
~/.acme.sh/account.conf |
CA 账号配置(含默认 CA、邮箱、密钥缓存) |
/data/unifi-cert-manager/config.conf |
DNS API 凭据及配置(权限 600,仅 root 可读) |
/data/ssl/fullchain.pem |
acme.sh 导出的完整证书链(部署来源) |
/data/ssl/key.pem |
acme.sh 导出的私钥(部署来源) |
/data/deploy-unifi-cert.sh |
证书部署脚本(替换证书、重启 unifi-core) |
/data/check-cert.sh |
每小时健康检查(被覆盖则自动修复) |
/data/on_boot.d/10-restore-cert.sh |
开机自愈脚本(30 秒后检查并修复) |
/data/cert_deploy.log |
证书部署、健康检查、开机自愈统一日志 |
| acme.sh cron 续签日志 | |
/data/unifi-core/config/unifi-core.crt |
UniFi Core 主证书(替换目标) |
/data/unifi-core/config/unifi-core-direct.crt |
UniFi 直连证书(替换目标) |
/data/unifi-core/config/<UUID>.crt |
Console UUID 证书(动态扫描替换) |









