群晖 NAS 公网访问配置(五):使用 certd 免费版配置泛域名证书
如果你和本站一样使用了 .dev 域,那么你肯定也会遇到和我一样的问题:只用 http 无法访问服务,浏览器会强制使用 HTTPS。
这个问题的背景是:
.dev是一个正式的通用顶级域(gTLD),由 Google(Charleston Road Registry / Google Registry)运营。
整个.dev被放到 HSTS preload 列表里,主流浏览器会把对.dev的请求当作必须走 HTTPS 的站点处理——如果没有有效 TLS,浏览器会拒绝/升级连接,导致无法通过http://yourname.dev正常打开。也就是说,使用.dev发布站点时必须配置有效的证书(Let’s Encrypt 等都可用)。
所以有了本篇文章:教你如何通过 certd 这个开源项目申请泛域名证书,并通过流水线定时触发 + 脚本导入实现全自动更新,以便 DSM 面板和 NAS 上的服务都能安全访问。
本文为《群晖 NAS 公网访问配置》系列文章的第五篇。全部文章请参考:
群晖 NAS 公网访问配置(一):配置 DDNS
群晖 NAS 公网访问配置(二):配置 WireGuard
群晖 NAS 公网访问配置(三):配置 Nginx 访问 Docker 服务
群晖 NAS 公网访问配置(四):配置云服务器
群晖 NAS 公网访问配置(五):使用 certd 免费版配置泛域名证书
方案概览
这套方案的核心思路很简单:
- 用
certd免费版通过 DNS-01 验证申请泛域名证书。 - 在流水线里把证书写入固定路径,再用脚本自动导入 DSM。
- 配置流水线定时触发,证书到期前自动续期并自动导入,全程不手动。
前置条件
- 你已按前四篇完成 DDNS、WireGuard、Nginx 等配置。
- 域名托管在腾讯云,并且能使用 API 密钥操作 DNS 解析。
- 群晖已安装 Container Manager(或 Docker 套件),可以运行容器。
- 先在套件中心安装 Python3 套件,脚本会用
python3(或python)解析 DSM 返回的数据。 - 准备一个主域名,支持
*.example.com泛域名证书。
在群晖部署 certd
certd 官方提供了 Docker 部署方式,群晖上直接用 Container Manager 即可。
- 打开 Container Manager,新增项目,选择
docker-compose.yaml。 - 建议使用下面的精简配置,并把数据目录改成你自己的卷路径。
version: "3.3"
services:
certd:
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
container_name: certd
restart: unless-stopped
volumes:
- /volume1/docker/certd:/app/data:delegated
ports:
- "7001:7001"
- "7002:7002"
environment:
- TZ=Asia/Shanghai
- certd_koa_hostname=0.0.0.0
- 部署完成后访问
http://群晖 IP:7001,使用默认账号admin/123456登录,并第一时间修改密码。
如果你不希望暴露端口,可仅在内网访问 certd,或通过群晖反向代理映射到一个内网域名。
在 certd 中申请泛域名证书
1. 添加腾讯云授权
进入 certd 的授权管理,新增腾讯云授权,填写 SecretId 和 SecretKey。
这两个值可以复用你在 DDNS 配置时创建的密钥。
2. 创建证书申请流水线
进入证书流水线,新增 “证书申请(JS 版)”,按下面的方式填写:
- 域名验证方式:DNS 直接验证。
- 证书颁发机构:选择 “Let’s Encrypt”。
- DNS 解析服务商:腾讯云。
- 证书域名:填写主域名和泛域名,例如
example.com与*.example.com。 - 更新天数:按需设置(默认值以界面为准),表示证书到期前多少天触发更新。
保存后不需要手动运行,后面设置定时触发后会自动执行。
如果你希望尽快拿到证书,可以把定时触发时间设置为当前时间后几分钟,让它自动跑首轮。
如果失败,多半是 DNS 生效时间不够。可以在流水线里把“等待解析生效时长”适当调大,再试一次。
将证书输出到固定路径
为了让脚本能拿到证书文件,需要把证书保存到 NAS 上一个固定目录。certd 的流水线里加一个“主机 - 复制到本机”任务即可。
- 打开对应流水线,新增任务 “主机 - 复制到本机”。
- 证书类型选择
pem。 - 路径建议这样填(相对路径会写到
/app/data下):- 全链证书保存路径:
tmp/fullchain.pem - 私钥保存路径:
tmp/private.key - 中间证书保存路径:
tmp/intermediate.pem
- 全链证书保存路径:
因为我在 docker-compose.yaml 里把 /volume1/docker/certd 映射到容器的 /app/data,所以上面三个文件最终会落在:
/volume1/docker/certd/tmp/fullchain.pem/volume1/docker/certd/tmp/private.key/volume1/docker/certd/tmp/intermediate.pem
你可以按自己的映射关系调整路径,但脚本里要用同一套路径。
用脚本导入证书到 DSM
(可选)为启用 2FA 的账户获取 device-id
如果你的 DSM 账户启用了两步验证(2FA),需要先获取一次 device-id,后续就可以用它来免 OTP 登录。
在你的本地电脑(或任何能访问 NAS 内网的设备)上执行以下命令,只需要执行一次(记得替换其中的参数):
curl -sS -k -G "https://192.168.1.10:5001/webapi/entry.cgi" \
--data-urlencode "api=SYNO.API.Auth" \
--data-urlencode "version=6" \
--data-urlencode "method=login" \
--data-urlencode "format=sid" \
--data-urlencode "account=admin" \
--data-urlencode "passwd=your-password" \
--data-urlencode "otp_code=123456" \
--data-urlencode "device_name=certd" \
--data-urlencode "enable_device_token=yes" \
--data-urlencode "enable_syno_token=yes" \
| grep -oP '"device_id"\s*:\s*"\K[^"]+'
参数说明:
https://192.168.1.10:5001:你的 NAS 内网地址和端口account:DSM 管理员账号passwd:DSM 管理员密码otp_code:从你的两步验证应用(如 Google Authenticator)获取的 6 位数字device_name:自定义设备名称,用于在 DSM 中标识该设备,建议填certd
命令会输出类似 aBcD1234eFgH5678 这样的字符串,这就是你的 device-id,记录下来供后续脚本使用。
如果命令执行失败或没有输出,检查:
- OTP 码是否正确且未过期(有效期 30 秒)
- 网络是否能访问到 NAS
- 账号密码是否正确
准备导入脚本
首次使用前不需要手动准备证书记录。执行脚本时传入 --cert-desc,并加上 --create,脚本会在 DSM 里找不到同名描述时自动创建证书记录并导入证书。
如果你需要手动排查或验证,也可以在 DSM 里走“新增 → 新增新证书 → 导入证书”,导入一次后再交给脚本更新。
DSM 自带的
Let's Encrypt需要公网 80 端口可达,且通常不支持DNS-01与泛域名,不适合本方案。
把下面脚本保存到 NAS 上,例如 /volume1/docker/certd/scripts/synology-import-cert.sh,无需修改脚本内容,所有配置都通过参数传入。
#!/usr/bin/env bash
set -euo pipefail
# 基于 DSM API 导入证书,并可选设置默认证书与服务绑定。
# 依赖 curl 和 awk 以及 sed,使用 --assign-services 时需要安装 Python3 套件。
usage() {
cat <<'EOF'
用法:
synology-import-cert.sh --base-url URL --username USER --password PASS --cert-desc DESC --fullchain PATH --key PATH [选项]
必填参数:
--base-url DSM 地址,例如 https://192.168.1.10:5001
--username DSM 管理员账号
--password DSM 管理员密码
--cert-desc 证书描述,用于定位或创建记录
--fullchain 全链证书路径
--key 私钥路径
可选参数:
--create 证书描述不存在时自动创建
--insecure 跳过 HTTPS 证书校验(等同 curl -k)
--set-default 设置为系统默认证书
--assign-services 绑定服务名称,逗号分隔,支持 all 表示全部
--device-id 2FA 已信任设备 ID
--device-name 设备名称(自定义),默认为 certd
-h, --help 显示帮助
提示:
使用 --assign-services 需要先安装 Python3 套件。
EOF
}
# 记录参数默认值,后续会被命令行参数覆盖。
SYNO_BASE_URL=""
SYNO_USERNAME=""
SYNO_PASSWORD=""
SYNO_CERT_DESC=""
CERT_FULLCHAIN=""
CERT_KEY=""
SYNO_CREATE="0"
SYNO_SET_DEFAULT="0"
SYNO_ASSIGN_SERVICES=""
SYNO_DEVICE_NAME="certd"
SYNO_DEVICE_ID=""
INSECURE="0"
# 统一错误输出,方便在流水线日志中定位问题。
fail() {
echo "错误:$1" >&2
exit 1
}
# 检查必填参数是否存在。
require_arg() {
[ -n "$2" ] || fail "缺少参数:$1"
}
# 检查参数是否带值。
require_value() {
local flag="$1"
local value="${2:-}"
[ -n "$value" ] || fail "参数 $flag 缺少值"
}
# 解析命令行参数。
while [ "$#" -gt 0 ]; do
case "$1" in
--base-url)
require_value "$1" "${2:-}"
SYNO_BASE_URL="$2"
shift 2
;;
--username)
require_value "$1" "${2:-}"
SYNO_USERNAME="$2"
shift 2
;;
--password)
require_value "$1" "${2:-}"
SYNO_PASSWORD="$2"
shift 2
;;
--cert-desc)
require_value "$1" "${2:-}"
SYNO_CERT_DESC="$2"
shift 2
;;
--fullchain)
require_value "$1" "${2:-}"
CERT_FULLCHAIN="$2"
shift 2
;;
--key)
require_value "$1" "${2:-}"
CERT_KEY="$2"
shift 2
;;
--device-name)
require_value "$1" "${2:-}"
SYNO_DEVICE_NAME="$2"
shift 2
;;
--device-id)
require_value "$1" "${2:-}"
SYNO_DEVICE_ID="$2"
shift 2
;;
--create)
SYNO_CREATE="1"
shift
;;
--set-default)
SYNO_SET_DEFAULT="1"
shift
;;
--assign-services)
require_value "$1" "${2:-}"
SYNO_ASSIGN_SERVICES="$2"
shift 2
;;
--insecure)
INSECURE="1"
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "未知参数:$1"
;;
esac
done
# 校验证书文件是否存在。
require_file() {
[ -f "$1" ] || fail "未找到文件:$1"
}
# 查找可用的 Python,用于解析 DSM 返回的 JSON。
find_python() {
if command -v python3 >/dev/null 2>&1; then
echo "python3"
return 0
fi
if command -v python >/dev/null 2>&1; then
echo "python"
return 0
fi
return 1
}
# 生成服务绑定所需的 settings JSON。
# DSM 需要完整的 service 对象,因此要从证书列表里提取结构。
build_service_settings() {
local services_csv="$1"
local new_cert_id="$2"
local python_bin
python_bin=$(find_python) || fail "未找到 python3 或 python,请先在套件中心安装 Python3 套件后再使用 --assign-services"
local tmp_err
tmp_err=$(mktemp)
local settings
settings=$(
printf "%s" "$cert_list" | "$python_bin" - "$services_csv" "$new_cert_id" 2>"$tmp_err" <<'PY'
import json
import sys
# 读取参数与证书列表。
services_arg = sys.argv[1].strip()
new_id = sys.argv[2]
data = json.load(sys.stdin)
# 建立服务名称到证书与服务信息的映射。
service_map = {}
for cert in data.get("data", {}).get("certificates", []):
for service in cert.get("services", []) or []:
name = service.get("display_name") or ""
if not name:
continue
service_map[name] = {
"service": service,
"old_id": cert.get("id", "")
}
if not service_map:
sys.stderr.write("未获取到服务列表,请确认 DSM 返回的证书列表包含 services 字段。\n")
sys.exit(2)
# 解析服务名称,支持 all 或 * 表示全部。
if services_arg.lower() in ("all", "*"):
services = sorted(service_map.keys())
else:
services = [s.strip() for s in services_arg.split(",") if s.strip()]
if not services:
sys.stderr.write("服务名称不能为空。\n")
sys.exit(2)
missing = [name for name in services if name not in service_map]
if missing:
available = sorted(service_map.keys())
sys.stderr.write("未找到服务名称:" + ", ".join(missing) + "\n")
if available:
sys.stderr.write("可选服务名称:" + ", ".join(available) + "\n")
sys.exit(2)
# 生成 DSM 需要的 settings 数组结构。
settings = []
for name in services:
info = service_map[name]
settings.append({"service": info["service"], "old_id": info["old_id"], "id": new_id})
sys.stdout.write(json.dumps(settings, separators=(",", ":")))
PY
) || {
local err
err=$(cat "$tmp_err")
rm -f "$tmp_err"
[ -n "$err" ] || err="生成服务绑定参数失败"
fail "$err"
}
rm -f "$tmp_err"
printf "%s" "$settings"
}
# 检查必填参数。
require_arg "--base-url" "$SYNO_BASE_URL"
require_arg "--username" "$SYNO_USERNAME"
require_arg "--password" "$SYNO_PASSWORD"
require_arg "--cert-desc" "$SYNO_CERT_DESC"
require_arg "--fullchain" "$CERT_FULLCHAIN"
require_arg "--key" "$CERT_KEY"
# 校验证书文件路径。
require_file "$CERT_FULLCHAIN"
require_file "$CERT_KEY"
# 如果要绑定服务,提前确认 Python 可用。
if [ -n "$SYNO_ASSIGN_SERVICES" ]; then
if ! find_python >/dev/null 2>&1; then
fail "未找到 python3 或 python,请先在套件中心安装 Python3 套件后再使用 --assign-services"
fi
fi
# 组装 curl 参数,可选跳过 HTTPS 证书校验。
CURL_ARGS=(-sS)
# 如需忽略证书校验,则追加 -k。
if [ "$INSECURE" = "1" ]; then
CURL_ARGS+=(-k)
fi
# 获取 DSM API 入口路径与版本,兼容不同 DSM 版本。
auth_info=$(curl "${CURL_ARGS[@]}" "$SYNO_BASE_URL/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query&query=SYNO.API.Auth")
api_path=$(echo "$auth_info" | sed -n 's/.*"path" *: *"\([^"]*\)".*/\1/p')
api_version=$(echo "$auth_info" | sed -n 's/.*"maxVersion" *: *\([0-9]\+\).*/\1/p')
# 如果返回为空,使用常见默认值兜底。
[ -n "$api_path" ] || api_path="entry.cgi"
[ -n "$api_version" ] || api_version="6"
# 登录获取 sid 与 SynoToken,后续接口都依赖它们。
login_args=(
"${CURL_ARGS[@]}" -G "$SYNO_BASE_URL/webapi/$api_path"
--data-urlencode "api=SYNO.API.Auth"
--data-urlencode "version=$api_version"
--data-urlencode "method=login"
--data-urlencode "format=sid"
--data-urlencode "account=$SYNO_USERNAME"
--data-urlencode "passwd=$SYNO_PASSWORD"
--data-urlencode "enable_syno_token=yes"
)
# 已信任设备登录,适合自动化场景。
if [ -n "$SYNO_DEVICE_ID" ]; then
login_args+=(--data-urlencode "device_id=$SYNO_DEVICE_ID" --data-urlencode "device_name=$SYNO_DEVICE_NAME")
fi
login_response=$(curl "${login_args[@]}")
# 简单判断是否有错误码返回。
if echo "$login_response" | grep -q '"error"'; then
error_code=$(echo "$login_response" | sed -n 's/.*"code":\([0-9]\+\).*/\1/p')
fail "登录失败,错误码:$error_code"
fi
# 解析会话标识与 Token。
sid=$(echo "$login_response" | sed -n 's/.*"sid" *: *"\([^"]*\)".*/\1/p')
token=$(echo "$login_response" | sed -n 's/.*"synotoken" *: *"\([^"]*\)".*/\1/p')
[ -n "$sid" ] || fail "未获取到 sid"
[ -n "$token" ] || fail "未获取到 SynoToken"
# 获取证书列表,用于定位证书 ID 与服务列表。
cert_list=$(curl "${CURL_ARGS[@]}" -X POST "$SYNO_BASE_URL/webapi/entry.cgi" \
-H "X-SYNO-TOKEN: $token" \
--data "api=SYNO.Core.Certificate.CRT" \
--data "method=list" \
--data "version=1" \
--data "_sid=$sid")
if echo "$cert_list" | grep -q '"error"'; then
fail "获取证书列表失败,请检查账号权限。"
fi
# 基于证书描述匹配证书 ID,避免额外 JSON 解析依赖。
escaped_desc=$(printf "%s" "$SYNO_CERT_DESC" | sed 's/\([].*^$[]\)/\\\1/g;s/"/\\\\"/g')
cert_id=$(echo "$cert_list" | sed -n "s/.*\\\"desc\\\":\\\"$escaped_desc\\\",\\\"id\\\":\\\"\\([^\"]*\\)\\\".*/\\1/p")
if [ -z "$cert_id" ]; then
if [ "${SYNO_CREATE:-}" = "1" ]; then
echo "提示:未找到证书描述,将自动创建记录:$SYNO_CERT_DESC"
else
fail "未找到证书描述:$SYNO_CERT_DESC,请先在 DSM 中创建同名证书记录。"
fi
fi
# 记录当前证书是否是默认值,便于复用。
is_default=$(echo "$cert_list" | sed -n "s/.*\\\"desc\\\":\\\"$escaped_desc\\\"[^}]*\\\"is_default\\\":\\(true\\|false\\).*/\\1/p")
# DSM 导入接口要求叶子证书与中间证书分开。
tmp_leaf=$(mktemp)
tmp_intermediate=$(mktemp)
trap 'rm -f "$tmp_leaf" "$tmp_intermediate"' EXIT
: > "$tmp_leaf"
: > "$tmp_intermediate"
awk '/-----BEGIN CERTIFICATE-----/ {n++} { if (n == 1) print > leaf; else print > inter }' \
leaf="$tmp_leaf" inter="$tmp_intermediate" "$CERT_FULLCHAIN"
# 上传证书并可选设置为默认。
upload_url="$SYNO_BASE_URL/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1&SynoToken=$token&_sid=$sid"
upload_args=(
"${CURL_ARGS[@]}" -X POST "$upload_url"
-H "X-SYNO-TOKEN: $token"
-F "key=@$CERT_KEY"
-F "cert=@$tmp_leaf"
-F "inter_cert=@$tmp_intermediate"
-F "id=$cert_id"
-F "desc=$SYNO_CERT_DESC"
)
# 如果显式设置默认,或原记录已是默认,则保持默认状态。
if [ "$SYNO_SET_DEFAULT" = "1" ] || [ "$is_default" = "true" ]; then
upload_args+=(-F "as_default=true")
fi
upload_response=$(curl "${upload_args[@]}")
if echo "$upload_response" | grep -q '"success":true'; then
echo "证书导入完成。"
new_cert_id=$(echo "$upload_response" | sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p')
[ -n "$new_cert_id" ] && cert_id="$new_cert_id"
else
fail "证书导入失败:$upload_response"
fi
# 可选:再次调用接口设置系统默认证书。
if [ "$SYNO_SET_DEFAULT" = "1" ]; then
[ -n "$cert_id" ] || fail "未获取到证书 ID,无法设置默认证书"
default_id="\"$cert_id\""
default_desc="\"\""
set_default_response=$(curl "${CURL_ARGS[@]}" -X POST "$SYNO_BASE_URL/webapi/entry.cgi" \
-H "X-SYNO-TOKEN: $token" \
--data-urlencode "api=SYNO.Core.Certificate.CRT" \
--data-urlencode "method=set" \
--data-urlencode "version=1" \
--data-urlencode "_sid=$sid" \
--data-urlencode "id=$default_id" \
--data-urlencode "desc=$default_desc" \
--data-urlencode "as_default=true")
if echo "$set_default_response" | grep -q '"success":true'; then
echo "默认证书已更新。"
else
fail "设置默认证书失败:$set_default_response"
fi
fi
# 可选:绑定服务证书。
if [ -n "$SYNO_ASSIGN_SERVICES" ]; then
[ -n "$cert_id" ] || fail "未获取到证书 ID,无法绑定服务"
settings_json=$(build_service_settings "$SYNO_ASSIGN_SERVICES" "$cert_id")
assign_response=$(curl "${CURL_ARGS[@]}" -X POST "$SYNO_BASE_URL/webapi/entry.cgi" \
-H "X-SYNO-TOKEN: $token" \
--data-urlencode "api=SYNO.Core.Certificate.Service" \
--data-urlencode "method=set" \
--data-urlencode "version=1" \
--data-urlencode "_sid=$sid" \
--data-urlencode "settings=$settings_json")
if echo "$assign_response" | grep -q '"success":true'; then
echo "服务绑定完成。"
else
fail "服务绑定失败:$assign_response"
fi
fi
# 登出释放会话。
curl "${CURL_ARGS[@]}" -G "$SYNO_BASE_URL/webapi/$api_path" \
--data-urlencode "api=SYNO.API.Auth" \
--data-urlencode "version=$api_version" \
--data-urlencode "method=logout" \
--data-urlencode "_sid=$sid" >/dev/null 2>&1 || true
赋予脚本可执行权限:
chmod +x /volume1/docker/certd/scripts/synology-import-cert.sh
补充说明:
- 默认不跳过 HTTPS 证书校验。需要忽略校验时添加
--insecure,避免首次接入时因自签证书或域名不匹配而登录失败。 --create表示当证书描述不存在时自动创建记录,这样就不需要手动导入占位证书。该参数名称与行为来自 acme.sh 的 Synology 部署脚本。--set-default会把证书设置为系统默认证书,适合用来消除首次手动设置的步骤。--assign-services用于绑定服务名称,名称需要与 API 返回的display_name一致;如果不匹配,脚本会提示可选列表。支持all表示全部服务。该功能依赖python3或python。- 如果账号启用了 2FA,需要使用前面获取的
--device-id参数。--device-name有默认值certd,如需自定义可以传递该参数。未开启 2FA 时,这两个参数都不需要。你也可以为证书更新新建一个不启用 2FA 的管理员账号。
让流水线自动执行脚本
要做到全自动,需要让 certd 在证书更新后自动执行脚本。做法是给流水线增加一个 “主机 - 执行远程主机脚本命令” 任务。
- 在
certd的 “授权管理” 里新增 SSH 授权,主机填 NAS 地址,端口填你的 SSH 端口(可在 DSM 的 “控制面板 → 终端机和 SNMP → 终端机” 中查看,默认为22),账号使用管理员账户。
如果你之前没开 SSH,可以在 DSM 的 “控制面板 → 终端机和 SNMP → 终端机” 里开启。 - 回到证书流水线,在 “主机 - 复制到本机” 之后新增 “主机 - 执行远程主机脚本命令” 任务。
- 选择刚才的 SSH 授权,脚本命令填写:
如果启用了 2FA(使用前面获取的 device-id):
bash /volume1/docker/certd/scripts/synology-import-cert.sh \
--base-url "https://192.168.1.10:5001" \
--username "admin" \
--password "your-password" \
--cert-desc "nas-wildcard" \
--fullchain "/volume1/docker/certd/tmp/fullchain.pem" \
--key "/volume1/docker/certd/tmp/private.key" \
--device-id "your-device-id" \
--set-default \
--assign-services "all" \
--insecure \
--create
参数说明:
- 如果 DSM 证书尚未可信或域名不匹配,保留
--insecure。 - 如果你希望一次性覆盖全部服务,保留
--assign-services all;也可以指定具体服务名称,例如"DSM Desktop Service,Reverse Proxy"。
配置完成后,可以手动运行一次流水线测试。如果一切正常,证书会自动导入到 DSM。这样流水线每次拿到新证书后都会自动更新 DSM。
首次运行成功后,建议优化脚本配置:将 --base-url 改为你的域名,并去掉 --insecure 参数,这样可以正常校验 HTTPS 证书,更加安全:
bash /volume1/docker/certd/scripts/synology-import-cert.sh \
--base-url "https://nas.example.com:5001" \
--username "admin" \
--password "your-password" \
--cert-desc "nas-wildcard" \
--fullchain "/volume1/docker/certd/tmp/fullchain.pem" \
--key "/volume1/docker/certd/tmp/private.key" \
--device-id "your-device-id" \
--set-default \
--assign-services "all" \
--create
配置 Nginx 使用 SSL 证书
现在证书已经通过 certd 自动管理了,接下来需要让 Nginx 使用这些证书来支持 HTTPS 访问。我们使用 DSM 自带的 Nginx(参考第三篇文章)。
修改 Nginx 配置
DSM 自带的 Nginx 配置文件在 /etc/nginx/sites-enabled 目录下。
假设你在第三篇文章中为 calibre 配置了 calibre.example.com.conf,现在需要修改它以支持 HTTPS。
使用 SSH 连接到 NAS,编辑对应的配置文件:
sudo vim /etc/nginx/sites-enabled/calibre.example.com.conf
将原有的配置改为:
# HTTP 自动跳转到 HTTPS
server {
listen 80;
server_name calibre.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS 配置
server {
listen 443 ssl;
server_name calibre.example.com;
# SSL 证书配置
ssl_certificate /volume1/docker/certd/tmp/fullchain.pem;
ssl_certificate_key /volume1/docker/certd/tmp/private.key;
# SSL 协议与加密套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_pass http://172.17.0.1:8083;
client_max_body_size 2000m;
}
}
关键修改点:
- 新增 80 端口的 server 块,自动跳转到 HTTPS
- 原有的 server 块改为监听 443 端口,并启用 SSL
- 使用 certd 导出的证书路径:
/volume1/docker/certd/tmp/fullchain.pem和/volume1/docker/certd/tmp/private.key - 新增
X-Forwarded-Protoheader,让后端应用知道使用的是 HTTPS
重启 Nginx
保存配置文件后,测试配置并重启 Nginx 使配置生效:
sudo /usr/bin/nginx -c /etc/nginx/nginx.conf.run -t
sudo /usr/bin/nginx -c /etc/nginx/nginx.conf.run -s reload
验证 HTTPS 是否生效
用浏览器访问 https://calibre.example.com,检查:
- 浏览器地址栏显示小锁图标。
- 证书信息显示正确的域名和颁发机构。
- 页面能正常加载,没有证书警告。
如果有问题,可以查看 Nginx 日志:
sudo tail -f /var/log/nginx/error.log
常见问题:
- 证书路径不对:确认
/volume1/docker/certd/tmp/下有fullchain.pem和private.key文件。 - 权限问题:确保 Nginx 能读取证书文件,可以执行
sudo chmod 644 /volume1/docker/certd/tmp/*.pem /volume1/docker/certd/tmp/*.key。 - 证书未更新:certd 更新证书后,Nginx 需要重启才能重新加载证书。你可以在流水线的 “主机 - 执行远程主机脚本命令” 任务中,在导入脚本后面加一个重启命令:
bash /volume1/docker/certd/scripts/synology-import-cert.sh \
--base-url "https://nas.example.com:5001" \
--username "admin" \
--password "your-password" \
--cert-desc "nas-wildcard" \
--fullchain "/volume1/docker/certd/tmp/fullchain.pem" \
--key "/volume1/docker/certd/tmp/private.key" \
--device-id "your-device-id" \
--set-default \
--assign-services "all" \
--create
# 重启 Nginx 以重新加载证书
sudo /usr/bin/nginx -c /etc/nginx/nginx.conf.run -s reload
续期与维护
Let’s Encrypt 证书有效期是 90 天。
要实现全自动,只需要在流水线里配置定时触发即可。
建议设置为每天执行一次,例如 0 0 3 * * *(每天凌晨 3 点)。
certd 会在证书到期前按照“更新天数”设定的天数自动申请新证书并继续执行后续部署任务,没到期前不会重复申请。默认值以界面为准。