UniFi Cloud Gateway — Let’s Encrypt 证书自动化手动安装教程

UniFi Cloud Gateway — Let’s Encrypt 证书自动化手动安装教程

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 验证


目录

  1. 前置准备
  2. 手动安装 acme.sh
  3. 切换 CA 并注册账号
  4. 配置 DNSPod API 凭据
  5. 签发 RSA-2048 证书
  6. 创建 UniFi 证书部署脚本
  7. 绑定 acme.sh 自动部署 Hook
  8. 配置 Cron 自动续签
  9. 配置开机自愈(on_boot.d)
  10. 验证证书是否生效
  11. 日常维护与故障排查
  12. 常见问题汇总

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 不完整,opensslsystemctl 找不到,脚本静默失败。

验证

# 手动模拟 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 证书(动态扫描替换)
© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享