tapwarden:金鑰放 Bitwarden 或自架 Vaultwarden,簽 SSH 一樣要 Touch ID

2026-07-05
tapwarden:自架 Vaultwarden 簽 SSH 也要 Touch ID

自己平常簽 SSH、簽 git commit 用的是 1Password 的 agent,金鑰放進 1Password 的 vault,每次要簽章都跳一次 Touch ID,體驗很好。剛好 1Password 到期,想說乾脆整個密碼管理都換成自架的 Vaultwarden,SSH 簽章這塊也得跟著搬過去。

Bitwarden 官方沒有對應的 agent。網路上找得到的替代方案(vault-conductor、bw-agent 這類)能從 Secrets Manager 或 secure notes 撈金鑰,但少了 1Password 那個「每次簽章都要按一下」的關卡:同一個使用者跑的程式,只要能連上 agent 的 socket,就能靜靜簽過去,不會有任何提示。

於是寫了 tapwarden:SSH agent 本體,金鑰來源是 Bitwarden Secrets Manager 或自架的 Vaultwarden,每一次簽章都會跳出 Touch ID 提示,金鑰只存在記憶體,從不落地。

現有選項各缺一角

README 裡的比較表格說得很直白:

金鑰來源每次簽章都要驗證
1Password agent1Password vault✅ Touch ID
Bitwarden GUI agent個人 vault❌ 手動解鎖一次就好
vault-conductor / bw-agentSecrets Manager / secure notes❌ 靜默簽章
tapwardenSecrets Manager 或 Vaultwarden(可限縮範圍)✅ 每次簽章都要 Touch ID

tapwarden 想要的東西,其他選項都只能給一半:金鑰的存取權限要收得夠小,環境裡的憑證只能讀到你授權的那幾把 SSH 金鑰(一個只綁定單一專案的 BWS machine token,或是一個除了金鑰什麼都沒有的 Vaultwarden 帳號),不是你整個密碼庫;而且每一次要簽章,都得有人真的在電腦前面按一下 Touch ID,就算是同一個使用者跑的程式碰到 agent 的 socket,也沒辦法靜默簽過去。

金鑰從哪來:兩種後端

Bitwarden Secrets Manager 是官方產品,雲端版或自架版都支援:在 web vault 開通 Secrets Manager,建立 project,一把 SSH 金鑰對應一個 secret,再開一個只讀該 project 的 machine account,拿到 access token。設定檔裡只填 secret 的 UUID 和「哪個環境變數放 token」,token 本身永遠不會寫進設定檔。

Vaultwarden 沒有做 Secrets Manager,那是 Bitwarden 專有授權的功能,所以 tapwarden 對 Vaultwarden 走另一條路:讀「SSH 金鑰」類型的 vault item(cipher type 5),從一個只放金鑰、什麼都不做的專用帳號讀。伺服器端要加 EXPERIMENTAL_CLIENT_FEATURE_FLAGS=ssh-key-vault-item 這個 flag 才能建立這類 item。設定這條路徑不用手動寫 YAML,跑 tapwarden setup 就好:它會幫你登入一次、列出可選的金鑰、寫好設定檔,預設把憑證存進 macOS Keychain,之後每次 agent 要讀這組憑證,也一樣要過 Touch ID。想留在 CI 或 Linux 上跑的話,setup 也可以選擇把憑證留在環境變數。

怎麼授權:Touch ID 或 FIDO2 安全金鑰

授權因子可以從 Touch ID 換成 FIDO2 安全金鑰(YubiKey 之類),在設定裡選 factor: touch_id(預設)或 factor: yubikey,兩者是二選一,不能同時開。YubiKey 扮演的角色跟 Touch ID 一樣:每次簽章都要有人真的去碰一下那把金鑰。

註冊用 tapwarden register-yubikey 跑一次:插上金鑰、輸入 PIN(如果金鑰有設)、碰一下,它會印出 credential ID 和 base64 的公鑰,自己貼進設定檔。PIN 只有註冊時要,之後簽章只需要碰。註冊和簽章的時候都只能插一把 FIDO2 金鑰。另外 v0.2.0 建立的設定檔要重新註冊一次,才能啟用對 assertion 的密碼學驗證。

要注意這不會讓它變成跨平台 —— FIDO2 這塊的實作是綁 macOS API 的,換成 YubiKey 一樣只能在 macOS 上跑。

背景常駐與 launchd 的小地雷

tapwarden start 把自己裝成 LaunchAgent,開機自動啟動、crash 會重啟,指令印出 socket 路徑,~/.ssh/config 裡的 IdentityAgent 指過去就不用管了:

Host *
  IdentityAgent <tapwarden socket-path 的輸出>

ssh-add -L 列公鑰不會跳提示,實際 ssh 連線簽章才會跳 Touch ID。

