自己平常簽 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,就能靜靜簽過去,不會有任何提示。

於是寫了 sigilo: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❌ 靜默簽章
sigiloSecrets Manager 或 Vaultwarden(可限縮範圍)✅ 每次簽章都要 Touch ID

sigilo 想要的東西,其他選項都只能給一半:金鑰的存取權限要收得夠小,環境裡的憑證只能讀到你授權的那幾把 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 專有授權的功能,所以 sigilo 對 Vaultwarden 走另一條路:讀「SSH 金鑰」類型的 vault item(cipher type 5),從一個只放金鑰、什麼都不做的專用帳號讀。伺服器端要加 EXPERIMENTAL_CLIENT_FEATURE_FLAGS=ssh-key-vault-item 這個 flag 才能建立這類 item。設定這條路徑不用手動寫 YAML,跑 sigilo setup 就好:它會幫你登入一次、列出可選的金鑰、寫好設定檔,預設把憑證存進 macOS Keychain,之後每次 agent 要讀這組憑證,也一樣要過 Touch ID。想留在 CI 或 Linux 上跑的話,setup 也可以選擇把憑證留在環境變數。

背景常駐與 launchd 的小地雷

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

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

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

這裡有一個踩過的坑:launchd 啟動的程式看不到你 shell 裡 export 的環境變數。如果設定走的是環境變數這條路(BWS backend,或 Vaultwarden 選了 credentials: env),背景執行的 agent 讀不到那些變數,直接會失敗。解法三選一:改用 sigilo start --fg 在有 export 過的 shell 裡跑;換成 credentials: keychainsigilo setup 會幫你設好);或自己去 LaunchAgent 的 plist 裡加 EnvironmentVariables。踩過一次才知道這是背景服務常見的坑,不是 sigilo 特有的問題。

簽署二進位檔案

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

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

要做到 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。只支援 macOS,Touch ID 是這個機制的硬需求,Linux 支援還在 roadmap 上,跟簽署、上架 Homebrew、把 BWS 的 access token 也收進系統 keystore 排在一起。

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

專案原始碼:GitHub