Keeper 是一個為 Go 語言設計的加密秘密儲存庫。它使用 Argon2id 金鑰衍生和 XChaCha20-Poly1305(預設)認證加密來加密靜態的任意位元組負載,並將它們儲存在嵌入式的 bbolt 資料庫中。

它提供三種可獨立使用的組件:

Keeper 最初是為 Agbero 負載平衡器設計的基礎秘密管理層,但它不依賴於 Agbero,並且可以在任何 Go 專案中使用。

Keeper 將秘密劃分為儲存桶(buckets)。每個儲存桶都有一個不可變的 BucketSecurityPolicy,用於管理其資料加密金鑰(DEK)的保護方式。提供四個等級可供選擇。

Scheme 是一種 URI 前綴,用於對相關的儲存桶進行分組(例如 vault://, certs://, space://,或任何您註冊的名稱)。安全等級是儲存桶策略的一個屬性,在建立時設定且之後不可更改。

您可以在同一個 Scheme 中自由混合不同的安全等級。例如,vault://system 可能設定為 LevelPasswordOnly(在啟動時自動解鎖),而 vault://admin 可能設定為 LevelAdminWrapped(需要明確的憑證)。

儲存桶的 DEK 是使用 HKDF-SHA256 從主金鑰派生的,並為每個儲存桶使用一個域分隔的 info 字串(keeper-bucket-dek-v1:scheme:namespace)。所有 LevelPasswordOnly 儲存桶在呼叫 UnlockDatabase 並提供正確的主密碼後都會自動解鎖。執行階段不需要為每個儲存桶設定個別憑證。此等級適用於進程在啟動時需要但不需要人工互動的秘密。

儲存桶有一個隨機生成的 32 位元組 DEK,對該儲存桶是唯一的。DEK 絕不會以明文形式儲存。對於每個授權的管理員,都會從 HKDF(masterKey‖adminCred, dekSalt) 派生出一個金鑰加密金鑰(KEK),並使用 XChaCha20-Poly1305 來包裝 DEK。在管理員呼叫 UnlockBucket 並提供其憑證之前,該儲存桶是無法存取的。僅憑主密碼無法解密該儲存桶。撤銷一個管理員的存取權不會影響其他管理員的包裝副本。

儲存桶的 DEK 在 CreateBucket 時生成,並立即由呼叫者提供的 HSMProvider 包裝。該提供者執行包裝和解包操作——在將原始 DEK 交給提供者後,keeper 絕不會處理它。UnlockDatabase 會自動呼叫提供者來解包並為所有已註冊的 HSM 儲存桶設定 Envelope。主金鑰輪換不會重新加密這些儲存桶;DEK 由提供者控制。

在 pkg/hsm 中提供了一個內建的 SoftHSM 實現,該實現由一個受 memguard 保護的包裝金鑰支援,用於測試和 CI 環境。請勿在生產環境中使用它。

在金鑰管理行為上與 LevelHSM 相同,但 HSMProvider 由 pkg/remote.Provider 實現——這是一個可配置的 HTTPS 配接器,通過 TLS 將包裝和解包委託給任何遠端 KMS 服務。pkg/remote 中提供了 HashiCorp Vault Transit、AWS KMS 和 GCP Cloud KMS 的預建配置。對於生產環境使用,請配置 TLSClientCert 和 TLSClientKey 以啟用雙向 TLS 驗證。

在首次派生時儲存一個驗證雜湊:

後續的 DeriveMaster 呼叫會重新計算此雜湊,並使用 crypto/subtle.ConstantTimeCompare 進行比較。如果雜湊不匹配,則返回 ErrInvalidPassphrase。

KDF 鹽(salt)是故意以未加密的形式儲存的。在呼叫 UnlockDatabase 之前必須能夠讀取它以派生主金鑰——使用從主金鑰派生的金鑰來加密它將形成循環。KDF 鹽不是秘密;它的目的是提供唯一性,而不是保密性。

每個明文值都使用儲存桶的 DEK 和 XChaCha20-Poly1305 進行加密:

儲存的記錄是一個 msgpack 編碼的 Secret 結構,其中包含密文、加密的元資料和結構版本。認證是隱含的:使用錯誤的金鑰解密的密文會在返回任何明文之前產生 AEAD 認證失敗。

KEK 是使用 HKDF 派生的,而不是第二次 Argon2 傳遞。主金鑰已經由一個高成本的 KDF 生成;第二次呼叫 Argon2 會為每次 UnlockBucket 呼叫增加數百毫秒的延遲,而沒有安全上的好處。HKDF-SHA256 的運行時間約為一微秒。

縱深防禦:即使攻擊者僅破壞了資料庫,他們也只能獲得包裝的 DEK 和 HKDF 鹽,但無法在不知道主金鑰的情況下派生 KEK。如果攻擊者僅破壞了主金鑰,他們也無法解開任何 LevelAdminWrapped 的 DEK,除非他們同時知道管理員憑證。

秘密的元資料(建立時間、更新時間、存取次數、版本)與密文分開加密:

對於 LevelAdminWrapped、LevelHSM 和 LevelRemote 儲存桶,這意味著在不知道儲存桶憑證的情況下無法存取元資料,從而防止擁有資料庫檔案讀取權限的攻擊者了解存取模式或時間戳。

時序側通道注意事項:XChaCha20-Poly1305 在返回認證錯誤之前會處理完整的密文。後備解密路徑(新的派生 DEK →舊的主金鑰作為 DEK)花費的實際時間與哪個金鑰成功無關。沒有時序側通道會洩漏記錄的遷移狀態。

所有結構性元資料也會在靜態時加密。在 UnlockDatabase 時,會從主金鑰派生出兩個金鑰:

policyEncKey 用於加密:BucketSecurityPolicy 值和輪換 WAL(Write-Ahead Log)。

auditEncKey 用於加密:每個審計事件的 Scheme、Namespace 和 Details 欄位。

兩個金鑰都會在呼叫 Lock() 時從記憶體中清除。用於元資料加密的密文與用於秘密的加密介面相同(由使用者選擇的密文,預設為 XChaCha20-Poly1305,FIPS 為 AES-256-GCM),這意味著使用者選擇的密文演算法會自動應用於策略、WAL 和審計加密。

所有加密元資料 Blob 的線路格式:

磁碟上的策略金鑰是不可識別的雜湊,而不是明文的 scheme:namespace 字串,這可以防止離線枚舉儲存桶名稱:

記憶體中的 schemeRegistry 繼續使用「scheme:namespace」作為其金鑰——只有磁碟上的表示法會改變。

每個策略記錄在一次 bbolt 交易中原子地寫入兩個完整性標籤:

在 UnlockDatabase 之前,只有 SHA-256 雜湊可用。解鎖後,loadPolicy 會驗證 HMAC 標籤。UnlockDatabase 會呼叫 upgradePolicyHMACs 來為在此功能存在之前建立的策略補回 HMAC 標籤。

簽署金鑰在 UnlockDatabase 時啟用,並在 Lock 時清除。當主金鑰輪換時,Rotate 會將一個金鑰輪換檢查點事件附加到每個活動的審計鏈中,並使用舊的審計金鑰作為舊時段的最後一個事件進行簽署。歷史記錄永遠不會被重寫;檢查點是不同時段之間的信任橋樑。

所有中間金鑰在使用後會立即被歸零。主金鑰絕不會以任何形式寫入磁碟。

底層資料庫是 bbolt。所有儲存桶及其內容:

Event 結構使用獨立的明文路由欄位(Scheme、Namespace)以及加密的負載欄位(EncScheme、EncNamespace、EncDetails)。檢查和會針對明文路由欄位和加密的 EncDetails 位元組進行計算,因此鏈的完整性可以在三個層級上進行驗證,而無需任何金鑰:

例如:合規性審計員僅接收 auditEncKey。他們可以驗證跨金鑰輪換的完整 HMAC 鏈並讀取所有事件詳細資訊,但無法解密任何秘密值。只有資料庫檔案的公開觀察者仍然可以檢測到任何事件是否在事後被修改或插入。

KDF 鹽以 msgpack 編碼的 SaltStore 形式儲存在 salt 元資料金鑰下。每次鹽輪換都會附加一個新的 SaltEntry 並推進 CurrentVersion。舊條目會被保留作為審計記錄。SaltStore 是未加密儲存的——請參閱安全決策。

Rotate 在觸碰任何記錄之前會寫入一個 WAL。WAL 包含 WrappedOldKey:預輪換的主金鑰,使用新的主金鑰進行加密。在崩潰後,舊密碼將丟失;WrappedOldKey 是跨越邊界攜帶舊金鑰的唯一正確方式。在 UnlockDatabase 時,當存在 WAL 時,新的主金鑰會解密 WrappedOldKey,並從 WAL 游標恢復輪換。WAL 本身使用 policyEncKey 加密。

每個重要的操作都會將一個防篡改的事件附加到儲存桶的審計鏈中。鏈的完整性依賴於兩種機制。

檢查和。對 prevChecksum、ID、BucketID、Scheme、Namespace、EncDetails、EventType 和 Timestamp 計算 SHA-256。使用 Scheme / Namespace 作為明文(始終與加密形式一起保留)確保檢查和在不同載入路徑之間保持穩定。EncDetails 提供對加密負載的完整性。

HMAC。對包括 Seq 在內的所有欄位計算 HMAC-SHA256。能夠寫入資料庫但不知道審計金鑰的攻擊者無法產生有效的 HMAC。VerifyIntegrity 會針對每個事件檢查這兩個層級。

金鑰輪換時段邊界。在 Rotate 時,一個檢查點事件會附加到每個活動鏈中,攜帶進出審計金鑰的指紋。檢查點使用出站金鑰進行簽署。持有任何時段金鑰的審計員可以從 wrapped_new_key 欄位恢復後續時段金鑰,並驗證整個鏈的 HMAC 連續性。

自動修剪。當在 Config 中設定了 AuditPruneInterval 時,jack.Scheduler 會定期運行並對每個已註冊的儲存桶呼叫 PruneEvents。LevelHSM 和 LevelRemote 儲存桶無論此設定如何都不會被修剪。

Jack 是一個可選的進程監督庫。當通過 WithJack 提供 JackConfig 時,keeper 會自動啟用背景組件:

如果未提供 JackConfig,keeper 將在沒有這些背景任務的情況下運行。Keeper 絕不會呼叫 pool.Shutdown——pool 的生命週期屬於呼叫者。

x/keepcmd 提供可重用的 keeper 操作,與任何 CLI 框架解耦。將其嵌入您自己的應用程式中,即可獲得類型化、可測試的秘密管理功能,而無需引入 CLI 二進位檔。

keepcmd 絕不會呼叫 prompter 或從 stdin 讀取。密碼解析完全由呼叫者負責——這使得該套件在無頭伺服器環境中更加安全。

NoClose: true 可防止 Commands 在每次操作後呼叫 store.Close()。在 REPL / 會話環境中使用此選項,其中一個 store 會在多次呼叫之間共用。

x/keephandler 在任何 net/http mux 上掛載 keeper HTTP 端點。沒有外部路由依賴——它使用 Go 1.22+ 的方法+模式路由和標準庫 http.ServeMux。

BeforeFunc 返回 (allow bool, err error)。

Hook.CaptureBody bool 控制 AfterFunc 是否接收回應主體。false(預設)會產生一個輕量級的 statusWriter 包裝器;true 會將整個主體緩衝到 bytes.Buffer 中供 AfterFunc 使用——每個請求分配一次。

Hooks 按註冊順序執行。多次呼叫 WithHooks 是累加的。對於給定的路由名稱,只有第一個註冊的 hook 會被使用——同一路由的後續註冊將被忽略。

UnlockDatabase 按順序執行以下操作:

所有 sentinel 錯誤都與 errors.Is 和 errors.As 一起工作。堆疊追蹤在建立點通過 github.com/olekukonko/errors 捕獲。

ErrAuthFailed 統一了所有 UnlockBucket 失敗(CWE-204 / CVSS 5.3)。未知管理員 ID 和錯誤密碼都會返回 ErrAuthFailed。這可以防止通過時序或錯誤字串比較來枚舉管理員 ID。RevokeAdmin 保留 ErrAdminNotFound,因為它是對已解鎖儲存的管理操作。故意省略了管理員 ID 存在性的常數時間比較。能夠測量 bbolt 儲存桶查找的亞微秒差異的攻擊者需要本地檔案系統存取權——屆時他們可以直接讀取策略儲存桶。威脅模型假設資料庫檔案可能被破壞;針對遠端枚舉的時序防禦是主要考量。

Argon2id 主導了時序。Argon2id 在典型硬體上需要 200–500 毫秒。派生後比較差異要小四個或更多數量級,並且無法遠端測量。沒有應用人工均衡。

DEK 在 CAS 交易邊界內檢索。CompareAndSwapNamespacedFull 在 bbolt 寫入交易內檢索儲存桶 DEK,消除了在檢索和使用之間可能發生併發 Rotate 的窗口。

密碼永遠不會以 Go 字串形式儲存在 HTTP handler 中。所有三個密碼欄位(passphrase, new_passphrase)都通過原始映射提取,直接從 JSON 解碼為 []byte,將字串後備陣列保留在長期堆疊之外。[]byte 副本在使用後會通過 wipeBytes 歸零。

CLI 中沒有 --passphrase 標誌。標誌會出現在 ps 輸出和 shell 歷史記錄中。CLI 僅從 KEEPER_PASSPHRASE 環境變數或互動式無回顯提示中接受密碼。

REPL 中的秘密值永遠不可見。在 REPL 中使用 set <key> 而不帶內聯值時,會使用 term.ReadPassword——它不會出現在終端滾動、shell 歷史記錄或 ps 中。當方便時,可以為非敏感資料提供內聯值(set key value)。

SaltStore 故意未加密。KDF 鹽必須在 UnlockDatabase 之前可讀以派生主金鑰。policyEncKey(用於所有其他元資料加密)本身是從主金鑰派生的——使用 policyEncKey 加密鹽將形成循環。KDF 鹽提供唯一性,而不是保密性;加密它沒有安全價值。

策略儲存桶金鑰被雜湊,而不是明文。磁碟上的策略金鑰是 hex(SHA-256("scheme:namespace"))[:32]——128 位金鑰空間——而不是可讀字串。離線攻擊者讀取 bbolt 檔案,在解密策略 Blob 之前無法枚舉儲存桶名稱。

元資料加密使用與秘密相同的密文介面。所有 policyEncKey 和 auditEncKey 操作都通過 s.config.NewCipher(key)——與為秘密值配置的 crypt.Cipher 介面相同。使用者選擇的密文(FIPS 140 的 AES-256-GCM,預設為 XChaCha20-Poly1305)會自動應用於策略、WAL 和審計加密。沒有程式碼路徑會硬編碼特定的演算法。

LevelHSM 和 LevelRemote 儲存桶在主金鑰輪換期間被跳過。reencryptAllWithKey 和 RotateSalt 明確跳過這些儲存桶。DEK 由提供者控制;主鹽輪換不會影響它。

帶有 WrappedOldKey 的崩潰安全輪換。Rotate 在觸碰任何記錄之前會寫入一個 WAL。WAL 包含 WrappedOldKey:預輪換的主金鑰,使用新的主金鑰進行加密。崩潰後,UnlockDatabase 會使用已驗證的新金鑰解密 WrappedOldKey,並從游標恢復輪換。