這裡有一個踩過的坑:launchd 啟動的程式看不到你 shell 裡 export 的環境變數。如果設定走的是環境變數這條路(BWS backend,或 Vaultwarden 選了 credentials: env),背景執行的 agent 讀不到那些變數,直接會失敗。解法有幾個:BWS backend 可以用 tapwarden store-token,把 access token 一次存進 macOS Keychain,之後讀取一樣要過 Touch ID,背景 agent 就不必依賴 shell 環境;Vaultwarden 那邊換成 credentials: keychain(tapwarden setup 會幫你設好);想先確認問題出在哪,用 tapwarden start --fg 在有 export 過的 shell 裡跑一次;真的想留在環境變數,就自己去 LaunchAgent 的 plist 裡加 EnvironmentVariables。踩過一次才知道這是背景服務常見的坑,不是 tapwarden 特有的問題。

出問題的時候 tapwarden doctor 會直接告訴你卡在哪一關:它會檢查設定檔、憑證讀不讀得到、Touch ID/YubiKey 有沒有偵測到、LaunchAgent 狀態、socket 路徑、~/.ssh/config 有沒有接上,任何一項失敗就以非零 exit code 結束。加 --check-backend 會多做一步,實際去後端把每一把設定好的金鑰抓一次,等於端到端驗證 —— 代價是這個模式需要後端憑證真的可用。

指令一覽

前面提過的和沒提過的都收在這裡,當作速查表:

指令做什麼
tapwarden setup互動式設定 Vaultwarden,登入一次、列出金鑰、寫好設定檔
tapwarden start裝成 LaunchAgent 背景常駐,--fg 改成前景跑
tapwarden stop停掉 agent,但 LaunchAgent 定義保留,之後還能再啟動
tapwarden socket-path印出 agent socket 路徑,填進 ~/.ssh/config 用的
tapwarden doctor唯讀診斷,--check-backend 會實際去後端抓一次金鑰
tapwarden logs看 ~/Library/Logs/tapwarden.log 的最後 50 行
tapwarden store-token把 BWS access token 存進 Keychain,僅 BWS backend 適用
tapwarden register-yubikey註冊 FIDO2 安全金鑰當作授權因子
tapwarden uninstall停掉 agent 並移除 LaunchAgent plist

stop 和 uninstall 差在前者只是暫停、plist 還在,後者會把 ~/Library/LaunchAgents/com.tapwarden.agent.plist 一起刪掉,要再用得重裝一次。

socket 路徑不是固定的,會落在 $XDG_RUNTIME_DIR/tapwarden 或一個帶 uid 後綴的暫存目錄下,依系統設定而定,所以 ~/.ssh/config 裡請照 socket-path 印出來的填,不要自己猜。

簽署二進位檔案

沒簽署的 binary 沒有穩定的 code identity,結果就是每次重新編譯,macOS 的 Keychain 都會當成新程式重新問一次授權。免費的解法是自己開一張 self-signed 的 code signing 憑證,每次 build 完簽一下:

codesign -s tapwarden-dev --force target/release/tapwarden

要做到 distribution 等級(Developer ID + 公證,Gatekeeper 和系統層級的 keychain ACL 都要靠這個)需要付費的 Apple Developer 帳號,目前還在 roadmap 上,還沒做。

安全模型裡在意的細節

grace 模式(簽過一次之後,同一把金鑰在 grace_seconds 內免提示)只影響簽章本身,不會連帶讓 Keychain 裡的 Vaultwarden 憑證跳過 Touch ID,這兩個關卡是分開算的,approval 不會互相借用。

Vaultwarden 那邊的加密實作(EncString 用 AES-256-CBC + HMAC-SHA256,KDF 推導)是照著官方 Bitwarden SDK 的原始碼和它公開的測試向量對著寫的,MAC 一定是在 decrypt 之前、用 constant-time 比對驗證完才繼續,避免 timing side channel。專案本身沒有依賴官方 SDK,REST 呼叫直接建在 reqwest 和 rustls 上,為的是讓依賴樹小一點、好稽核(cargo audit 目前乾淨,唯一忽略的 advisory 也寫在 .cargo/audit.toml 裡註明原因)。

Agent 的 socket 放在每個使用者專屬、權限 0700 的目錄下,會檢查有沒有被 symlink 攻擊,bind 之前也會先收緊 umask。

現況

目前是能用的階段:Touch ID 已經在沒簽署的 binary 上驗證過可以跳出提示,兩種後端都對著官方 SDK 的協定和測試向量實作完,也自己做過一輪 security review。BWS access token 收進系統 keystore 這件事後來也做掉了,就是上面的 store-token。

macOS 是硬需求 —— 授權因子雖然多了 FIDO2 安全金鑰這個選項,但那塊實作一樣綁 macOS API,所以 Linux 支援還在 roadmap 上,跟 Developer ID 簽署加公證、上架 Homebrew 排在一起。

1Password 那套 Touch ID 體驗確實很好用,換過來之後才發現,這其實才是我一直想要的:金鑰收在自己控制的 Vaultwarden 裡,簽章一樣得先過 Touch ID 才放行。如果你也想把密碼管理整個搬到自架 Vaultwarden,又捨不得 1Password 那種簽章體驗,這是一個選項。

專案原始碼:GitHub

https://blog.2ac.io/posts/feed.xml