前言
最近折腾群晖 Nas 部署的时候结合路由器遇到一些问题,就写下记录,方便以后踩坑回头再看。这次遇到的问题是自动化更新 SSL 证书,由于家中路由器性能过好承担一些任务,就包括了 SSL 证书更新,群晖那边共用路由器同一个域名,但系统需要 SSL 自定义域名证书,所以本文就是根据自身情况尽节约互联网资源来更新 SSL 证书。
环境
宽带:联通 200M ,支持 IPv6
主路由器:华硕路由器 RT-AX86U 信息如下:
KoolCenter梅林官改:3004.388.8_2- 安装
DDNS-Go用于自动更新IP - 安装
Let's Encrypt插件用于自动更新域名证书 - 安装
魔法猫咪用于畅通网络
群晖 Nas:Synology DS220+ 信息如下:
DSM版本:DSM 7.2.2-72806 Update 1RAID类别:RAID0- 安装
Docker
光猫修改
宽带师傅安装宽带的时候,默认是路由模式,但这样对宽带速度牺牲很大,主要光猫设备性能不咋的,所以要从路由模式修改成桥接,然后用路由器拨号就行,正好尝试下是否有公网地址。
我家的光猫设备类型GPON/4+1(C 系统) 首先需要知道管理员超级密码,光猫后面账号是默认密码,并没什么用,根据我的型号直接进入光猫网关:
光猫后台:http://192.168.1.1/cu.html
开启Telnet:http://192.168.1.1/telnet?enable=1&key=光猫MAC
关闭Telnet:http://192.168.1.1/telnet?enable=0&key=光猫MAC
直接开启 Telnet 后在终端输入:telnet 192.168.1.1 ,账号为 admin 密码是 Fh@你家光猫的MAC地址后六位
进来之后输入命令:
/var # load_cli factory
cli main init.....argc=2 /r/n
consoleFd = 3
----cli_main_begin 475 3
########cli sip init ok########
cannot open (fh-bin)confile
cl_run_memfile_config unlocked success!
Config\factorydir# show admin_name
Success! admin_name=CUAdmin
Config\factorydir# show admin_pwd
Success! admin_pwd=CUAdminPs6nAyME
这里面的密码还会随时变,所以需要进入的时候建议从头再查看,然后进入基本配置选项中的网络连接修改成桥接模式即可,这里就不再详细描述,这类教程网上一大把。
AX86U 路由器设置
设置好光猫后在路由器设置拨号账号,输入宽带和密码即可,然后在 IPv6 设置选择 Native 选项即可获取 IPv6 地址。去无线网络 - 专业设置关闭 无线传输公平性 和 通用 Beamforming 选项可以解决家里智能设备离线的问题。去软件中心安装 Let's Encrypt 插件配置好相关配置后,在 USB 相关应用 新建一个 FTP 账号,这个账号用于给群晖自动更新证书用的,这里注意是专门新建一个文件夹,然后文件夹只授权 Read 权限,不需要给他读写权限以免账号被黑。
然后登录路由器生成 RSA 密钥对用于路由器免密登录群晖来执行脚本操作:
# 大多数环境下生成密钥是ssh-keygen,但在华硕路由器命令是dropbearkey
dropbearkey -t rsa -f id_rsa
生成好的密钥 id_rsa 文件放在 ~/.ssh 文件内,然后将 id_rsa.pub 内容复制到记录下来,到后面会用到。
后续我发现路由器重启 .ssh 目录密钥会丢失,所以换个位置,例如我放在硬盘上:
/tmp/mnt/WDisk/WorkTemp/ssh/id_rsa
路由器安装的是 Let's Encrypt 插件实际上是 acme.sh 实现的,这里就不关注细节,我找了下插件的位置,在路由器的 /koolshare/scripts/ 位置下面的 acme_config.sh ,查阅代码在更新证书任何情况下会走 install_cert 方法,所以在 install_cert 方法结束后执行更新证书到 FTP 公开地方然后再执行群晖的脚本即可:
install_cert(){
# ... 更多内容就不展示了
# 在最后添加下面代码
# 复制证书并且执行群晖脚本
cp /koolshare/acme/你的域名/你的域名.cer /mnt/WDisk/iNas/cert.pem
cp /koolshare/acme/你的域名/你的域名.key /mnt/WDisk/iNas/key.pem
cp /koolshare/acme/你的域名/fullchain.cer /mnt/WDisk/iNas/fullchain.pem
# 此处是群晖 ssh 免密登录执行脚本
ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa root@192.168.50.2 -p 2233 '/root/scripts/my_script.sh'
# ssh -o StrictHostKeyChecking=no -i /tmp/mnt/WDisk/WorkTemp/ssh/id_rsa root@192.168.50.2 -p 2233 '/root/scripts/my_script.sh'
}
路由器端口转发
外部网络(WAN) - 端口转发新增配置:
- 服务名称:NAS Server
- 通讯协议:TCP
- 外部端口:5000,5001,6690
- 内部端口:空
- 本地 IP 地址:填写 NAS 本地 IP 地址
- 来源 IP:空
5000,5001对应 Synology DSM 端口号,6690 是强制要求转发端口,否则无法正常使用 Drive 等相关应用
Synology 群晖设置
群晖要打开控制面板 - 终端机和 SNMP - 启用 SSH,然后修改端口号,建议修改成千位端口号,打开终端登录群晖的 ssh:
ssh 你的用户名@群晖ip -p 端口号
# 登录 root 账号
sudo -i
cd /root/.ssh
# 如果提示文件不存在那就新建 .ssh 文件夹
mkdir .ssh
cd .ssh
vi authorized_keys
# 将上面路由器复制的 id_ras.pub 文件内容复制进去
chmod 755 ~
chmod 600 ~/.ssh/authorized_keys
vim /etc/ssh/sshd_config
# 编辑 sshd_config 将前面 # 删除
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
然后在群晖 控制面板 -> 终端机和 SNMP 关闭再开启 SSH,登录路由器输入:
ssh -i ~/.ssh/id_rsa root@192.168.50.2 -p 2233
是否直接能登录群晖,如果提示权限不足请检查下是否设置 authorized_keys 权限为 600 。登录成功回到群晖的 ssh 终端在 /root 新建一个文件:
mkdir scripts
vim my_script.sh
脚本内容编写如下:
#!/bin/bash
# 定义FTP相关变量
FTP_SERVER="路由器 ftp 地址包括端口号:192.168.50.1"
FTP_USER="路由器 ftp 账户名"
FTP_PASSWORD="路由器 ftp 密码"
FTP_DIR="路由器 ftp 目录,例如我的是:/WDisk/iNas/"
# 定义本地相关变量
LOCAL_DIR="."
# 定义证书相关变量
CERT_FILE="cert.pem"
KEY_FILE="key.pem"
PRIVKEY_FILE="privkey.pem"
FULLCHAIN_FILE="fullchain.pem"
# 定义目标目录相关变量
CERT_DIRS=(
"/volume1/docker/AList/ssl/"
"/usr/syno/etc/certificate/system/default/"
"/usr/syno/etc/certificate/_archive/$(cat /usr/syno/etc/certificate/_archive/DEFAULT | tr -d '\n')/"
)
# 定义日志文件路径
LOG_FILE="$HOME/scripts/script_log.txt"
# 日志记录函数
log_message() {
local level=$1
local message=$2
local line_number=${3:-"N/A"}
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [LINE:$line_number] [$level] $message" >> "$LOG_FILE"
}
# 错误处理函数
handle_error() {
local message=$1
local exit_code=${2:-1}
log_message "ERROR" "$message" "${BASH_LINENO[0]}"
exit "$exit_code"
}
# 检查脚本运行环境
check_environment() {
command -v wget >/dev/null 2>&1 || handle_error "wget 命令未安装,请安装后重试。" 2
command -v openssl >/dev/null 2>&1 || handle_error "openssl 命令未安装,请安装后重试。" 2
command -v systemctl >/dev/null 2>&1 || handle_error "systemctl 命令未安装,请检查环境。" 2
}
# 下载证书文件函数
download_file() {
local file=$1
log_message "INFO" "开始下载文件 $file ..." "${BASH_LINENO[0]}"
wget -t 3 -P "$LOCAL_DIR" "ftp://$FTP_USER:$FTP_PASSWORD@$FTP_SERVER$FTP_DIR$file"
if [ $? -ne 0 ]; then
handle_error "文件 $file 下载失败,请检查网络连接或FTP服务器设置。" 3
fi
}
# 转换密钥格式
convert_key_format() {
log_message "INFO" "开始转换密钥格式..." "${BASH_LINENO[0]}"
openssl pkcs8 -topk8 -inform PEM -in "$LOCAL_DIR/$KEY_FILE" -outform PEM -nocrypt -out "$LOCAL_DIR/$PRIVKEY_FILE"
if [ ! -s "$LOCAL_DIR/$PRIVKEY_FILE" ]; then
handle_error "密钥格式转换失败,请检查 openssl 配置及相关文件。" 4
fi
}
# 复制文件到目标目录
copy_and_check() {
local source_file=$1
local target_dir=$2
log_message "INFO" "复制文件 $source_file 到目录 $target_dir ..." "${BASH_LINENO[0]}"
cp -f "$source_file" "$target_dir"
if [ $? -ne 0 ]; then
handle_error "文件 $source_file 复制到 $target_dir 失败,请检查权限或磁盘空间。" 5
fi
}
# 重启服务
restart_service() {
local service=$1
log_message "INFO" "重启服务 $service ..." "${BASH_LINENO[0]}"
systemctl reload "$service"
if [ $? -ne 0 ]; then
handle_error "$service 服务重载失败,请检查配置。" 6
fi
systemctl restart "$service"
if [ $? -ne 0 ]; then
handle_error "$service 服务重启失败,请检查配置。" 7
fi
}
# 清理临时文件
cleanup_files() {
local files=("$@")
log_message "INFO" "开始清理临时文件..." "${BASH_LINENO[0]}"
for file in "${files[@]}"; do
rm -f "$file"
done
log_message "INFO" "临时文件清理完成。" "${BASH_LINENO[0]}"
}
# 主程序
main() {
check_environment
# 下载文件
download_file "$CERT_FILE"
download_file "$KEY_FILE"
download_file "$FULLCHAIN_FILE"
# 转换密钥格式
convert_key_format
# 复制文件到目标目录
for cert_dir in "${CERT_DIRS[@]}"; do
copy_and_check "$LOCAL_DIR/$CERT_FILE" "$cert_dir"
copy_and_check "$LOCAL_DIR/$PRIVKEY_FILE" "$cert_dir"
copy_and_check "$LOCAL_DIR/$FULLCHAIN_FILE" "$cert_dir"
done
# 重启服务
restart_service "nginx"
# 清理临时文件
cleanup_files "$LOCAL_DIR/$CERT_FILE" "$LOCAL_DIR/$KEY_FILE" "$LOCAL_DIR/$PRIVKEY_FILE" "$LOCAL_DIR/$FULLCHAIN_FILE"
log_message "INFO" "脚本执行完成!" "${BASH_LINENO[0]}"
exit
}
# 执行主程序
main "$@"
编写好的脚本再授权:chmod +x ./my_script.sh,脚本内容主要内容就是从路由器的 FTP 读取 pem 文件然后下载群晖,如果不确定 FTP 是否能用可以用命令测试下:curl ftp://账号:密码@192.168.50.1/WDisk/iNas/cert.pem 如果有返回内容那就没问题的,群晖下载证书后,还需要将原先的 私钥 转换成 PKCS#8 的格式密钥,然后再将密钥分别复制到几个地方,除了群晖自带的 ssl 默认证书,还涉及到 AList 的 ssl 证书。如果路由器执行定时脚本就会关联执行到群晖的脚本,就不需要操心手动更新这么多证书了。
一些问题
梅林华硕路由器端口转发在重启的时候失效了
这个问题存在了很久,主要问题不明确,重启概率不多,有时候重启发现家里部分设备不能在网络上访问,查询好久才发现是端口转发失效了,明明在 webui 显示开启状态,实际上状态是不对,所以后面查询一些 API 操作命令:
/bin/sleep 10
# vts_enable_x 为端口转发 1=启动 0=禁用
nvram set vts_enable_x=1
# 提交变量
nvram commit
# 重启防火墙
service restart_firewall
根据 Asuswrt-Merlin 文档得知 在路由器重启放在 services-start 脚本最合适了,打开 /jffs/scripts/services-start 添加上面命令即可。
路由器更新 SSL 证书
新建脚本 install_cert.sh 如下:
#!/bin/sh
SRC_DIR="/jffs/.koolshare/acme/router.yourdomain.com"
DST_DIR="/jffs/.cert"
# 1. 确保目标目录存在
[ -d "$DST_DIR" ] || mkdir -p "$DST_DIR"
# 2. 复制并按 Merlin 要求重命名
cp -f "$SRC_DIR/fullchain.cer" "$DST_DIR/cert.pem"
cp -f "$SRC_DIR/router.yourdomain.com.key" "$DST_DIR/key.pem"
# 3. 权限收紧(httpd 会读取,但别给全世界777)
chmod 600 "$DST_DIR/cert.pem" "$DST_DIR/key.pem"
# 4. 通知 httpd 重新载入证书
service restart_httpd
刷新路由器 DNS 缓存
同样在 /jffs/scripts/services-star 脚本添加:
# 刷新 DNS 缓存
/bin/sleep 60
service restart_dnsmasq
Synology DDNS 获取公网 IP 不对
这个一般是用魔法导致的,走的路线不对,可以自己定义规则即可:
DOMAIN-SUFFIX,checkip.synology.com
DOMAIN-SUFFIX,checkipv6.synology.com
DOMAIN-SUFFIX,checkport.synology.com
DOMAIN-SUFFIX,ddns.synology.com
将上面规则走 DIRECT 就可以正确获取公网地址了。
修正群晖自带域名证书问题
群晖上面默认绑定自己的域名之外,我还额外启用群晖自带的域名访问:https://demo.synology.me,访问发现证书绑定的是自己域名证书而不是群晖自带的证书,研究下只需要修改 nginx 配置文件即可,在 /etc/nginx/sites-enabled/ 新建 server.ReverseProxyMy.conf 内容如下:
server {
listen 1998;
listen [::]:1998;
server_name demo.synology.me;
gzip on;
include conf.d/alias.*.conf;
root /usr/syno/synoman;
index index.cgi;
ignore_invalid_headers off;
include /usr/syno/share/nginx/conf.d/dsm.*.conf;
include conf.d/dsm.*.conf;
location = / {
try_files $uri /index.cgi$is_args$query_string;
}
location ~ ^/volume(?:X|USB|SATA|Gluster)?\d+/ {
internal;
root /;
open_file_cache off;
include conf.d/x-accel.*.conf;
}
location ~ /webman/modules/(PersonalSettings|ExternalDevices|FileBrowser)/index_ds.php$ {
alias /usr/syno/share/OAuth/index_ds.php;
default_type text/html;
}
location ~ \.cgi {
include scgi_params;
scgi_pass synoscgi;
scgi_read_timeout 3600s;
}
location ~ /synoscgi.sock/socket.io/ {
proxy_read_timeout 3600s;
include proxy.conf;
rewrite /synoscgi.sock/(.*)$ /$1 break;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://synoscgi.sock;
}
location ~ /api/ {
include scgi_params;
scgi_pass synoscgi_restfulapi;
scgi_read_timeout 3600s;
}
error_page 403 404 500 502 503 504 /dsm_error_page;
location /dsm_error_page {
internal;
root /usr/syno/share/nginx;
rewrite (.*) /error.html break;
allow all;
}
location ~ ^/webman/modules/Indexer/ {
deny all;
}
location ~ ^/webapi/lib/ {
deny all;
}
location ~ ^/webapi/(:?(:?.*)\.lib|(:?.*)\.api|(:?.*)\.auth|lib.def)$ {
deny all;
}
location ~ /\. {
access_log off;
log_not_found off;
deny all;
}
location ~* \.(?:js|css|png|jpg|gif|ico)$ {
access_log off;
log_not_found off;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
allow all;
access_log off;
log_not_found off;
}
}
server {
listen 1999 ssl;
listen [::]:1999 ssl;
server_name demo.synology.me;
location ^~ /.well-known/acme-challenge {
root /var/lib/letsencrypt;
default_type text/plain;
}
ssl_certificate /usr/syno/etc/certificate/_archive/MSyrAt/cert.pem;
ssl_certificate_key /usr/syno/etc/certificate/_archive/MSyrAt/privkey.pem;
include /usr/syno/etc/security-profile/tls-profile/config/system_quickconnect.conf*;
include conf.d/ssl.*.conf;
include conf.d/alias.*.conf;
root /usr/syno/synoman;
index index.cgi;
ignore_invalid_headers off;
include /usr/syno/share/nginx/conf.d/dsm.*.conf;
include conf.d/dsm.*.conf;
location = / {
try_files $uri /index.cgi$is_args$query_string;
}
location ~ ^/volume(?:X|USB|SATA|Gluster)?\d+/ {
internal;
root /;
open_file_cache off;
include conf.d/x-accel.*.conf;
}
location ~ /webman/modules/(PersonalSettings|ExternalDevices|FileBrowser)/index_ds.php$ {
alias /usr/syno/share/OAuth/index_ds.php;
default_type text/html;
}
location ~ \.cgi {
include scgi_params;
scgi_pass synoscgi;
scgi_read_timeout 3600s;
}
location ~ /synoscgi.sock/socket.io/ {
proxy_read_timeout 3600s;
include proxy.conf;
rewrite /synoscgi.sock/(.*)$ /$1 break;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://synoscgi.sock;
}
location ~ /api/ {
include scgi_params;
scgi_pass synoscgi_restfulapi;
scgi_read_timeout 3600s;
}
error_page 403 404 500 502 503 504 /dsm_error_page;
location /dsm_error_page {
internal;
root /usr/syno/share/nginx;
rewrite (.*) /error.html break;
allow all;
}
location ~ ^/webman/modules/Indexer/ {
deny all;
}
location ~ ^/webapi/lib/ {
deny all;
}
location ~ ^/webapi/(:?(:?.*)\.lib|(:?.*)\.api|(:?.*)\.auth|lib.def)$ {
deny all;
}
location ~ /\. {
access_log off;
log_not_found off;
deny all;
}
location ~* \.(?:js|css|png|jpg|gif|ico)$ {
access_log off;
log_not_found off;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
allow all;
access_log off;
log_not_found off;
}
}
只需要修改 ssl_certificate 和 ssl_certificate_key 地方,其他都是从 nginx.conf 复制而来,可以从那边复制过来一份也可以的,上面的证书地址需要自己搜索才能确定:
cd /usr/syno/etc/certificate/_archive/
grep -ri "demo.synology.me" /usr/syno/etc/certificate/_archive/
/usr/syno/etc/certificate/_archive/MSyrAt/renew.json: "domains" : "demo.synology.me;*.demo.synology.me",
输出确定下来就知道在 /usr/syno/etc/certificate/_archive/MSyrAt 目录下,再填入 nginx 配置文件里面即可。
# 重启 nginx 服务
systemctl reload nginx
结束
脚本为什么不用 curl 是因为群晖的 curl 不支付 ftp 协议:
curl -u "iNas:123456" ftp://192.168.50.1/WDisk/iNas/key.pem
curl: (1) Protocol "ftp" not supported or disabled in libcurl
所以没办法改用 wget 来下载证书。
另外值得注意是 控制面板 - 安全性 - 证书 要先上传自己域名的证书,作为初始化,方便后续脚本自动更新证书。