SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-09-13

自架密鑰管理器比較:OpenBao, Infisical 與檔案加密方案

單機伺服器該選 OpenBao、Infisical 還是簡單的 env 檔案?本文分析各方案的維護成本與安全性,並解釋為何環境變數洩漏風險會讓複雜的密鑰管理器反而不如權限 600 的檔案安全。

自架密鑰管理器與密碼管理器的差異

自架密鑰管理器負責將憑證交付給處理程序,而密碼管理器則是交付給使用者。其餘所有差異皆源於此。密碼管理器由在場且保持專注的人類解鎖;密鑰管理器則必須在凌晨 03:00 無人值守時,將資料庫密碼提供給應用程式。

兩者的故障模式有顯著差異。密碼管理器被鎖定僅造成不便,只需重新輸入主密碼即可;密鑰管理器若處於密封狀態則會導致服務中斷,任何在此期間重啟的服務都會因無法取得憑證而無法運作。以 Vaultwarden 作為個人密碼管理器 能妥善解決人類使用者的需求,但它並非為解決機器自動化需求而設計。若您同時部署此類服務,應加強保護的重點是管理權杖(admin token)與備份檔,而非保險庫內容(客戶端已對其加密)。Vaultwarden 加固指南 涵蓋了上述兩者的防護措施。

單機伺服器的實務選擇可分為兩類。OpenBao 與 Infisical 屬於服務型:包含 API、資料庫、TLS(傳輸層安全性協定)、登入步驟,以及您必須持續維護的背景處理程序。SOPS 搭配 age、systemd credentials 與 Docker secrets 則屬於檔案型:資料在靜態儲存時加密,由已執行的處理程序解密,無需額外監控。

以下是直接的建議:對於僅有一至兩位使用者的單機環境,檔案型方案通常是正確選擇。若部署了 OpenBao 卻無法正確解鎖或執行輪替,其安全性反而不如權限設為 600 的 env 檔案。因為前者增加了維護成本與備份風險,卻未提供任何您原本無法手動完成的輪替功能。

使用 600 權限的 env 檔案是否足夠?

通常足夠。此權限可防範伺服器上的其他使用者讀取您的資料庫密碼。Unix 檔案權限能達成此目的,且在網路啟動前即已生效。

sudo install -d -m 750 -o root -g myapp /etc/myapp
sudo install -m 640 -o root -g myapp /dev/null /etc/myapp/env
sudoedit /etc/myapp/env

請從兩方面進行驗證:

sudo -u myapp cat /etc/myapp/env
sudo -u nobody cat /etc/myapp/env

第一行指令會印出檔案內容。第二行指令會印出 cat: /etc/myapp/env: Permission denied,因為 nobody 不屬於 myapp 群組,且該檔案未設定任何全域(world)存取位元。這就是完整的安全模型,且確實有效。

洩漏發生在後續步驟。若 systemd 單元設定了 EnvironmentFile=,這些數值會被複製到處理程序的環境變數中,而處理程序的環境變數是可讀的。

[Service]
User=myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myapp
sudo cat /proc/$(pgrep -n myapp)/environ | tr '\0' '\n'

上述指令會以明文印出您的機密資訊,因為 /proc/<pid>/environ 可由 root 以及執行該處理程序的使用者讀取。若崩潰報告程式(crash reporter)將環境變數附加至報告中,也會發生同樣的情況。任何以相同帳號執行的工具亦然,這正是為何 將機密資訊移出 AI 代理 的第一步,就是先將其從環境變數中移除。請將此檔案與 專用的低權限服務使用者 搭配使用,確保「執行該處理程序的使用者」並非 root

使用 age 搭配 SOPS:將加密後的機密資訊提交至 git

SOPS (secrets operations) 會加密 YAML 或 JSON 檔案中的值,並保留明文的鍵。age 是一款輕量級加密工具,提供一組金鑰對且無需金鑰伺服器。兩者結合後,您可以將 secrets.enc.yaml 與程式碼一同提交,且 git diff 仍能顯示變更了哪些設定,而不會洩漏變更後的內容。

