群晖自动化更新证书,同步Lucky大吉

本文讨论的是局域网内已有一台部署了自动续期证书的机器,本文以部署了Lucky大吉服务的openwrt为例,需要将该证书同步到群辉。

你也可以通过Lucky大吉的Web服务(反向代理功能)来实现,本文不做赘述。

openwrt侧操作

Lucky证书路径映射

即证书保存位置,我填写的是/etc/lucky,假设你的证书名称为example.com,那么实际证书路径应为证书/etc/lucky/example.com.pem、私钥/etc/lucky/example.com.key、中间证书/etc/lucky/example.com_issuerCertificate.crt

配置私钥登录openwrt

在任意机器上生成rsa公私钥密钥对

1
ssh-keygen -t rsa -f route

将公钥route.pub上传至openwrt,将私钥route妥善保存,我将其上传到了群晖的/var/services/homes/yourname/.ssh/routeyourname是你的用户名。

群辉侧操作

新建证书

安全性 -> 证书新建一个自签名证书,确定其使用范围。

确定证书目录

默认的证书目录为/usr/syno/etc/certificate/_archive内的由六位字符构成的目录。例如我的证书文件夹为/usr/syno/etc/certificate/_archive/spaE6k

新建脚本

新建文件夹/var/services/homes/yourname/cert,新建文件update.sh并填入以下,更改关键变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/bin/bash

# ========== 配置 ==========
TMP_DIR="/var/services/homes/yourname/cert/certs"
mkdir -p "$TMP_DIR"

REMOTE_HOST="root@192.168.2.1"
SSH_KEY="/var/services/homes/yourname/.ssh/route"
REMOTE_BASE="/etc/lucky"
REMOTE_CERT="example.com.pem"
REMOTE_KEY="example.com.key"
REMOTE_ISSUER="example.com_issuerCertificate.crt"

LOCAL_BASE="/usr/syno/etc/certificate/_archive/spaE6k"

# ========== 工具路径(群晖专用) ==========
SYNOW3TOOL="/usr/syno/bin/synow3tool"
NGINX_BIN="nginx" # 如果不在 PATH 中,请改为 /usr/sbin/nginx

# ========== 下载到永久临时目录 ==========
echo "→ 下载证书到 $TMP_DIR"
scp -i "$SSH_KEY" "$REMOTE_HOST:$REMOTE_BASE/$REMOTE_CERT" "$TMP_DIR/$REMOTE_CERT"
if [ $? -ne 0 ]; then
echo "✗ 下载 $REMOTE_CERT 失败"
exit 1
fi
scp -i "$SSH_KEY" "$REMOTE_HOST:$REMOTE_BASE/$REMOTE_KEY" "$TMP_DIR/$REMOTE_KEY"
if [ $? -ne 0 ]; then
echo "✗ 下载 $REMOTE_KEY 失败"
exit 1
fi
scp -i "$SSH_KEY" "$REMOTE_HOST:$REMOTE_BASE/$REMOTE_ISSUER" "$TMP_DIR/$REMOTE_ISSUER"
if [ $? -ne 0 ]; then
echo "✗ 下载 $REMOTE_ISSUER 失败"
exit 1
fi

# ========== 检查变化 ==========
NEED_UPDATE=0
if [ -f "$LOCAL_BASE/cert.pem" ] && [ -f "$LOCAL_BASE/privkey.pem" ] && [ -f "$LOCAL_BASE/fullchain.pem" ]; then
cmp -s "$LOCAL_BASE/cert.pem" "$TMP_DIR/$REMOTE_CERT" || NEED_UPDATE=1
cmp -s "$LOCAL_BASE/privkey.pem" "$TMP_DIR/$REMOTE_KEY" || NEED_UPDATE=1
# 拼接当前系统的 fullchain 以比较
cat "$TMP_DIR/$REMOTE_CERT" "$TMP_DIR/$REMOTE_ISSUER" > "$TMP_DIR/fullchain.new"
cmp -s "$LOCAL_BASE/fullchain.pem" "$TMP_DIR/fullchain.new" || NEED_UPDATE=1
else
NEED_UPDATE=1
fi

if [ $NEED_UPDATE -eq 0 ]; then
echo "✓ 证书未发生变化,无需更新。"
exit 0
fi

