
Keeper は、Go 向けの暗号化シークレットストアです。Argon2id キー導出と XChaCha20-Poly1305(デフォルト)認証暗号化を使用して任意のバイトペイロードを保存時に暗号化し、埋め込み bbolt データベースに格納します。
単独で使用できる3つのコンポーネントとして提供されます。
x/keephandler) — 1回の呼び出しで任意の net/http マルチプレクサに keeper エンドポイントをマウント可能。アクセス制御と監査ログのためのプラガブルなフック、ガード、レスポンスエンコーダを備えています。cmd/keeper) — 永続的な REPL セッション、エコーなしのシークレット入力、シェル履歴への露出ゼロを備えたターミナルインターフェース。Keeper は Agbero ロードバランサの基礎シークレット管理層として設計されましたが、Agbero への依存はなく、あらゆる Go プロジェクトで動作します。
Keeper はシークレットをバケットに分割します。すべてのバケットには、そのデータ暗号鍵 (DEK) の保護方法を管理する不変の BucketSecurityPolicy があります。4つのレベルが利用可能です。
スキーム は関連するバケットをグループ化する URI プレフィックスです (vault://、certs://、space://、または任意の登録名)。セキュリティレベル は作成時に設定され、その後は不変となるバケットポリシーのプロパティです。
同じスキーム内でセキュリティレベルを自由に混在させることができます。たとえば、vault://system は LevelPasswordOnly(起動時に自動ロック解除)に、vault://admin は LevelAdminWrapped(明示的な認証情報が必要)に設定できます。
バケット DEK はマスター鍵から HKDF-SHA256 を使用して導出され、バケットごとにドメイン分離された情報文字列(keeper-bucket-dek-v1:scheme:namespace)が適用されます。すべての LevelPasswordOnly バケットは、正しいマスターパスフレーズで UnlockDatabase が呼び出されると自動的にロック解除されます。実行時にバケットごとの認証情報は必要ありません。このレベルは、プロセスが人間の操作なしで起動時に必要とするシークレットに適しています。
バケットには、そのバケット固有のランダムに生成された 32 バイトの DEK があります。DEK はプレーンテキストで保存されることはありません。認可された管理者ごとに、HKDF(masterKey‖adminCred, dekSalt) から鍵暗号鍵 (KEK) が導出され、XChaCha20-Poly1305 を使用して DEK をラップします。管理者が UnlockBucket を認証情報で呼び出すまで、バケットにはアクセスできません。マスターパスフレーズだけではバケットを復号できません。1人の管理者を無効にしても、他の管理者のラップされたコピーには影響しません。
バケット DEK は CreateBucket 時に生成され、呼び出し元が提供する HSMProvider によって即座にラップされます。プロバイダがラップおよびアンラップ操作を実行し、Keeper はプロバイダに DEK を渡した後は生の DEK を扱いません。UnlockDatabase は、登録されているすべての HSM バケットに対して、プロバイダを自動的に呼び出してラップ解除と Envelope へのシードを行います。マスター鍵のローテーションはこれらのバケットを再暗号化しません。DEK はプロバイダが管理します。
テストおよび CI 環境向けに、pkg/hsm で memguard 保護されたラッピング鍵に基づく組み込みの SoftHSM 実装が利用可能です。本番環境では使用しないでください。
鍵管理の動作は LevelHSM と同じですが、HSMProvider は pkg/remote.Provider(TLS 経由で任意のリモート KMS サービスにラップおよびアンラップを委任する設定可能な HTTPS アダプタ)によって実装されています。HashiCorp Vault Transit、AWS KMS、GCP Cloud KMS 向けの構築済み設定が pkg/remote で提供されています。本番環境では、相互 TLS 認証を有効にするために TLSClientCert と TLSClientKey を設定してください。
salt ← random 32 bytes, generated once, stored as a versioned SaltStore (unencrypted) masterKey ← Argon2id(passphrase, salt, t=3, m=64 MiB, p=4) → 32 bytes
初回の導出時に検証ハッシュが保存されます:```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
Subsequent DeriveMaster calls recompute this hash and compare it with
crypto/subtle.ConstantTimeCompare. A mismatch returns ErrInvalidPassphrase.
後続の DeriveMaster 呼び出しは、このハッシュを再計算し、crypto/subtle.ConstantTimeCompare と比較します。不一致があると、ErrInvalidPassphrase を返します。
The KDF salt is stored unencrypted by design. It must be readable before
UnlockDatabase to derive the master key — encrypting it with a key derived
from the master would be circular. A KDF salt is not a secret; its purpose is
uniqueness, not confidentiality.
KDFソルトは設計上暗号化されずに保存されます。マスターキーを導出するために UnlockDatabase の前に読み取り可能である必要があります — マスターから導出されたキーで暗号化することは循環的になります。KDFソルトは秘密ではなく、その目的は一意性であり、機密性ではありません。
Each plaintext value is encrypted with XChaCha20-Poly1305 using the bucket DEK: 各平文値は、バケットDEKを使用してXChaCha20-Poly1305で暗号化されます:``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
保存されたレコードはmsgpackでエンコードされた`Secret`構造体で、暗号文、暗号化されたメタデータ、およびスキーマバージョンを含んでいます。認証は暗黙的です: 間違ったキーで復号された暗号文は、平文が返される前にAEAD認証エラーを発生させます。
### KEK導出 — LevelAdminWrapped```
salt ← random 32 bytes, generated at bucket creation, stored in policy
ikm ← masterKey ‖ adminCredential
KEK ← HKDF-SHA256(ikm, salt, info="keeper-kek-v1") → 32 bytes
wrappedDEK ← XChaCha20-Poly1305.Seal(nonce, KEK, DEK)
KEKは、2回目のArgon2パスではなく、HKDFを使用して導出されます。マスターキーはすでに高コストのKDFで生成されています。2回目のArgon2呼び出しは、UnlockBucket呼び出しごとに数百ミリ秒のレイテンシを追加し、セキュリティ上の利点はありません。HKDF-SHA256は約1マイクロ秒で動作します。
多層防御: データベースのみを侵害した攻撃者は、ラップされたDEKとHKDFソルトを入手できますが、マスターキーなしではKEKを導出できません。マスターキーのみを侵害した攻撃者は、管理者の資格情報を知らなければ、LevelAdminWrapped DEKをアンラップできません。
シークレットメタデータ(作成時刻、更新時刻、アクセス回数、バージョン)は、暗号文とは別に暗号化されます。``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
`LevelAdminWrapped`、`LevelHSM`、`LevelRemote` バケットの場合、これはメタデータがバケットの認証情報なしではアクセスできないことを意味し、データベースファイルへの読み取りアクセス権を持つ攻撃者がアクセスパターンやタイムスタンプを取得するのを防ぎます。
**タイミングサイドチャネルに関する注意:** XChaCha20-Poly1305は、認証エラーを返す前に完全な暗号文を処理します。フォールバック復号パス(新しい派生DEK → 古いマスターキーをDEKとして使用)は、どちらのキーが成功するかにかかわらず、同じウォールクロック時間を要します。タイミングサイドチャネルによってレコードの移行状態が漏洩することはありません。
### メタデータ暗号化 — ポリシー、WAL、および監査
すべての構造的メタデータも保存時に暗号化されます。`UnlockDatabase` 時にマスターキーから2つのキーが導出されます:```
policyEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-enc-v1") → 32 bytes
auditEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-enc-v1") → 32 bytes
policyEncKey は以下を暗号化します: BucketSecurityPolicy の値とローテーション WAL。
auditEncKey は以下を暗号化します: すべての監査イベントの Scheme、Namespace、Details フィールド。
両方のキーは Lock() でメモリから消去されます。メタデータの暗号化に使用される暗号は、シークレットに使用される設定可能な同じ crypt.Cipher インターフェースです —
ユーザーが選択した暗号(FIPS の場合は AES-256-GCM、デフォルトは XChaCha20-Poly1305)が自動的に適用されます。
すべての暗号化されたメタデータブロブのワイヤ形式:``` nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
### ポリシーバケットキーのハッシュ化
ディスク上のポリシーキーは、平文の `scheme:namespace`
文字列ではなく不透明なハッシュであり、バケット名のオフライン列挙を防ぎます:```
base ← hex(SHA-256("scheme:namespace"))[:32] // 32 hex chars = 128-bit key space
_policies/<base> → encrypted BucketSecurityPolicy
_policies/<base>__hash__ → SHA-256(encrypted policy bytes)
_policies/<base>__hmac__ → HMAC-SHA256(policyKey, encrypted policy bytes)
メモリ内の schemeRegistry は引き続き "scheme:namespace" をキーとして使用します。ディスク上の表現のみが変更されます。
各ポリシーレコードには、1 つの bbolt トランザクション内でアトミックに書き込まれる 2 つの整合性タグが付与されます。``` hash ← SHA-256(encryptedPolicyBytes) — unauthenticated, pre-unlock integrity policyKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-hmac-v1") → 32 bytes hmac ← HMAC-SHA256(policyKey, encryptedPolicyBytes) — authenticated, post-unlock integrity
`UnlockDatabase`の前では、SHA-256ハッシュのみが利用可能です。アンロック後、`loadPolicy`がHMACタグを検証します。`UnlockDatabase`は、この機能が存在する前に作成されたポリシーに対してHMACタグをバックフィルするために`upgradePolicyHMACs`を呼び出します。
### 監査 HMAC署名```
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)
署名鍵は UnlockDatabase で有効化され、Lock でクリアされます。マスター鍵がローテートされると、Rotate はアクティブなすべての監査チェーンに鍵ローテーションのチェックポイントイベントを追加し、古い監査鍵で署名されたものが古いエポックの最後のイベントとなります。履歴が書き換えられることは決してなく、チェックポイントがエポック間の信頼の橋渡しとなります。