Ubuntu 24.04 已內建 age。由於 SOPS 未內建,請從發布頁面取得 .deb。截至 2026 年 8 月,最新版本為 3.13.3。

sudo apt update && sudo apt install -y age
curl -LO https://github.com/getsops/sops/releases/download/v3.13.3/sops_3.13.3_amd64.deb
sudo apt install -y ./sops_3.13.3_amd64.deb
sops --version

產生一組金鑰對。age-keygen 會將私鑰寫入檔案並輸出公鑰,您會看到一行以 Public key: age1... 開頭的內容。

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt

將公鑰放入儲存庫根目錄的 .sops.yaml 中,這樣您就不必在指令列中記憶接收者。

creation_rules:
  - age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c
sops encrypt secrets.yaml > secrets.enc.yaml
sops decrypt secrets.enc.yaml

沒有 path_regex 的規則會匹配所有檔案,這正是您初期需要的設定。若日後要新增規則,請確保其匹配您傳遞給 sops 的檔案,因為規則是根據輸入路徑進行檢查,而非根據您重新導向輸出的檔案。

執行時,請將值僅傳遞給單一處理程序:

sops exec-env secrets.enc.yaml './myapp'

sops exec-env 會在記憶體中解密並將值設定於子處理程序的環境變數中,因此不會將明文寫入磁碟。上一節提到的環境變數注意事項同樣適用於該子處理程序。

此處有兩個常見陷阱。在 systemd 下出現 Failed to get the data key required to decrypt the SOPS file 錯誤,通常是因為 SOPS 找錯了家目錄,因為服務單元不會繼承您的 HOME。請在服務單元中透過 Environment=SOPS_AGE_KEY_FILE=/etc/sops/age.txt 明確指定路徑。此外,編輯 .sops.yaml 不會重新加密現有的內容:新增同事的公鑰僅對新檔案生效,因此請對每個現有檔案執行 sops updatekeys secrets.enc.yaml。如果您的設定已透過 Ansible 執行,使用 Ansible Vault 加密相同的值 也能達到相同目的,且無需額外工具。

systemd credentials:不會進入環境變數的機密資訊

Ubuntu 24.04 內建 systemd 255,因此無需額外安裝。systemd-creds 可在主機上加密機密資訊,並由 systemd 將其解密至僅該服務可讀取的私有目錄中。

sudo systemd-creds setup
sudo install -d -m 700 /etc/myapp
echo -n 'hunter2' | sudo systemd-creds encrypt --name=db_password - /etc/myapp/db_password.cred
[Service]
User=myapp
LoadCredentialEncrypted=db_password:/etc/myapp/db_password.cred
ExecStart=/usr/local/bin/myapp

服務會從 $CREDENTIALS_DIRECTORY 所指定目錄內的 db_password 檔案讀取數值。由於該數值不會出現在環境變數中,因此 /proc/<pid>/environ 無法顯示任何有用的資訊,且明文亦不會寫入根檔案系統。

在將單元(unit)指向該檔案前,請先驗證檔案是否能成功解密:

sudo systemd-creds decrypt /etc/myapp/db_password.cred -

請務必確認加密所使用的金鑰,因為這決定了您的備份是否有效。預設的 --with-key=auto 會在 TPM2(Trusted Platform Module version 2)晶片存在且可用時優先使用,否則將使用主機金鑰。大多數 VPS 實例並未配備 TPM2。

systemd-analyze has-tpm2

no 表示系統使用了主機金鑰,該金鑰位於 /var/lib/systemd/credential.secret,且僅 root 可讀取。若將 db_password.cred 還原至沒有該檔案的新 VPS 上,則任何資料都無法解密。請務必將 credential.secret 一併納入備份,或將明文儲存於您可存取的其他位置。

Docker secrets:位於 /run/secrets 下的檔案

