📌 前言
有在使用Synology NAS 的朋友,一定都會遇到 SSL 憑證管理的麻煩事。特別是當你同時擁有自訂頂級網域的 Wildcard 泛網域(如 *.yourdomain.com),又同時在使用Synology自帶的 DDNS (*.synology.me) 時,往往會因為驗證機制不同而需要維護多張憑證,如果又想使用Let's Encrypt的免費憑證,那麼每三個月就要手動更新一次。過去我是使用certbot指令,一次送出多個網域的憑證申請要求,如果是wildcard網域,就手工去DNS更新TXT類型的acme_challenge名稱,如果是單一網域,就直接在網站根目錄下 /.well-known/acme-challenge 手動新增acme挑戰的認證檔案,網域一多的話,真的非常麻煩。而且為了提升安全性,一直有聽說這些憑證管理單位未來將會強制要求憑證的更新時效縮短,如果一直還手工更新憑證,肯定是不可行的一件事,所以一直有想改成 acme.sh 自動更新的想法。
這篇文章將作為我自己的筆記:利用 acme.sh,在一張憑證上同時綁定 Wildcard 泛網域與多個單一子網域。並採用「混合驗證模式」(自訂wildcard網域走內建 DNS-01,Synology DDNS 走 HTTP-01 Webroot),最後透過 Python 發送 SMTP 憑證更新完成的通知信,實現完全極致的自動化!
acme.sh在DSM上的安裝就不在這篇文章介紹,請自行參照 https://github.com/acmesh-official/acme.sh 來進行安裝。
🚀 核心架構與設計邏輯
一般來說,申請憑證有兩種常見驗證方式:
- DNS-01 認證:適合用來申請 Wildcard 泛網域,不需要開放外網 Port 80,但需要透過 API 寫入 DNS TXT 紀錄。
- HTTP-01 認證:適合單一子網域,需要開放 Port 80。
Synology 自帶的 *.synology.me 當然無法取得可以更新 DNS 的 API 金鑰,但因為我們對外有開放 80/443。因此最好的方式就是:讓自訂的Wildcard網域走 DNS-01,Synology網域走 HTTP-01,然後全部打包在同一個憑證指令中完成更新跟部署!
🛠️ 實作步驟
步驟一:Synology DNS Server 建立金鑰 (TSIG Key)
如果你的自網域是代管在Synology內建的 DNS Server 套件 中,我們可以用較安全的 RFC2136 (nsupdate) 協議進行通訊。
- 開啟 Synology DNS Server 套件 > 區域 > 選取你的自訂網域(像我的就是 cychien.tw) > 編輯 > 區域設定。
- 切換到 安全性 頁籤,勾選 啟動 TSIG (傳輸簽章) 並產生金鑰(自訂一個金鑰名稱,如
acme-key,稍後會用到),演算法選擇HMAC-MD5。 - 金鑰產生完畢後,選擇「匯出金鑰」,將金鑰另存為檔案,然後用文字編輯器打開複製產生的 Base64 密鑰字串。
⚠️ 巨坑注意:為了提高安全性,Synology已預先在網域設定中預設都限定轄區更新規則,所以透過TSIG Key更新Synology DNS Server時,還要去設定轄區更新規則,否則會遇到 UPDATE STATUS: REFUSED 的錯誤。
DNS Server –> 轄區管理 –> cychien.tw(你自己的網域) –> 編輯 –> 轄區設定 –> 限定轄區更新。
然後把前面新建的acme-key以及本機IP 127.0.0.1 (因為後面腳本指定DNSServer為127.0.0.1) 加入更新規則中。
步驟二:部署完整的自動化指令
我們將邏輯拆分為兩個檔案:一個負責與 acme.sh 溝通及部署至 DSM 的 Shell Script,另一個負責動態發送通知信的 Python 腳本。
首先新增一個獨立運作的帳號cert_mgr,需具備administrators群組,不過為了保險起見,我是將這個帳號上所有的應用程式權限全部刪除,只保留可以操作 homes 以及 /var/lib/letsencrypt目錄權限即可。
1. Shell 核心控制碼:renew_certs.sh
這個腳本使用了 DOMAINS 陣列,用來設定所有要同時放在這張憑證中的網域。在執行 --issue 時,不同的網域後面可以緊跟著它專屬的驗證方式(--dns 或 --webroot),這就是 acme.sh 強大的混合驗證模式。
#!/bin/bash
# ==========================================
# 1. 基礎路徑與帳戶設定
# ==========================================
# 請確保此路徑與你安裝 acme.sh 的位置一致
ACME_HOME="/var/services/homes/cert_mgr/acme"
ACME_BIN="${ACME_HOME}/acme.sh"
ACCOUNT_EMAIL="your_email@example.com" # 接收 Let's Encrypt 憑證過期通知的 Email
FORCE_ISSUE_CERT="" # 如果要強制更新,可填入 "--force"
# 統一在此管理網域清單:第一個必須是主網域(Common Name),後續為憑證主體替代名稱(SAN)
DOMAINS=(
"cychien.tw"
"*.cychien.tw"
"myhost.synology.me"
"server1.myhost.synology.me"
"server2.myhost.synology.me"
)
# ==========================================
# 2. DNS-01 (Synology內建 DNS Server) 設定
# ==========================================
export SAVED_NSUPDATE_SERVER="127.0.0.1"
export SAVED_NSUPDATE_PORT="53"
# 金鑰演算法,需與Synology內建 DNS Server 一致
export SAVED_NSUPDATE_ALGO="HMAC-MD5"
# 與前一步驟建立的 TSIG Key 名稱需保持一致
export SAVED_NSUPDATE_KEY="acme-key"
# 填入Synology產生的 Base64 金鑰字串
export SAVED_NSUPDATE_SECRET="YOUR_BASE64_SECRET_KEY=="
# ==========================================
# 3. Synology DSM 憑證部署設定
# ==========================================
export SYNO_Username="你的DSM管理員帳號"
export SYNO_Password="你的DSM管理員密碼"
# 指定更新 DSM 中對應此名稱的既有憑證,也就是 cychien.tw
export SYNO_Certificate="${DOMAINS[0]}"
export SYNO_Create=1
# 驗證網站根目錄路徑:DSM 7.x Web Station 預設為 /var/lib/letsencrypt/
WEB_ROOT="/var/lib/letsencrypt/"
# ==========================================
# 4. 執行憑證申請 (混合認證模式)
# ==========================================
echo "====== 開始申請/更新多網域混合驗證憑證 ======"
# 透過陣列索引,精準分派驗證方式
# cychien.tw 與 *.cychien.tw 走Synology內建 DNS-01 驗證
# 其他 synology.me 子網域走本機 Webroot (HTTP-01) 驗證
"$ACME_BIN" $FORCE_ISSUE_CERT --issue --server letsencrypt --home "$ACME_HOME" \
-d "${DOMAINS[0]}" --dns dns_nsupdate \
-d "${DOMAINS[1]}" --dns dns_nsupdate \
-d "${DOMAINS[2]}" --webroot "$WEB_ROOT" \
-d "${DOMAINS[3]}" --webroot "$WEB_ROOT" \
-d "${DOMAINS[4]}" --webroot "$WEB_ROOT"
# 檢查上一個指令是否執行成功 (離開狀態碼 0 代表成功或不需更新)
if [ $? -ne 0 ]; then
echo "❌ 憑證申請或驗證失敗,請檢查 Log 輸出。"
exit 1
fi
# ==========================================
# 5. 執行憑證部署 (推送到 DSM 系統)
# ==========================================
echo "====== 開始將憑證部署至 Synology DSM ======"
"$ACME_BIN" --deploy --home "$ACME_HOME" \
-d "${DOMAINS[0]}" \
--deploy-hook synology_dsm
if [ $? -eq 0 ]; then
echo "🎉 憑證申請與 DSM 部署成功!"
# 呼叫 Python 發信腳本,同時把整個 DOMAINS 陣列當作參數傳進去
if [ -f "${ACME_HOME}/send_mail.py" ]; then
echo "正在呼叫 send_mail.py 發送動態通知信..."
python3 "${ACME_HOME}/send_mail.py" "${DOMAINS[@]}"
else
echo "⚠️ 找不到發信腳本: ${ACME_HOME}/send_mail.py"
fi
else
echo "❌ 憑證部署至 DSM 失敗。"
exit 1
fi
2. Python 動態發信:send_mail.py
為了不把網域在發信軟體裡寫死,這個 Python 腳本會直接解析從 Shell 傳進來的參數(sys.argv[1:]),自動組裝為憑證更新成功通知的內容,並透過未加密的SMTP協定寄出。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import smtplib
import socket
import sys
from email.mime.text import MIMEText
from email.header import Header
# ============================================
# 1. 寄件與收件 SMTP 參數設定 (適用內部自建明文 Port 25)
# ============================================
SMTP_SERVER = "127.0.0.1" # 你的 Mail Server 位址
SMTP_PORT = 25 # 明文標準 SMTP 連接埠
SMTP_USER = "nas-notify@cychien.tw" # 寄件人顯示的信箱
TO_EMAIL = "your_receive_email@example.com" # 接收通知的特定 Email
# ============================================
# 2. 動態解析從 Shell Script 傳進來的網域參數
# ============================================
# sys.argv[0] 為腳本本身路徑,sys.argv[1:] 則是從 Shell 傳遞過來的所有網域參數
if len(sys.argv) > 1:
domains = sys.argv[1:]
main_domain = domains[0] # 第一個網域為主網域
# 將所有網域組裝成內文清單字串
domain_list_str = "\n".join([f" - {d}" for d in domains])
else:
main_domain = "未知網域"
domain_list_str = " - 未提供網域清單"
# ============================================
# 3. 自動取得本機主機名稱與組裝內文
# ============================================
try:
hostname = socket.gethostname()
except Exception:
hostname = "Synology_NAS"
mail_msg = f"""您好:
Synology NAS ({hostname}) 上的 SSL 憑證已透過 acme.sh 順利自動更新並部署完成。
■ 主網域:{main_domain}
■ 憑證包含的完整網域清單:
{domain_list_str}
本郵件由 NAS 系統排程自動發送,憑證已生效,無須手動處理。"""
# ============================================
# 4. 郵件標頭與編碼設定
# ============================================
message = MIMEText(mail_msg, 'plain', 'utf-8')
message['From'] = Header(f"Synology NAS <{SMTP_USER}>", 'utf-8')
message['To'] = Header(TO_EMAIL, 'utf-8')
message['Subject'] = Header(f"[NAS 任務通知] SSL 憑證自動更新成功 ({main_domain})", 'utf-8')
# ============================================
# 5. 執行明文 SMTP 發信
# ============================================
try:
smtpObj = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
smtpObj.sendmail(SMTP_USER, [TO_EMAIL], message.as_string())
smtpObj.quit()
print("🎉 [Python] 郵件動態發送成功")
except Exception as e:
print(f"❌ [Python] 郵件動態發送失敗: {e}")
📅 完美的終點:掛載至 DSM 任務排程表
當我們把這兩支腳本賦予執行權限 (chmod +x) 之後
1.進入 DSM 控制台 > 安全性 > 憑證,將利用本指令產生的這張憑證手動「設為預設憑證」。
2.進入 DSM 控制台 > 任務排程表 > 新增 > 排程任務 > 使用者定義指令碼,任務名稱自己定義,使用者選 cert_mgr。
3.排程設定可以依據憑證的效期來決定,例如目前Let's Encrypt的憑證三個月到期一次,可以設定為每個月某日執行一次,指令直接填寫:
/var/services/homes/cert_mgr/acme/renew_certs.sh > /dev/null
acme.sh會自動檢查是否已經超過可更新憑證的日期,如果超過可更新日,就開始進行更新,否則就直接結束不更新憑證(因為有效期限還足夠)。
因為腳本中已經指定了 SYNO_Certificate="cychien.tw",未來每 60 天 acme.sh 自動續期時,它就會直接「覆寫」這張在 DSM 中已經被設定為預設憑證的內容。內容更新後,Synology底層的所有關聯服務就會自動無縫切換,完全不需要手動進後台點選!
更新完成後,信箱還會自動收到憑證的更新成功通知,讓你知道你的憑證更新已完成,至此 Let's Encrypt 憑證自動會更新就大功告成。