目前有幾個服務,是透過 docker 設置,使用反向代理網站的型態存在。最近,發現這些網站的服務都停止了,查了一下,原來是網站憑證過期,而我設置 certbot 向 Let’s Encrtpt 申請憑證自動延期的功能失效。
查了一下原因,原來是當初這些網站服務,當初憑證在申請時使用了 –manual(手動驗證,例如手動新增 DNS TXT 紀錄或放置驗證檔案)。
而在執行 certbot renew 時,Certbot 預設是以非互動式(非手動操作)的方式背景執行。又由於使用手動驗證又沒有設定自動化驗證腳本(–manual-auth-hook),所以系統無法自動完成驗證,因此停止自動延期。
要解決這個問題,目前一般來說有三個:
- 解法一:以互動模式手動更新(最快立即解決)
- 解法二:改用 Web 伺服器外掛自動化(若該主機有跑 Nginx / Apache)
- 解法三:改用 DNS API 外掛(推薦,完全無感自動續期)
這是在 DNS 伺服器不支援 API,或者你想先立刻將過期憑證展延,可以直接加上 –manual 參數進行互動式更新:
sudo certbot certonly --manual -d sample.yowlab.idv.tw --preferred-challenges http
(若當初是用 DNS 驗證,可改為 –preferred-challenges dns)
再按照終端機上的提示,手動新增 DNS TXT 紀錄或驗證檔案後按下 Enter,即可完成本次更新。
此方法,無法達成我需要自動化延期的需求。
因這台機器本身就有架設反向代理(如 Apache)導向 sample 主機,所以將驗證方式改成自動化外掛,未來 certbot renew 就能自動展延:
若 Nginx:
sudo certbot certonly --nginx -d sample.yowlab.idv.tw
獨立模式(Standalone)(更新時,要短暫停用佔用 80 port 的服務):
sudo certbot certonly --standalone -d sample.yowlab.idv.tw
成功申請後,Certbot 會自動覆蓋 /etc/letsencrypt/renewal/sample.yowlab.idv.tw.conf 的驗證方式,之後排程執行 certbot renew 就不會再報錯。
若網域是透過 Cloudflare 等支援 API 的託管商管理,可安裝對應的 DNS 外掛,例如 Cloudflare DNS 外掛。
此方法,目前我不是很依賴 Cloudflare, 所以不考慮採用。
實驗試做了一下,決定採用 獨立模式(Standalone)來解決:
sudo certbot certonly --standalone -d sample.yowlab.idv.tw
又考慮到要讓系統自動執行,且更新時,要短暫停用佔用 80 port 的服務,所以更新前先暫停 apache2 服務,更新後要重啟 apache2 服務。
certbot certonly \
--standalone \
--non-interactive \
--agree-tos \
--pre-hook "systemctl stop apache2" \
--post-hook "systemctl start apache2" \
--cert-name sample.yowlab.idv.tw \
-d sample.yowlab.idv.tw
參數說明:
- –non-interactive 確保無人值守自動執行。
- –agree-tos 代表 「Agree to the Terms of Service(同意服務條款)」。
- –prep-hook, –post-hook 參數,分別表示憑證更新前後,要做的事情。
- –cert-name 明確指定憑證名稱,以及憑證相關目錄名稱。
- –quiet:只有出錯時才會輸出訊息,平常無感安靜執行。
當 Certbot 遇到需要替換或擴充時會直接按標準規範更新,不會卡在終端機等待輸入 (E)/(C) 或 (U)/(R)。
當使用 Let’s Encrypt(ACME 協議)註冊帳號或申請憑證時,官方規定必須同意其 ACME 憑證認證機構的使用者服務條款(Subscriber Agreement)。這時會停下,等待使用者輸入同意或拒絕。
這個參數,便是直接向 Certbot 表明「我已同意該服務條款」,不再跳出詢問提示。
憑證更新時,要先暫停 apache2 服務,憑證更新後,要再度啟動 apache2 服務。
再來,因為我有多個服務,所以有多張憑證需要申請延期。因此,用程式來跑:
#!/usr/bin/env bash
set -uo pipefail
# 1. 固定主網域
BASE_DOMAIN="yowlab.idv.tw"
# 2. 變動子網域名單(以後增加直接在括號內補上,用空白或換行隔開)
SUBDOMAINS=(
"sample01"
"sample02"
"sample03"
"sample04"
)
# 3. 迴圈依序執行獨立簽發
for sub in "${SUBDOMAINS[@]}"; do
fqdn="${sub}.${BASE_DOMAIN}"
echo "=============================================="
echo "開始處理憑證: ${fqdn}"
echo "=============================================="
certbot certonly \
--standalone \
--non-interactive \
--agree-tos \
--pre-hook "systemctl stop apache2" \
--post-hook "systemctl start apache2" \
--cert-name "${fqdn}" \
-d "${fqdn}"
echo -e "\n"
done
echo "所有網域處理完成!"
最後再參考「讓 Certbot 可以每90天自動更新 Let’s Encrypt 憑證」這篇文,讓自動更新動起來。
特別說明:
#!/usr/bin/env bash set -uo pipefail
- #!/usr/bin/env bash 是目前的推薦寫法
- set -uo pipefail ,嚴格模式設定
- -u(Nounset:抓出未宣告變數)
- -o pipefail(Pipe 管道錯誤捕捉)
/usr/bin/env 是一個標準工具,它的作用是根據當前使用者的 PATH 環境變數動態尋找 bash 的實際安裝路徑。
這樣,不管 bash 是安裝在 /bin、/usr/bin 還是 /usr/local/bin,都能正確找到並啟動,具有更好的跨平台相容性。
預設情況下,Bash 遇到錯誤時的行為非常寬鬆(常常出錯了還繼續往下硬跑),這很容易導致災難。這行指令由兩個關鍵設定組合而成:
預設行為:如果你打錯變數名稱,或讀取一個根本沒被定義的變數,Bash 會把它當作「空字串」,靜悄悄地直接執行。
危險範例:假設腳本寫 TARGET_DIR=”/tmp/backup”,但刪除時打錯字寫成 rm -rf “${TAGET_DIR}/*”。因為 TAGET_DIR 沒定義,Bash 會把它看作空字串,結果指令變成 rm -rf “/*”,整台系統直接被清空。
加上 -u 後:只要程式讀取到任何未宣告或未給值的變數,Bash 會立刻中斷執行並印出錯誤訊息(unbound variable),防止災情發生。
預設行為:當指令使用 Pipe 管道 | 串接多個程序時,Bash 預設只看最後一個指令的結束狀態(Exit Code)。
範例:grep “error” non_exist_file.txt | wc -l
前半段 grep 因為找不到檔案早就出錯了,但後半段的 wc -l 算出來是 0(正常執行完畢),整行指令依然會回傳「成功(Exit Code 0)」,導致錯誤被掩蓋。
加上 -o pipefail 後:在整個 pipeline 串接中,只要有任何一個指令出錯失敗,整條管線的回傳值就會被判定為失敗,讓錯誤無所遁形。
小結
- #!/usr/bin/env bash:確保在任何 Unix/Linux 環境下都能精準抓到 bash 執行。
- set -uo pipefail:開啟防呆機制,禁止使用未定義的變數,且確保管線出錯時不會被忽略。