Compose 會讀取主機上的檔案,並將其掛載至容器內的 /run/secrets/<name>

services:
  app:
    image: myapp:latest
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
secrets:
  db_password:
    file: ./db_password.txt

cat /run/secrets/db_password

docker compose exec app cat /run/secrets/db_password
docker compose exec app env | grep -i password

env | grep DB_PASSWORD_FILE=/run/secrets/db_password

第一個指令會印出 secret。第二個指令僅會印出 DB_PASSWORD_FILE=/run/secrets/db_password,這正是重點所在:該值從未出現在容器的環境變數中,因此不會顯示於 docker inspect 的輸出結果。許多官方映像檔已預期此種結構,例如 Postgres 映像檔即是以此方式讀取 POSTGRES_PASSWORD_FILE

請務必釐清此機制的本質。在 Swarm 模式之外,任何層級皆無加密:./db_password.txt 僅為主機上的純文字檔案,其唯一的保護機制在於檔案權限模式與擁有者。請務必自行設定這兩者,因為 Compose 不會檢查並會直接掛載權限為全域可讀的檔案。關於此機制與簡易 env_file 捷徑之間的權衡取捨,請參閱 Compose 環境變數檔案與 secrets 指南

執行 OpenBao 與 Vault 的實際成本

OpenBao 是 Linux Foundation 對 HashiCorp Vault 進行的分支版本,源於 HashiCorp 在 2023 年將 Vault 改為 Business Source License。OpenBao 則維持在 MPL 2.0 (Mozilla Public License) 授權下。截至 2026 年 8 月,版本 2.6.2 為最新版。由於該分支保留了相同的指令介面,以下內容幾乎完全適用於 Vault。

docker pull docker.io/openbao/openbao

若您偏好使用 apt 管理升級,OpenBao 下載頁面提供 Debian 與 Ubuntu 套件。伺服器需要一個包含 listener 與 storage backend 的設定檔:

listener "tcp" {
  address       = "127.0.0.1:8200"
  tls_cert_file = "/path/to/full-chain.pem"
  tls_key_file  = "/path/to/private-key.pem"
}

storage "raft" {
  path = "/path/to/raft/data"
  node_id = "raft_node_1"
}

接著啟動一次:

bao operator init

預設情況下,系統會將 root key 分割為 5 個份額,並需要其中 3 個才能解除密封(unseal),這對應到 -key-shares-key-threshold 旗標。系統僅會顯示這些份額與初始 root token 一次,之後便不再顯示。

現在談談大多數比較文章會略過的重點。重啟後的伺服器即為密封狀態。 OpenBao 僅將 root key 保存在記憶體中,因此重啟後,除非有人提供足夠數量的份額,否則它無法解密自身的儲存空間。核心更新或記憶體不足導致的行程終止(OOM kill),最終都會導致伺服器處於密封狀態,進而使應用程式無法登入。

在單人使用的 VPS 上,Shamir 分割機制並無實質保護作用,因為所有 5 個份額最終都會存放在同一個人的密碼管理器中。自動解除密封(Auto unseal)會將金鑰移至受信任的裝置或服務;在大型雲端環境中,這意味著使用託管金鑰服務,但在您的 VPS 上,通常意味著將金鑰檔案存放在與受保護資料相同的磁碟上。這確實降低了安全性,但換取了伺服器在重啟後能自動恢復運作的便利。請務必在知情的情況下進行權衡,並記錄下您選擇的方案。

Infisical:包含 UI、資料庫與您自行保管的主金鑰

Infisical 是一個密鑰管理平台,提供網頁介面、專案管理、環境區隔以及基於使用者的存取控制。使用 Docker Compose 自架的流程相當簡潔:

curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
docker compose -f docker-compose.prod.yml up -d

在執行最後一個指令前,請先編輯 .env。其中有兩個數值必須由您自行設定,且其中一個在設定後絕對不能變更:

openssl rand -hex 16
openssl rand -base64 32

