在同一台實體主機上執行 API 服務、同步作業與監控收集器時,服務權杖常會散落在啟動指令碼、環境設定檔與部署紀錄中。檢查程序參數、記錄故障日誌,甚至只是封存一次設定,都可能擴大敏感資料的暴露範圍。本文在 Debian 12 上使用 systemd 的 LoadCredential,將金鑰來源檔保留在管理員控管的目錄中,只向指定服務提供執行階段副本,並透過可重複執行的檢查驗證輪替流程。
一、先釐清憑證隔離的邊界
這裡處理的是「服務如何取得秘密值」,而非金鑰核發、集中保管或遠端驗證協定。CloudDDR 實體主機的系統與資料由使用者自行管理;付款後可在控制台查看連線憑證,再透過 SSH 登入系統。以下操作需要管理員權限,請先在測試服務上完成,不要直接變更正式服務的啟動方式。
環境變數不適合作為秘密值的預設傳遞方式:內容可能出現在除錯輸出、子程序環境或診斷封存檔中。啟動參數也可能被程序檢查工具記錄。LoadCredential 則由系統服務管理器讀取來源檔,在服務啟動時提供憑證目錄,讓程式透過 CREDENTIALS_DIRECTORY 找到檔案。
憑證檔案隔離無法抵禦已取得管理員權限的程序,也無法阻止應用程式將秘密值寫入日誌。保護邊界必須同時涵蓋檔案權限、服務帳號與應用程式的輸出行為。
本例的來源檔仍是明文,不會因為使用 LoadCredential 就自動獲得靜態加密保護。整機備份、管理員存取與金鑰核發紀錄,也應依敏感資料的標準處理。
二、建立專用帳號與受限的來源檔
準備獨立的檢查環境
先確認 systemctl --version;本例使用 Debian 12 提供的 systemd,並安裝 Python 3。請確認帳號名稱未被既有服務使用;下列建立帳號的命令只應在帳號尚不存在時執行。
sudo apt-get update
sudo apt-get install python3
id credential-check
sudo useradd --system --no-create-home --shell /usr/sbin/nologin credential-check
sudo install -d -o root -g root -m 0700 /etc/service-secrets
sudo sh -c 'umask 077; printf "%s" "fixture-token-alpha" > /etc/service-secrets/api-token'
sudo stat -c '%U %G %a %n' /etc/service-secrets /etc/service-secrets/api-token
只有在 id 回報帳號不存在後,才執行 useradd;若帳號已存在,應先確認用途,不要直接沿用用途不明的帳號。預期目錄權限為 700、檔案權限為 600,擁有者與所屬群組皆為 root。範例字串是不具任何權限的測試資料,可以留在命令歷史中;真實權杖不得以這種方式直接寫進命令。
真實值應透過受控的檔案交接取得,再以管理員權限安裝到目標位置。不要透過編輯器暫存檔、終端機回顯或已啟用追蹤的 Shell 傳遞秘密值。服務帳號應無法讀取來源檔:
sudo -u credential-check test -r /etc/service-secrets/api-token
printf '%s\n' "$?"
此處預期結束碼為 1。若為 0,請先檢查目錄權限、檔案 ACL 與帳號的群組成員資格,不要繼續部署。
三、讓程式只讀取執行階段憑證
將以下程式儲存為 /usr/local/libexec/credential-check.py,建立其父目錄,並將程式的擁有者設為 root、權限設為 755。目錄與程式都不得允許服務帳號寫入。程式只回報固定的錯誤訊息,不輸出權杖、長度或摘要,避免為了驗收反而新增洩漏管道。
import os
from pathlib import Path
directory = os.environ.get("CREDENTIALS_DIRECTORY")
if not directory:
raise SystemExit("Credential directory missing")
try:
token = (Path(directory) / "api-token").read_bytes()
except OSError:
raise SystemExit("Credential unavailable") from None
if not token or b"\n" in token or b"\r" in token:
raise SystemExit("Credential format rejected")
本例要求權杖非空且不含換行字元;這並非適用於所有金鑰格式的通用規則。實際程式應依上游協定驗證格式,例如允許多行 PEM 內容時,就不能沿用這項檢查。
建立 /etc/systemd/system/credential-check.service:
[Unit]
Description=Service credential validation
[Service]
Type=oneshot
User=credential-check
Group=credential-check
LoadCredential=api-token:/etc/service-secrets/api-token
ExecStart=/usr/bin/python3 /usr/local/libexec/credential-check.py
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
UMask=0077
這是一次性檢查單元,成功執行後處於 inactive 狀態是正常結果。不必設定開機啟用,也不應為了測試而套用正式服務的重新啟動策略。實際應用程式可用相同方式讀取檔案,但檔案系統限制必須配合服務需要寫入的目錄進行驗證,不能直接照搬。
四、分別驗收可讀性與驗證結果
sudo systemctl daemon-reload
sudo systemctl start credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
sudo journalctl -u credential-check.service -n 20 --no-pager
預期結果為 Result=success、ExecMainStatus=0。這表示服務帳號能透過執行階段目錄讀取憑證,而前一節已確認它無法直接讀取來源檔。不要用 cat、完整環境變數清單或程序記憶體傾印來證明這一點。
這項檢查不代表權杖有效。接入實際服務後,還需執行一項不會變更業務資料的驗證請求,核對回應狀態與預期權限。驗證失敗時,應分別檢查憑證格式、上游是否已啟用新權杖、應用程式是否確實重新啟動,以及請求是否仍使用舊連線或快取。
驗收紀錄只保留單元版本、操作順序、結束碼與驗證結論,不儲存權杖值。若必須將紀錄對應到某次輪替,可使用內部變更編號,而非權杖摘要。檢查日誌時,也要確認異常處理路徑沒有輸出請求標頭或完整設定。
五、以原子替換完成輪替與失敗復原
更新來源檔不會改變執行中服務的憑證副本。輪替順序應為:取得新值、驗證格式、原子替換、重新啟動服務、確認驗證成功,最後依上游規則撤銷舊值。只執行 daemon-reload 不會更新執行中的秘密值。
以下命令只用於輪替測試資料;暫存檔與目標檔位於同一目錄,確保重新命名作業在同一檔案系統內完成。
sudo sh -c 'umask 077; printf "%s" "fixture-token-beta" > /etc/service-secrets/api-token.next'
sudo chown root:root /etc/service-secrets/api-token.next
sudo chmod 0600 /etc/service-secrets/api-token.next
sudo mv -f /etc/service-secrets/api-token.next /etc/service-secrets/api-token
sudo systemctl restart credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
接著進行失敗演練:將測試來源檔替換為空檔案,再啟動檢查單元,預期會得到非零結束碼與固定的格式錯誤訊息;還原非空的測試值後重新啟動,預期應成功。這能確認應用程式不會在秘密值缺失時悄悄繼續執行。不要對正式權杖進行破壞性演練。
實際輪替前,應先確認上游能否同時接受新舊權杖。若不能,必須規劃協調切換;若舊值已撤銷,即使還原舊檔案,也無法恢復驗證。舊值只應在受控的回退期限內保留,並採取同等的權限保護,不得長期複製存放在業務目錄中。
完成驗收後,停用並移除測試單元、指令碼與測試憑證,執行 daemon-reload,並確認沒有遺留暫存檔。最終交付成果應是一套可複核的流程:一般帳號無法讀取來源檔,目標服務能讀取執行階段憑證,錯誤值會導致明確失敗,輪替後的實際驗證成功,且日誌中沒有敏感值。
常見問題
LoadCredential 能避免管理員讀取服務金鑰嗎?
不能。系統管理員與取得足夠權限的程序仍可能讀取來源檔案或服務記憶體。它減少金鑰在啟動參數、環境變數與一般帳號之間的暴露,並非針對管理員的保密邊界。
替換憑證來源檔案後,執行中的服務會自動取得新金鑰嗎?
不會。LoadCredential 在服務啟動時載入憑證。本文採用原子替換來源檔案後重新啟動服務,再驗證新金鑰的認證結果;只執行 daemon-reload 不會更新執行中的憑證。