用 HashiCorp Vault 當 SSH CA:跟 authorized_keys 說再見
前言
當你管理的機器從 1 台變成 50 台之後,SSH 金鑰一定會讓你半夜驚醒:
- 「有個工程師離職了,他的 public key 到底散在哪幾台機器的
authorized_keys裡?我要一台一台去刪嗎?」 - 「跳板機(bastion / control node)上放了一把萬能私鑰,只要那把鑰匙外洩,整個機房就全裸了。」
- 「我只想讓 CI 在『部署的那 5 分鐘』內能連進去,但傳統金鑰一放上去就是永久有效,根本沒有『到期』這回事。」
這些痛點的共通根源是:傳統 SSH 是「一把鑰匙配一把鎖」的信任模型——你得把每一把公鑰(public key)事先塞進每一台目標主機。機器一多,這件事就變成災難。
這篇文章要介紹一個更聰明的解法:把 HashiCorp Vault 當成一個「SSH 憑證中心(CA, Certificate Authority)」。
🔑 CA(Certificate Authority,憑證簽發中心)是什麼? 你可以把它想成「機場的證件核發櫃檯」。目標主機不再認得每一位旅客(每一把公鑰),它只認得「這張登機證是不是機場官方蓋章的」。只要 Vault 蓋過章,主機就放行——而且這張登機證還有登機時間限制(TTL),過期自動作廢。
讀完這篇,你會學到:用 Vault 簽發短命(例如 5 分鐘) 的 SSH 憑證,讓跳板機不必再存永久金鑰,還能針對不同權限需求發不同的憑證。這套架構和 [[在 k3s 上手把手打造 Prometheus + Grafana 監控系統|Kubernetes]] 環境也能無縫搭配(本文最後會談到)。
核心概念:從「認鑰匙」到「認簽章」
先建立心智模型。傳統 SSH 與 Vault SSH CA 的差別,一張圖就看懂:
flowchart LR
subgraph 傳統做法["😵 傳統:每台主機都要放你的公鑰"]
U1[你的 public key] -->|事先手動複製| H1[主機 A
authorized_keys]
U1 -->|事先手動複製| H2[主機 B
authorized_keys]
U1 -->|事先手動複製| H3[主機 C
authorized_keys]
end
subgraph CA做法["😎 Vault CA:主機只信任『一個簽章者』"]
V[Vault CA] -.->|主機只放 Vault 的 public key 一次| T1[主機 A]
V -.->|同一把| T2[主機 B]
V -.->|同一把| T3[主機 C]
end
關鍵差異:CA 做法下,目標主機只需要放一次 Vault 的 CA 公鑰(TrustedUserCAKeys),之後不管你發幾張憑證、給幾個人,主機都不用再改設定。人員異動?只要在 Vault 撤銷/停發即可,不必碰任何一台主機。
一次連線的完整旅程
再看「實際連線時」發生什麼事。假設「跳板機(control node)」想連到「目標主機」:
sequenceDiagram
participant C as 跳板機
(control node)
participant V as Vault
(CA)
participant T as 目標主機
(target)
Note over C: 我手上有一對 key pair
(private + public)
C->>V: ① 帶著 public key 求簽
(附上身分證明)
V-->>C: ② 回傳 signed cert
(TTL 只有 5 分鐘)
C->>T: ③ 帶 private key + cert 連線
Note over T: 我信任 Vault CA,
檢查這張 cert 是它蓋的章嗎?
過期了沒?
T-->>C: ④ 驗章通過 → 允許登入 ✅
用一句話總結整條流程:
你的 key pair(本地)→ 把 public key 送去 Vault 簽 → Vault 回傳 signed cert(TTL 5 分鐘) → SSH Server 看到 cert,確認是 Vault CA 簽的且沒過期 → 允許登入。
有三個名詞先在這裡解釋清楚,後面就不卡了:
| 名詞 | 白話解釋 |
|---|---|
| key pair(金鑰對) | 你用 ssh-keygen 產生的一對鑰匙:private key(私鑰,自己藏好)與 public key(公鑰,可以公開)。 |
| signed cert(簽署憑證) | Vault 拿你的 public key「加工」出來的一張證件,裡面寫著「誰可以登入、能登入誰、有效到幾點」,並蓋上 Vault 的簽章。 |
| TTL(Time To Live) | 憑證的壽命。設 5 分鐘,就代表這張證件 5 分鐘後自動失效,就算被偷走也沒用。 |
準備工作:Vault 與角色設計
本文假設你已經有一個跑起來的 Vault(dev 模式即可,vault server -dev,預設監聽 http://127.0.0.1:8200)。我們要打造的角色叫 deploy-service——一個負責自動部署的服務,它需要在每次部署前跟 Vault 要一張短命憑證去連目標機執行 SSH Command。
整體會用到兩個 Vault 元件,先有個印象:
flowchart LR
A["SSH Secrets Engine
(負責簽 SSH cert)"]
B["AppRole Auth Method
(負責驗證『你是誰』)"]
B -->|換到 token| C[deploy-service 的 token]
C -->|拿 token 去簽 cert| A
A -->|回傳 signed cert| D[拿去連 SSH]
- SSH Secrets Engine:Vault 裡真正「當 CA、簽 SSH 憑證」的模組。
- AppRole Auth Method:讓「機器/服務」不用人工打帳密,改用
role_id+secret_id(像帳號+密碼)換一個 token 來跟 Vault 溝通。
💡 為什麼用 AppRole 而不是直接用 root token? 因為 root token 權力太大、又不會過期,放在 CI 或服務裡等於自殺。AppRole 讓你能發一個「只能簽
deploy-service這條路徑」的最小權限身分,這才是 production 的正確姿勢。
逐步範例
我們分四大階段:① 啟用 SSH CA → ② 設定 AppRole 身分 → ③ 用 curl 手動跑通一次 → ④ 讓目標主機信任 CA。每一步都有可以直接貼上跑的指令。
📌 全文以
curl直打 Vault API 示範(最透明,看得到每個環節)。UI 能做的地方我也會標出路徑,但 AppRole 的 Role 管理只能用 API/CLI(UI 不支援)。
階段一:啟用並設定 SSH Secrets Engine
Step 1:啟用 SSH Secrets Engine
我們把它掛在自訂路徑 ssh/ssh-client-signer 底下(路徑名稱可自取,之後所有 API 都會用到它):
1 | # UI 路徑:Vault → Secrets → Enable new engine → SSH |
Step 2:產生這個 engine 的 CA 金鑰
CA 自己也需要一對金鑰(它就是用這把 private key 去「蓋章」)。讓 Vault 自動幫我們產生:
1 | curl --header "X-Vault-Token: $VAULT_TOKEN" \ |
Step 3:建立 SSH Role「deploy-service」
Role 定義「這種憑證長什麼樣、能做什麼」。這是安全的核心,逐欄位看:
1 | curl --header "X-Vault-Token: $VAULT_TOKEN" \ |
| 欄位 | 說明 |
|---|---|
key_type: ca |
這個 Role 是「CA 簽發模式」(另一種是幫你託管私鑰,本文不用)。 |
allow_user_certificates |
允許簽「使用者憑證」(登入用),對應下面 cert_type: user。 |
allowed_users |
白名單:這張憑證只允許以 root 或 deploy 這兩個 OS 使用者身分登入。這就是「針對權限需求發不同憑證」的關鍵——想給更小權限,就開一個 Role 只允許 deploy。 |
default_user |
沒指定時預設用哪個使用者。 |
ttl / max_ttl |
憑證預設 5 分鐘、最長可要到 30 分鐘。短命是安全的靈魂。 |
default_extensions |
憑證的能力。permit-pty = 允許互動式 terminal;不給的話連 shell 都開不了(適合只想跑單一指令的場景)。 |
🎯 一個 Role = 一種權限等級。你可以再開
readonly-service(allowed_users: "readonly"、不給permit-pty),或dba-service(只允許postgres使用者)。權限切分全靠多開幾個 Role,而目標主機完全不用改。
階段二:設定 AppRole,讓服務能自己拿身分
現在讓 deploy-service 這個服務,能用「帳號+密碼」自動跟 Vault 換 token。
Step 4:啟用 AppRole Auth Method
1 | # UI 路徑:Vault → Access → Auth Methods → Enable new method → AppRole |
Step 5:建立一個「最小權限」Policy
Policy(權限政策)決定「這個身分能碰 Vault 的哪些路徑」。我們只給它一件事——簽 deploy-service 的 SSH cert:
1 | # Policy 內容(HCL 格式):只允許對「簽 deploy-service cert」的路徑做 create/update |
⚠️ 常見踩雷:Policy 裡的路徑
ssh/ssh-client-signer/sign/deploy-service必須跟你 Step 1 的 mount 路徑一字不差。如果你當初掛在別的路徑(例如只有ssh/),這裡就要跟著改成ssh/sign/deploy-service,否則簽 cert 時會被拒(permission denied)。
Step 6:建立 AppRole 並綁上 Policy
1 | curl --header "X-Vault-Token: $VAULT_TOKEN" \ |
| 參數 | 值 | 說明 |
|---|---|---|
token_policies |
deploy-service-policy |
換來的 token 綁上剛剛那個最小權限 policy。 |
token_ttl |
1h |
AppRole 換來的 token 有效 1 小時。 |
token_max_ttl |
4h |
token 續期(renew)最長能撐到 4 小時。 |
secret_id_ttl |
0 |
secret_id 永不過期。這是 dev 方便設定;production 請務必設時限。 |
secret_id_num_uses |
0 |
secret_id 可無限次使用;production 建議設成有限次數。 |
🧠 Token 類型小知識:AppRole 換來的 token 預設是
default-service型——可以續期(renew)、有父子關係。這正是我們要的,因為deploy-service是長期運行的服務,token 需要能在有效期內延長,不然每小時就得用role_id+secret_id重換一次。另一種batch型輕量但不能 renew,適合高頻、短命的一次性請求,這裡不適用。
Step 7:取出 role_id 與 secret_id
把這兩個值存進服務的 .env。role_id 像「帳號」(固定不變),secret_id 像「密碼」(可撤銷、可輪替):
1 | # 取得 role_id(固定的,像帳號) |
🔐 兩個值分工要記牢:登入時用
secret_id(不是 accessor)。secret_id_accessor是「把手」,平常查詢、萬一secret_id外洩時可以拿它來撤銷該把密碼(而不會意外印出真正的祕密)。想知道某個 Role 目前有幾組 secret_id,就用LIST方法查——它只會回傳一串accessor,不會洩漏真祕密。
階段三:用 curl 手動跑通一次
正式接進服務前,先手動把「換 token → 簽 cert → 看 cert」跑一遍,確認每個環節都對。
Step 8:用 role_id + secret_id 換 token
1 | curl --request POST \ |
🐚 上面用了
'"$VAR"'這種寫法把 shell 變數插進單引號 JSON——因為單引號內變數不會展開。把client_token存成環境變數VAULT_ROLE_TOKEN給下一步用。
Step 9:產生一對測試金鑰
1 | ssh-keygen -t ed25519 -f /tmp/test_key -N "" |
🔎 為什麼選 Ed25519?它比傳統 RSA 更短、更快、更安全,是現代 SSH 的首選。
Step 10:用 token 向 Vault 簽 cert
把公鑰送去 sign/deploy-service,要一張使用者憑證(cert_type: user):
1 | curl --header "X-Vault-Token: $VAULT_ROLE_TOKEN" \ |
簽 cert 的 API 路徑要牢記這個公式:
1 | /v1/{mount_path}/sign/{SSH Role Name} |
Step 11:把 cert 存檔並檢查內容
永遠要驗一下 Vault 到底簽了什麼給你——尤其是 Valid(有效時間)、Principals(能登入哪些使用者)、Extensions(有沒有 permit-pty):
1 | # 把 signed_key 內容存成 cert 檔(OpenSSH 慣例:<key 名>-cert.pub) |
你會看到類似這樣的輸出,確認每一項都符合 Step 3 的設定:
1 | Type: ssh-ed25519-cert-v01@openssh.com user certificate |
階段四:讓目標主機信任 Vault CA
最後一哩路:目標主機得知道「Vault 這個 CA 值得信任」。這一步在每台目標主機上只需做一次。
Step 12:取得 Vault CA 的公鑰
這是個公開端點,不需 token:
1 | curl http://127.0.0.1:8200/v1/ssh/ssh-client-signer/public_key |
Step 13:在目標主機上信任這個 CA
1 | # 1. 把 CA 公鑰放到主機上 |
🚨 超級大坑:
TrustedUserCAKeys不能重複設定來「累加」多個檔案!很多人以為可以這樣分開寫兩個 CA:
1
2 TrustedUserCAKeys /etc/ssh/ca1.pub
TrustedUserCAKeys /etc/ssh/ca2.pub
sshd不會同時載入兩個檔案。 這個選項不是「可累加的清單」,重複出現時只有其中一個會生效,另一個被默默忽略——你會在「明明加了 CA 卻登不進去」的鬼打牆裡耗掉一整個下午。
正解:把多把 CA 公鑰塞進同一個檔案:
1 | # 一個檔案,一行一把 CA 公鑰,OpenSSH 會把每一把都當成可信任的 User CA |
或者直接用 cat 合併現有的多個 .pub:
1 | cat ca1.pub ca2.pub ca3.pub > /etc/ssh/trusted-user-ca-keys.pem |
Step 14:用 cert 登入!
見證奇蹟的時刻。私鑰和 cert 都要帶上,SSH client 會自動配對使用:
1 | ssh -i /tmp/test_key \ |
✅ 登入成功——而且你從沒把任何 public key 放進目標主機的
authorized_keys。目標主機只認得 Vault 的簽章,其餘一概不管。5 分鐘後這張 cert 自動失效,就算私鑰+cert 被人整包偷走,也活不過午休。
進階:在 Kubernetes 裡怎麼玩
如果你的目標主機(或 SSH 服務)跑在 [[在 k3s 上手把手打造 Prometheus + Grafana 監控系統|Kubernetes]] 上,「信任 CA」這一步會遇到一個現實問題:多個團隊各有自己的 CA 公鑰 ConfigMap,但 TrustedUserCAKeys 又只能吃一個檔案(見上面的大坑)。
慣用解法是:在 initContainer 或 entrypoint 裡,開機時把所有團隊的 CA 公鑰合併成一個檔案:
1 | # initContainer / entrypoint 內:把掛進來的所有 *.pub 合成一份 |
而 sshd_config 永遠只寫死一行,乾淨俐落:
1 | TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem |
這樣一來,新增一個團隊 = 多掛一個 ConfigMap 進 /ca/,重啟 Pod 即可,sshd_config 一個字都不用動。
完整範例:一個服務的部署流程長怎樣
把前面所有片段串起來,deploy-service 每次要 SSH 進目標機時,實際跑的就是這條 pipeline:
flowchart LR
A[服務啟動] --> B["用 role_id + secret_id
換 token"]
B --> C["每次連線前,用 token
簽一張 cert (TTL 5min)"]
C --> D["帶 private key + cert
SSH 進目標主機"]
D --> E["目標主機驗 Vault 簽章
→ 放行"]
E -.->|5 分鐘後 cert 自動失效| F[歸零,下次重簽]
濃縮成一句話收尾:
deploy-service啟動 → 用role_id+secret_id向 Vault 換 token → 每次 SSH 連線前,用 token 向 Vault 簽 cert(TTL 5 分鐘)→ 拿 cert + private key 連 SSH Node → 主機驗章放行 → cert 過期自動歸零。
結語
我們從「每台主機都要塞公鑰」的傳統煉獄,一路走到「主機只信任一個 CA、憑證 5 分鐘自動作廢」的現代架構。重點回顧:
- 信任模型翻轉:目標主機從「認每一把鑰匙」變成「認一個簽章者(Vault CA)」,靠
TrustedUserCAKeys設定,每台只需做一次。 - 短命憑證(TTL):憑證幾分鐘就失效,大幅壓縮金鑰外洩的傷害窗口。
- 權限即 Role:想切分權限,就多開幾個 SSH Role(不同
allowed_users、不同extensions),目標主機完全不用改。 - 服務用 AppRole:機器用
role_id+secret_id換最小權限 token,杜絕 root token 亂放。 - 記得那個大坑:
TrustedUserCAKeys不能累加,多把 CA 一律合併成單一檔案。
下一步可以動手做的:把 deploy-service 的簽 cert 邏輯包進你的 CI/部署腳本、把 secret_id 改成有時限+有限次數(production 必做)、或替不同團隊各開一個最小權限 Role。跟散落各處、永不過期的 authorized_keys 說再見吧!