# ========== 备份当前系统证书(旧版本) ==========
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="$TMP_DIR/backup_$TIMESTAMP"
mkdir -p "$BACKUP_DIR"
echo "→ 备份当前系统证书至 $BACKUP_DIR"
for f in cert.pem privkey.pem fullchain.pem; do
if [ -f "$LOCAL_BASE/$f" ]; then
cp "$LOCAL_BASE/$f" "$BACKUP_DIR/${f}_$TIMESTAMP"
else
echo "⚠ 文件 $f 不存在(首次部署),跳过备份"
fi
done

# ========== 拼接完整的 fullchain.pem ==========
cat "$TMP_DIR/$REMOTE_CERT" "$TMP_DIR/$REMOTE_ISSUER" > "$TMP_DIR/fullchain.pem"

# ========== 替换证书到归档目录 ==========
echo "→ 替换证书到归档目录..."
sudo install -o root -g http -m 644 "$TMP_DIR/$REMOTE_CERT" "$LOCAL_BASE/cert.pem"
if [ $? -ne 0 ]; then
echo "✗ 替换 cert.pem 失败,回滚..."
rollback
exit 1
fi
sudo install -o root -g http -m 644 "$TMP_DIR/$REMOTE_KEY" "$LOCAL_BASE/privkey.pem"
if [ $? -ne 0 ]; then
echo "✗ 替换 privkey.pem 失败,回滚..."
rollback
exit 1
fi
sudo install -o root -g http -m 644 "$TMP_DIR/fullchain.pem" "$LOCAL_BASE/fullchain.pem"
if [ $? -ne 0 ]; then
echo "✗ 替换 fullchain.pem 失败,回滚..."
rollback
exit 1
fi

# ========== 定义回滚函数 ==========
rollback() {
echo "→ 正在回滚到备份版本..."
for f in cert.pem privkey.pem fullchain.pem; do
backup_file="$BACKUP_DIR/${f}_$TIMESTAMP"
if [ -f "$backup_file" ]; then
sudo install -o root -g http -m 644 "$backup_file" "$LOCAL_BASE/$f"
fi
done
echo "✓ 回滚完成。"
}

# ========== 执行 synow3tool 更新系统证书配置 ==========
echo "→ 执行 $SYNOW3TOOL --gen-all 更新所有服务证书配置..."
sudo $SYNOW3TOOL --gen-all
if [ $? -ne 0 ]; then
echo "✗ synow3tool 执行失败,回滚证书..."
rollback
sudo $SYNOW3TOOL --gen-all
exit 1
fi

# ========== 检查 Nginx 配置 ==========
echo "→ 检查 Nginx 配置..."
if $NGINX_BIN -t; then
echo "✓ 配置正确,重新加载 Nginx..."
if $NGINX_BIN -s reload; then
echo "✓ 证书更新成功!"
exit 0
else
echo "✗ Reload 失败,回滚证书..."
rollback
exit 1
fi
else
echo "✗ Nginx 配置语法错误,回滚证书..."
rollback
exit 1
fi

这里还提供一个restore.sh以方便进行回滚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/bin/bash

# ========== 配置 ==========
BACKUP_BASE="/var/services/homes/yourname/cert/certs" # 永久备份目录
ARCHIVE_DIR="/usr/syno/etc/certificate/_archive/spaE6k" # 证书归档目录
LOCAL_FILES=("cert.pem" "privkey.pem" "fullchain.pem") # 目标文件名

# ========== 检查备份目录是否存在 ==========
if [ ! -d "$BACKUP_BASE" ]; then
echo "✗ 备份目录不存在: $BACKUP_BASE"
exit 1
fi

# ========== 获取所有备份目录(按时间降序排列) ==========
mapfile -t BACKUP_DIRS < <(find "$BACKUP_BASE" -maxdepth 1 -type d -name "backup_*" | sort -r)

