群晖自动签发并替换SSL证书
群晖自动签发并替换SSL证书
这套方案不走阿里云 SSL 证书管理里的“购买免费 90 天证书 -> 申请签发 -> 下载 Apache 证书”流程。
实际流程是:
Let's Encrypt 免费 90 天证书
-> acme.sh 调用阿里云 DNS API 自动添加 TXT 验证记录
-> 验证通过后自动签发证书
-> acme.sh 的 Synology DSM deploy hook 自动导入/替换群晖证书
-> DSM 任务计划每周检查,到期前自动续签
目标配置:
- 域名:
nas.example.com - NAS:
192.168.1.100 - DSM:
7.2-64570 - 群晖管理员用户:
nasadmin - DSM 证书描述:
nas.example.com - 证书类型:ECC
ec-384 - 自动检查时间:每周一凌晨
03:30
这里的“DSM 证书描述”非常关键。acme.sh 的 Synology 部署插件是通过证书描述匹配要替换的证书,不是通过“域名”“颁发者”或“受让人”字段匹配。需要自动更新哪个证书,就一定要先把那个证书的描述设置成和脚本里的 CERT_DESC 完全一致。
0. 重要安全提醒
不要把阿里云 AccessKeySecret 发到聊天里,也不要长期保存在 DSM 任务计划脚本文本里。
如果你曾经把 AccessKeyId / AccessKeySecret 明文写进脚本文件、截图、聊天窗口或 DSM 临时任务里,建议在阿里云 RAM 里删除这组 AccessKey,然后重新创建一组新的专用 AccessKey。旧 Key 删除后就算泄露也不能再用。
本次更新后的脚本会在 SSH 终端里交互输入 Key,不再把 Key 写死在脚本文件里。
1. 准备阿里云 RAM AccessKey
这一节只做一件事:创建一个专门给 acme.sh 使用的 RAM 用户,让它只能操作阿里云 DNS 的 TXT 记录。不要使用主账号 AccessKey,也不要给这个 RAM 用户管理员权限。
1.1 创建自定义权限策略
先创建权限策略,再创建用户并授权。这样可以保证这个 AccessKey 从一开始就是最小权限。
- 登录 RAM 控制台。
- 进入
权限管理 -> 权限策略。 - 点击
创建权限策略。 - 创建方式选择
脚本编辑或JSON。 - 策略名称建议填写:
AcmeNasDnsChallengePolicy
- 策略内容填写:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"alidns:DescribeDomainRecords",
"alidns:AddDomainRecord",
"alidns:DeleteDomainRecord"
],
"Resource": "*"
}
]
}
这三个权限分别用于:
DescribeDomainRecords:查询_acme-challengeTXT 记录是否存在。AddDomainRecord:签发证书时临时新增 TXT 记录。DeleteDomainRecord:验证结束后删除临时 TXT 记录。
不要直接授予 AdministratorAccess,也不建议为了省事授予过大的 DNS 管理权限。
1.2 创建专用 RAM 用户
- 进入
身份管理 -> 用户。 - 点击
创建用户。 - 登录名称建议填写:
acme-nas-dns
- 显示名称可以填写:
acme-nas-dns
- 访问方式只选择:
使用永久 AccessKey 访问
不要勾选 控制台访问,这个用户只给脚本调用 API,不需要登录阿里云控制台。
- 创建用户后,页面会显示:
AccessKey ID
AccessKey Secret
其中:
AccessKey ID对应脚本运行时输入的Aliyun AccessKeyId。AccessKey Secret对应脚本运行时输入的Aliyun AccessKeySecret。
AccessKey Secret 只在创建时显示一次,后续无法再次查看。请立刻复制保存到自己的密码管理器里,不要发到聊天、博客、截图或普通文本文件中。
如果创建用户时没有自动生成 AccessKey,可以进入该 RAM 用户详情页,在 凭证管理 -> AccessKey 里点击 创建 AccessKey。
1.3 给 RAM 用户授权
新建的 RAM 用户默认没有任何权限,必须把刚才的自定义策略授权给它。
- 进入
身份管理 -> 用户。 - 找到
acme-nas-dns。 - 点击用户名称进入详情,或在列表操作里点击
添加权限。 - 授权范围选择当前阿里云账号即可。
- 权限策略类型选择
自定义策略。 - 勾选:
AcmeNasDnsChallengePolicy
- 确认授权。
授权完成后,这个 RAM 用户就只具备自动 DNS 验证所需的最小能力。
1.4 安全检查
开始运行群晖脚本前,建议确认:
- 没有使用主账号 AccessKey。
- RAM 用户没有
AdministratorAccess。 - RAM 用户没有控制台登录密码。
AccessKey Secret没有写进博客、截图、聊天记录或 DSM 任务计划正文。- 如果之前泄露过 AccessKey,先在 RAM 用户的
凭证管理 -> AccessKey里禁用或删除旧 Key,再创建新 Key。
阿里云官方也提醒:AccessKey 是长期有效的程序访问凭证,泄露会威胁账号资源安全。这个方案通过“专用 RAM 用户 + 最小 DNS 权限”来降低风险。如果你有更高安全要求,可以进一步配置 AccessKey 网络访问限制策略或改造成 STS 临时凭证方案,但复杂度会高很多。
官方参考:
2. 首次部署:只运行一次
推荐通过 SSH 运行,不推荐把带密钥的首次部署脚本长期放在 DSM 任务计划里。
运行脚本前,先到 DSM 设置证书描述:
- 打开
控制面板 -> 安全性 -> 证书 - 选中需要自动更新的证书
- 点击
操作 -> 编辑 - 把
描述设置为:
nas.example.com
这个值必须和脚本里的 CERT_DESC="nas.example.com" 完全一致。如果描述没有设置或写错,脚本可能找不到要替换的证书;在 SYNO_CREATE=1 存在时,还可能新建一张证书,导致原来绑定到服务的证书没有被替换。
在你的电脑终端连接群晖:
ssh nasadmin@192.168.1.100
进入 root:
sudo -i
创建脚本文件:
vi /tmp/synology-acme-nas-setup.sh
2.1 首次部署脚本内容
把下面脚本的全部内容粘贴进刚创建的文件:
#!/bin/sh
set -eu
umask 077
# One-time setup for Synology DSM 7.2:
# - install acme.sh under /usr/local/share/acme.sh
# - issue a Let's Encrypt certificate for nas.example.com via Aliyun DNS
# - deploy it to the DSM certificate named nas.example.com
# - create /usr/local/share/acme.sh/nas-cert-renew.sh for the weekly DSM task
#
# Run this script as root from an interactive SSH session.
# It asks for Aliyun AccessKey locally and does not keep those secrets in this file.
DOMAIN="${DOMAIN:-nas.example.com}"
CERT_DESC="${CERT_DESC:-nas.example.com}"
ACME_HOME="${ACME_HOME:-/usr/local/share/acme.sh}"
RENEW_SCRIPT="${RENEW_SCRIPT:-/usr/local/share/acme.sh/nas-cert-renew.sh}"
LOG_FILE="${LOG_FILE:-/var/log/acme-nas-cert-renew.log}"
ACCOUNT_EMAIL="${ACCOUNT_EMAIL:-}"
Ali_Key="${Ali_Key:-}"
Ali_Secret="${Ali_Secret:-}"
KEY_LENGTH="${KEY_LENGTH:-ec-384}"
DNS_SLEEP="${DNS_SLEEP:-120}"
case "$KEY_LENGTH" in
ec-*|ecc|ECC) ACME_CERT_EXTRA_ARG="--ecc" ;;
*) ACME_CERT_EXTRA_ARG="" ;;
esac
fail() {
echo "ERROR: $*" >&2
exit 1
}
warn() {
echo "WARN: $*" >&2
}
need_root() {
if [ "$(id -u)" != "0" ]; then
fail "Please run this script as root. Use 'sudo -i' in SSH, or choose user root in DSM Task Scheduler."
fi
}
prompt_value() {
_var_name="$1"
_prompt="$2"
_current_value="$(eval "printf '%s' \"\${$_var_name:-}\"")"
if [ -n "$_current_value" ]; then
return
fi
if [ ! -t 0 ]; then
fail "Missing $_var_name. Run this script in an interactive SSH session, or export $_var_name before running."
fi
printf "%s" "$_prompt"
IFS= read -r _input_value
[ -n "$_input_value" ] || fail "$_var_name cannot be empty."
eval "$_var_name=\$_input_value"
}
prompt_secret() {
_var_name="$1"
_prompt="$2"
_current_value="$(eval "printf '%s' \"\${$_var_name:-}\"")"
if [ -n "$_current_value" ]; then
return
fi
if [ ! -t 0 ]; then
fail "Missing $_var_name. Run this script in an interactive SSH session, or export $_var_name before running."
fi
printf "%s" "$_prompt"
_old_stty="$(stty -g 2>/dev/null || true)"
if stty -echo 2>/dev/null; then
IFS= read -r _input_value
stty "$_old_stty" 2>/dev/null || true
printf "\n"
else
IFS= read -r _input_value
fi
[ -n "$_input_value" ] || fail "$_var_name cannot be empty."
eval "$_var_name=\$_input_value"
}
collect_inputs() {
prompt_value ACCOUNT_EMAIL "ACME account email: "
prompt_value Ali_Key "Aliyun AccessKeyId: "
prompt_secret Ali_Secret "Aliyun AccessKeySecret: "
}
cleanup_tmp() {
if [ -n "${tmp_dir:-}" ] && [ -d "$tmp_dir" ]; then
rm -rf "$tmp_dir"
fi
}
download_acmesh() {
tmp_dir="$(mktemp -d /tmp/acme-nas-cert-setup.XXXXXX 2>/dev/null || true)"
if [ -z "$tmp_dir" ]; then
tmp_dir="/tmp/acme-nas-cert-setup.$$"
rm -rf "$tmp_dir"
mkdir -p "$tmp_dir"
fi
trap cleanup_tmp EXIT HUP INT TERM
if command -v curl >/dev/null 2>&1; then
curl -L --fail -o "$tmp_dir/acme.sh.tar.gz" \
"https://github.com/acmesh-official/acme.sh/archive/master.tar.gz"
elif command -v wget >/dev/null 2>&1; then
wget -O "$tmp_dir/acme.sh.tar.gz" \
"https://github.com/acmesh-official/acme.sh/archive/master.tar.gz"
else
fail "Neither curl nor wget is available on this NAS."
fi
tar -xzf "$tmp_dir/acme.sh.tar.gz" -C "$tmp_dir"
(cd "$tmp_dir/acme.sh-master" && ./acme.sh --install --nocron --home "$ACME_HOME" --accountemail "$ACCOUNT_EMAIL")
}
install_acmesh_if_needed() {
if [ -x "$ACME_HOME/acme.sh" ]; then
echo "acme.sh already installed at $ACME_HOME"
"$ACME_HOME/acme.sh" --upgrade --auto-upgrade 1 || warn "acme.sh upgrade failed; continuing with installed version."
else
echo "Installing acme.sh to $ACME_HOME"
mkdir -p "$ACME_HOME"
download_acmesh
fi
chmod 700 "$ACME_HOME" 2>/dev/null || true
}
issue_certificate() {
export Ali_Key Ali_Secret
"$ACME_HOME/acme.sh" --set-default-ca --server letsencrypt
set +e
"$ACME_HOME/acme.sh" --issue --server letsencrypt --dns dns_ali -d "$DOMAIN" --keylength "$KEY_LENGTH" --dnssleep "$DNS_SLEEP"
issue_rc="$?"
set -e
if [ "$issue_rc" = "0" ]; then
echo "Certificate issued successfully."
elif [ "$issue_rc" = "2" ]; then
echo "Certificate already exists and is not due for renewal; continuing to deploy the existing certificate."
else
fail "Certificate issue failed with exit code $issue_rc."
fi
}
deploy_to_synology() {
# Temp-admin deployment avoids saving the DSM administrator password.
# It must run locally on DSM as root.
export SYNO_USE_TEMP_ADMIN=1
export SYNO_CERTIFICATE="$CERT_DESC"
export SYNO_CREATE=1
export SYNO_SCHEME="http"
export SYNO_HOSTNAME="localhost"
export SYNO_PORT="5000"
if [ -n "$ACME_CERT_EXTRA_ARG" ]; then
"$ACME_HOME/acme.sh" --deploy -d "$DOMAIN" "$ACME_CERT_EXTRA_ARG" --deploy-hook synology_dsm
else
"$ACME_HOME/acme.sh" --deploy -d "$DOMAIN" --deploy-hook synology_dsm
fi
}
create_renew_script() {
cat > "$RENEW_SCRIPT" <<EOF
#!/bin/sh
set -eu
ACME_HOME="$ACME_HOME"
DOMAIN="$DOMAIN"
CERT_DESC="$CERT_DESC"
LOG_FILE="$LOG_FILE"
# The first setup deploy saves synology_dsm as this domain's deploy hook.
# acme.sh --cron will run the saved deploy hook after a successful renewal.
export SYNO_USE_TEMP_ADMIN=1
export SYNO_CERTIFICATE="\$CERT_DESC"
export SYNO_CREATE=1
export SYNO_SCHEME="http"
export SYNO_HOSTNAME="localhost"
export SYNO_PORT="5000"
{
echo "==== \$(date '+%Y-%m-%d %H:%M:%S') acme.sh cron start ===="
set +e
"\$ACME_HOME/acme.sh" --cron --home "\$ACME_HOME" --log
cron_rc="\$?"
set -e
if [ "\$cron_rc" = "0" ] || [ "\$cron_rc" = "2" ]; then
echo "acme.sh cron finished with code \$cron_rc"
else
echo "acme.sh cron failed with code \$cron_rc"
exit "\$cron_rc"
fi
echo "==== \$(date '+%Y-%m-%d %H:%M:%S') acme.sh cron done ===="
} >> "\$LOG_FILE" 2>&1
EOF
chmod 700 "$RENEW_SCRIPT"
echo "Renew script created: $RENEW_SCRIPT"
}
secure_saved_files() {
chmod 700 "$ACME_HOME" 2>/dev/null || true
chmod 600 "$ACME_HOME/account.conf" 2>/dev/null || true
if [ -d "$ACME_HOME/${DOMAIN}_ecc" ]; then
chmod 700 "$ACME_HOME/${DOMAIN}_ecc" 2>/dev/null || true
fi
}
show_summary() {
echo
echo "Done."
echo "Domain: $DOMAIN"
echo "DSM certificate description: $CERT_DESC"
echo "Certificate key type: $KEY_LENGTH"
echo "Renew command for DSM weekly task:"
echo "/bin/sh $RENEW_SCRIPT"
echo "Renew log:"
echo "$LOG_FILE"
echo
echo "Verify with:"
if [ -n "$ACME_CERT_EXTRA_ARG" ]; then
echo "$ACME_HOME/acme.sh --info -d $DOMAIN $ACME_CERT_EXTRA_ARG"
else
echo "$ACME_HOME/acme.sh --info -d $DOMAIN"
fi
}
need_root
collect_inputs
install_acmesh_if_needed
issue_certificate
deploy_to_synology
create_renew_script
secure_saved_files
show_summary
保存后执行:
sh /tmp/synology-acme-nas-setup.sh
脚本会依次询问:
ACME account email:
Aliyun AccessKeyId:
Aliyun AccessKeySecret:
说明:
ACME account email是 Let's Encrypt/acme.sh 的联系邮箱,可以填你常用邮箱。Aliyun AccessKeyId和Aliyun AccessKeySecret是 RAM 用户的 AccessKey。- 输入
Aliyun AccessKeySecret时终端不会显示字符,这是正常的。
首次脚本会做这些事:
- 安装或更新
/usr/local/share/acme.sh - 使用阿里云 DNS 自动签发
nas.example.com的 Let's Encrypt 证书 - 使用 DSM 本机临时管理员方式导入/替换证书,不保存你的群晖管理员密码
- 生成后续每周续签脚本:
/usr/local/share/acme.sh/nas-cert-renew.sh
首次成功后,删除 /tmp 里的临时脚本:
rm -f /tmp/synology-acme-nas-setup.sh
注意:acme.sh 会把阿里云 DNS API 凭证保存到 root 用户的 acme 配置中,用于后续自动续签。脚本会把 /usr/local/share/acme.sh/account.conf 权限收紧为 600。
3. 创建 DSM 每周任务计划
第一次脚本成功后,再创建这个正式任务。
在 DSM 页面中:
- 打开
控制面板 -> 任务计划 - 点击
新增 - 选择
计划的任务 -> 用户定义的脚本 常规页:
任务名称:renew cert nas.example.com
用户账号:root
启用:勾选
计划页:
运行日期:在以下天中运行
重复:每周
星期:周一
开始时间:03:30
不要勾选“在同一天内继续运行”
任务设置页,用户定义脚本只填这一行:
/bin/sh /usr/local/share/acme.sh/nas-cert-renew.sh
这个每周任务不会每周都重新申请证书。acme.sh --cron 会判断证书是否接近到期,通常到期前约 30 天才续签。首次部署时保存的 synology_dsm deploy hook 会在续签成功后自动把新证书导入 DSM。
4. 验证是否成功
SSH 里查看证书信息:
/usr/local/share/acme.sh/acme.sh --info -d nas.example.com --ecc
查看续签任务日志:
tail -n 100 /var/log/acme-nas-cert-renew.log
回到 DSM 页面确认:
控制面板 -> 安全性 -> 证书
检查:
nas.example.com不再显示过期- 如果脚本创建了新证书,进入
证书 -> 设置,确认相关服务绑定到nas.example.com - 如果原来的
nas.example.com是默认证书,acme.sh 替换同名证书时通常会保留默认状态
5. 常见问题
5.1 脚本会自动购买阿里云免费证书吗?
不会。
它不使用阿里云 SSL 证书管理服务,不会自动购买、申请、下载 Apache 证书。
它使用 Let's Encrypt 免费证书,阿里云只负责 DNS 验证。
5.2 没有已购买的阿里云证书会失败吗?
不会。
证书由 Let's Encrypt 新签发。DSM 里如果已有描述为 nas.example.com 的证书,会替换它;如果没有同名证书,脚本设置了 SYNO_CREATE=1,会创建新证书。
5.3 报错:You don't specify aliyun api key and secret yet
说明脚本没有拿到阿里云 RAM 的 AccessKey。
请通过 SSH 交互运行脚本,看到提示后输入:
Aliyun AccessKeyId
Aliyun AccessKeySecret
不要用没有交互输入能力的 DSM 任务计划来跑首次部署脚本。
5.4 报错:For creating temp admin user, the deploy script must be run as root
说明不是 root 用户运行。
SSH 中先执行:
sudo -i
或者 DSM 任务计划中用户账号必须选择 root。
5.5 报错:Cannot find path 或找不到证书目录
旧版脚本可能漏了 ECC 参数。当前脚本签发 ec-384 证书,并在部署时带了 --ecc。
请使用最新的 synology-acme-nas-setup.sh。
5.6 DNS 验证失败
可能是阿里云权限不足,或 DNS TXT 记录传播慢。
检查 RAM 用户是否有:
alidns:DescribeDomainRecords
alidns:AddDomainRecord
alidns:DeleteDomainRecord
当前脚本默认等待 DNS 传播 120 秒。如果仍失败,可以稍后重新执行一次首次部署脚本。
6. 回滚和停止
如果新证书部署后想回退:
- 进入
控制面板 -> 安全性 -> 证书 - 选择旧证书或
synology - 进入
设置 - 把相关服务切回旧证书
如果只想停止自动续签:
- 删除 DSM 任务计划里的
renew cert nas.example.com - 保留
/usr/local/share/acme.sh,不会影响当前 DSM 证书
如果要删除本次自动化文件:
rm -f /usr/local/share/acme.sh/nas-cert-renew.sh
rm -f /var/log/acme-nas-cert-renew.log
谨慎删除整个 /usr/local/share/acme.sh,除非确认没有其他证书在使用它。

浙公网安备 33010602011771号