第一個是 ENCRYPTION_KEY,這是一個 16 位元組的十六進位字串。它是用於加密 PostgreSQL 內部密鑰的金鑰;若遺失此金鑰,完美的資料庫備份將淪為無法解讀的密文,若在執行中的實例變更此金鑰,現有的密鑰將無法解密。第二個是 AUTH_SECRET,這是一個用於處理 Session 的 32 位元組 Base64 字串。SITE_URL 必須是您實際存取時的完整 URL(包含協定),否則登入後的重新導向將會失敗。

若您的需求重點在於人員協作(例如:小型團隊的網頁介面與環境隔離),而非自動過期的資料庫憑證,那麼 Infisical 會比 OpenBao 更合適。使用此方案需自行維護 PostgreSQL、Redis 與 TLS 憑證,這意味著您必須負責上述元件的修補更新與備份。

當 secrets service 離線且應用程式重啟時會發生什麼事

此問題決定了 secrets service 是否應部署在單一伺服器上。檔案在網路啟動前即可讀取,但服務則不然。

當伺服器重啟時,應用程式與 OpenBao 會同時啟動。應用程式嘗試取得資料庫密碼,但此時 OpenBao 仍處於 sealed 狀態,請求因此失敗。systemd 會不斷重啟應用程式,直到人工輸入 unseal shares 為止。系統並未損壞,但也無法運作。

處理此問題有兩種誠實的做法。一是調整單元啟動順序並讓應用程式重試:After= secrets service,加上 Restart=on-failure 以及足夠長的 RestartSec=,以避免頻繁發送 API 請求。另一種做法是在部署時而非開機時取得密鑰:將密鑰寫入權限為 600 的檔案或 systemd credential,讓執行中的系統依賴檔案而非 API。

Token 過期是時鐘週期較慢時會遇到的相同問題。OpenBao 的 token 與 lease 都有存活時間(TTL),因此若長期執行的處理程序未進行更新,將會在與部署無關的時刻失去存取權。這類故障之所以令人困惑,正是因為當天並未進行任何變更。

備份儲存區本身

此處的每個選項都有對應的密鑰,若備份時缺少該密鑰,備份將毫無價值。請務必記錄密鑰存放的位置。

對於 env 檔案,檔案本身即是機密,因此備份必須加密。對於 SOPS,加密後的檔案可存放於任何公開位置,但位於 ~/.config/sops/age/keys.txt 的 age 私鑰是絕對不能遺失的項目。對於 systemd credentials,請將 /var/lib/systemd/credential.secret.cred 檔案一併備份。對於 Infisical,請執行 PostgreSQL 傾印(dump),並將 ENCRYPTION_KEY 存放在與傾印檔案不同的位置。

使用 raft 儲存的 OpenBao 可自行建立快照:

bao operator raft snapshot save backup.snap
bao operator raft snapshot restore backup.snap

快照中包含已加密的儲存資料,因此將其還原至全新的伺服器時,仍需使用來自 bao operator init 的解鎖份額(unseal shares)。若僅執行將快照複製到物件儲存的每日排程,卻未妥善保存解鎖份額,該備份將無法發揮作用。在正式依賴此備份機制前,請務必在測試用的 VPS 上執行還原演練。

稽核日誌:誰讀取了哪些密鑰

檔案系統無法提供稽核追蹤。權限模式與擁有者僅能顯示「誰有權」讀取密鑰,卻無法顯示「誰實際」讀取了密鑰。使用 auditd 監控路徑是次佳的替代方案,但它僅能回報檔案被開啟,無法得知具體讀取了哪些數值。

OpenBao 會將每一項請求記錄至您明確啟用的稽核裝置中:

bao audit enable file file_path=/var/log/openbao_audit.log