if [ ${#BACKUP_DIRS[@]} -eq 0 ]; then
echo "✗ 没有找到任何备份版本。"
exit 1
fi

# ========== 显示备份列表 ==========
echo "可用备份版本(按时间从新到旧):"
for i in "${!BACKUP_DIRS[@]}"; do
# 提取时间戳(例如 backup_20260701_120000 → 2026-07-01 12:00:00)
dir_name=$(basename "${BACKUP_DIRS[$i]}")
timestamp=${dir_name#backup_}
# 将时间戳格式化为可读日期(可选)
date_str=$(date -d "${timestamp:0:8} ${timestamp:9:2}:${timestamp:11:2}:${timestamp:13:2}" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "$timestamp")
printf " [%2d] %s (%s)\n" "$((i+1))" "$dir_name" "$date_str"
done

# ========== 让用户选择 ==========
read -p "请输入要恢复的版本编号(1-${#BACKUP_DIRS[@]}): " choice
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#BACKUP_DIRS[@]}" ]; then
echo "✗ 无效选择。"
exit 1
fi

SELECTED_DIR="${BACKUP_DIRS[$((choice-1))]}"
echo "→ 选择恢复版本: $(basename "$SELECTED_DIR")"

# ========== 检查备份文件完整性 ==========
missing=0
for f in "${LOCAL_FILES[@]}"; do
# 备份文件命名格式:原文件名_时间戳(例如 cert.pem_20260701_120000)
# 由于时间戳不同,我们用通配符匹配
backup_file=$(find "$SELECTED_DIR" -maxdepth 1 -type f -name "${f}_*" | head -n 1)
if [ -z "$backup_file" ]; then
echo "✗ 在备份目录中未找到文件: ${f}_*"
missing=1
else
echo " ✓ 找到备份文件: $(basename "$backup_file")"
fi
done

if [ $missing -eq 1 ]; then
echo "✗ 备份文件不完整,无法恢复。"
exit 1
fi

# ========== 恢复证书到归档目录 ==========
echo "→ 开始恢复证书到 $ARCHIVE_DIR ..."
for f in "${LOCAL_FILES[@]}"; do
backup_file=$(find "$SELECTED_DIR" -maxdepth 1 -type f -name "${f}_*" | head -n 1)
sudo install -o root -g http -m 644 "$backup_file" "$ARCHIVE_DIR/$f"
if [ $? -ne 0 ]; then
echo "✗ 恢复 $f 失败,脚本退出(请检查是否已安装 sudo 且有权操作)。"
exit 1
fi
echo " ✓ 恢复 $f 成功"
done

# ========== 触发系统证书更新 ==========
echo "→ 执行 synow3tool --gen-all 更新系统证书配置..."
sudo /usr/syno/bin/synow3tool --gen-all
if [ $? -ne 0 ]; then
echo "✗ synow3tool 执行失败,请手动检查。"
exit 1
fi

echo "→ 重新加载 Nginx 服务..."
sudo synosystemctl reload nginx
if [ $? -ne 0 ]; then
echo "⚠ Nginx 重载失败,请检查服务状态。"
exit 1
fi

echo "✓ 证书已成功恢复到版本: $(basename "$SELECTED_DIR")"
exit 0

流程介绍

graph TD
    A([开始]) --> B[设置环境变量与永久临时目录]
    B --> C[从远程服务器下载三个证书文件]
    C --> D{下载是否成功?}
    D -->|失败| E[输出错误并退出]
    D -->|成功| F[检测证书是否变化]
    F --> G{证书与当前系统版本<br/>相同?}
    G -->|是| H[输出 无需更新 并退出]
    G -->|否| I[创建带时间戳的备份目录]
    I --> J[备份当前系统证书<br/>到备份目录]
    J --> K[拼接服务器证书和中间证书<br/>生成 fullchain.pem]
    K --> L[用 install 命令替换归档目录下的<br/>cert.pem, privkey.pem, fullchain.pem]
    L --> M{替换是否成功?}
    M -->|任一失败| N[调用回滚函数恢复备份]
    N --> O([退出])
    M -->|全部成功| P[执行 synow3tool --gen-all<br/>更新所有服务证书配置]
    P --> Q{ synow3tool 执行成功?}
    Q -->|失败| N
    Q -->|成功| R[检查 Nginx 配置: nginx -t]
    R --> S{配置语法正确?}
    S -->|错误| N
    S -->|正确| T[重载 Nginx: nginx -s reload]
    T --> U{重载成功?}
    U -->|失败| N
    U -->|成功| V[输出 更新成功 并退出]
    
    %% 回滚函数详细步骤
    N --> N1[遍历备份目录中的文件<br/>用 install 恢复到归档目录]
    N1 --> O

新建计划任务

新建计划任务 -> 新增 -> 计划的任务 -> 用户定义的脚本,以root运行,每天运行,运行命令填bash /var/services/homes/yourname/cert/update.sh

建议先手动执行再通过计划任务执行。

注意

上述操作会介入群辉默认的证书管理,自行判断风险。

你也可以对脚本稍加修改以便用lucky触发。

一般证书会在到期前一个月尝试续签,所以脚本每日执行即可。

参考

2025年3月12日:群晖7.11使用lucky进行SSL证书的申请和自动更新


群晖自动化更新证书,同步Lucky大吉
https://boyinthesun.cn/posts/0f0dcf9618db.html
作者
BoyInTheSun
发布于
2025年4月22日
更新于
2026年7月1日
许可协议