關於此日誌的兩項事實會影響伺服器的運作方式。請求與回應中的大多數字串皆會透過 HMAC-SHA256 與 salt 進行雜湊處理,因此您可以在不洩漏明文的情況下,將已知的數值與日誌進行比對。整數與布林值則會以明文寫入,因此數值型密鑰無法透過此雜湊機制獲得保護。

此外,請注意此營運陷阱:當沒有任何已啟用的稽核裝置可供記錄時,OpenBao 將拒絕回應請求;若裝置發生阻塞式故障,請求將會掛起直到問題排除。設計上,/var/log 的磁碟空間耗盡會導致密鑰 API 停止服務。請務必在第一天就為稽核日誌規劃獨立空間並設定 logrotate 規則,不要等到發生第一次服務中斷後才處理。

該選擇哪種自架密鑰管理工具?

先計算機器數量與人員數量,再進行選擇。

  1. 單機、單人:使用權限為 600 的 env 檔案,由 root 擁有並由服務使用者讀取。若希望將數值從處理程序環境中移除,請使用 systemd credentials。
  2. 單機、2 至 5 人,且設定檔已納入 git 管理:使用 SOPS 搭配 age。每個人擁有一組金鑰對,並在 .sops.yaml 中列出所有允許解密的公鑰。
  3. 多台機器、單一設定儲存庫,且無需過期憑證:仍使用 SOPS 搭配 age,每台主機配置一個接收者金鑰,如此一來,即使某台主機的金鑰遭竊,也僅能解密該主機的檔案。
  4. 多台機器與多個團隊,且確實需要具備生命週期的資料庫憑證,並需保留可供查核的稽核軌跡:使用 OpenBao,並在預算中每月編列 1 小時的維運時間,用於執行解鎖與還原演練。

上述四種情境的底層規則皆相同。請執行能滿足你明確需求且規模最小的方案,因為當密鑰管理工具故障時,其狀態與密鑰管理工具為空無異。

FAQ

對於單台 VPS 而言,自架密鑰管理服務值得嗎?

通常不值得,若您指的是 OpenBao 或 Infisical 這類服務。在單台機器且僅有一兩位使用者的情況下,權限為 600 的 env 檔案或 systemd 加密憑證,對於防範其他本地使用者已具備同等保護力,且無需執行解鎖步驟,亦無額外服務需要維護更新。當您擁有數台機器、多位使用者,或有憑證需自動過期而無法手動輪替的實際需求時,密鑰管理服務才具備價值。

密碼管理器與密鑰管理器有何不同?

密碼管理器儲存的是由人員輸入的憑證,並由人類在場時進行解鎖。密鑰管理器則是將憑證提供給處理程序(process),因此它必須在凌晨 3 點無人看管時也能運作。這導致了兩者的差異:密碼管理器鎖定時會要求您重新輸入主密碼,而密鑰管理器一旦被鎖定(sealed),所有在該期間重啟的服務都會停止運作。

如果 OpenBao 在重啟後被鎖定(sealed),我的應用程式會發生什麼事?

它們將無法獲取密鑰,導致啟動失敗,並由 systemd 進行無限迴圈重啟,直到有人提供解鎖門檻(預設為 5 個分片中的 3 個)。OpenBao 僅將根金鑰保留在記憶體中,因此每次重啟都會再次鎖定。您可以選擇開啟自動解鎖(auto unseal),但需接受在單台 VPS 上,解鎖金鑰最終會與資料存放在同一顆磁碟上的事實;或者在部署時將密鑰渲染為檔案,確保開機過程不依賴 API。

我可以將 SOPS 加密後的檔案提交到公開儲存庫嗎?

由於內容已加密,沒有 age 私鑰的人無法讀取。但金鑰本身並未加密:讀者可以看見您持有 STRIPE_SECRET_KEYSMTP_PASSWORD,以及每個金鑰變更的頻率。對於大多數專案而言,這類元資料(metadata)是可以接受的,但對少數專案則不然。請務必將 age 私鑰排除在儲存庫之外,並在每次新增或移除接收者時,對所有現有檔案執行 sops updatekeys