
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 はアクティブなすべての監査チェーンに鍵ローテーションのチェックポイントイベントを追加し、古い監査鍵で署名されたものが古いエポックの最後のイベントとなります。履歴が書き換えられることは決してなく、チェックポイントがエポック間の信頼の橋渡しとなります。
passphrase │ └─ Argon2id(salt) ──→ masterKey (32 bytes, memguard Enclave) │ ├─ HKDF("keeper-audit-hmac-v1") ──→ auditKey (HMAC signing) ├─ HKDF("keeper-audit-enc-v1") ──→ auditEncKey (audit field encryption) ├─ HKDF("keeper-policy-hmac-v1") ──→ policyKey (policy HMAC) ├─ HKDF("keeper-policy-enc-v1") ──→ policyEncKey (policy/WAL encryption) │ ├─ [LevelPasswordOnly] │ └─ HKDF("keeper-bucket-dek-v1:scheme:ns") ──→ DEK │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ ├─ [LevelAdminWrapped] │ ├─ random 32 bytes ──→ DEK │ │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ │ │ └─ HKDF("keeper-kek-v1", masterKey‖adminCred, dekSalt) │ └─ KEK │ └─ XChaCha20-Poly1305(KEK, DEK) ──→ wrappedDEK │ └─ [LevelHSM / LevelRemote] ├─ random 32 bytes ──→ DEK │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ └─ HSMProvider.WrapDEK(DEK) ──→ wrappedDEK (stored; provider controls the wrapping key)
すべての中間キーは使用後すぐにゼロクリアされます。マスターキーは決してディスクに書き込まれません。
---
## ストレージスキーマ
基盤となるデータベースはbboltです。すべてのバケットとその内容:
| bbolt バケット | キー | 値 |
|---|---|---|
| `__meta__` | `salt` | msgpack — SaltStore (暗号化なし; 暗号化した場合の循環依存) |
| `__meta__` | `verify` | raw bytes — Argon2id検証ハッシュ |
| `__meta__` | `rotation_wal` | `nonce‖AEAD(msgpack(RotationWAL))` |
| `__meta__` | `bucket_dek_done` | `"1"` — DEK移行完了マーカー |
| `__policies__` | `hex(SHA-256(scheme:ns))[:32]` | `nonce‖AEAD(msgpack(BucketSecurityPolicy))` |
| `__policies__` | `<base>__hash__` | hex SHA-256 of encrypted policy bytes |
| `__policies__` | `<base>__hmac__` | hex HMAC-SHA256(policyKey, encrypted policy bytes) |
| `__audit__/scheme/namespace` | event UUID | JSON — 監査イベント |
| `__audit__/scheme/namespace` | `__chain_index__` | JSON — chainIndex |
| `scheme/namespace` | キー文字列 | msgpack — Secret 構造体 |
### Secret 構造体 (msgpack)```go
type Secret struct {
Ciphertext []byte `msgpack:"ct"`
EncryptedMeta []byte `msgpack:"em,omitempty"`
SchemaVersion int `msgpack:"sv"` // always 1
}
Event構造体は、平文のルーティングフィールド(Scheme、Namespace)と暗号化されたペイロードフィールド(EncScheme、EncNamespace、EncDetails)を併用します。チェックサムは平文ルーティングフィールドと暗号化されたEncDetailsバイトに対して計算されるため、チェーンの整合性はキーなしで3つの階層で検証できます。
| 階層 | 保持 | 検証可能 |
|---|---|---|
| 公開 | なし | SHA-256チェックサムチェーン(改ざんと挿入を検出) |
| 監査キー保持者 | auditEncKey | 完全なチェーン + Scheme/Namespace/Detailsの復号 |
| オペレーター | マスターパスフレーズ | すべて |
例: コンプライアンス監査人はauditEncKeyのみを受け取ります。キーローテーション全体で完全なHMACチェーンを検証し、すべてのイベントの詳細を読むことができますが、秘密の値を復号することはできません。データベースファイルのみを持つ公開観測者は、イベントが後から改ざんまたは挿入されたかどうかを検出できます。
KDFソルトは、saltメタデータキーの下にmsgpackエンコードされたSaltStoreとして保存されます。ソルトのローテーションごとに新しいSaltEntryが追加され、CurrentVersionが進みます。古いエントリは監査証跡として保持されます。SaltStoreは暗号化されずに保存されます — セキュリティの決定を参照。
Rotateは、レコードに触れる前にWALを書き込みます。WALにはWrappedOldKey(新しいマスターキーで暗号化されたローテーション前のマスターキー)が含まれます。クラッシュ後、古いパスフレーズは失われます。WrappedOldKeyは、古いキーを境界を越えて運ぶ唯一の正しい方法です。UnlockDatabaseで、WALが存在する場合、新しいマスターキーがWrappedOldKeyを復号し、WALカーソルからローテーションを再開します。WAL自体はpolicyEncKeyで暗号化されています。
すべての重要な操作は、バケットの監査チェーンに改ざん検出可能なイベントを追加します。チェーン整合性は2つのメカニズムに依存します。
チェックサム. 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は自動的にバックグラウンドコンポーネントをアクティブにします。
AutoLockInterval後にLevelAdminWrappedバケットのDEKを削除します。LevelPasswordOnlyバケットはロック解除されたままなので、バックグラウンドジョブは中断されずに続行します。ルーパータスク内の単一書き込みロックパターンにより、以前の設計にあったRUnlock→Lockの競合状態を排除します。LevelAdminWrapped DEKのTTLベースの有効期限。jack.Doctorに登録。PruneEvents。JackConfigが提供されない場合、keeperはこれらのバックグラウンドタスクなしで実行されます。Keeperは決してpool.Shutdownを呼び出しません — プールのライフサイクルは呼び出し元に属します。
x/keepcmdは、CLIフレームワークから切り離された再利用可能なkeeper操作を提供します。独自のアプリケーションに埋め込むことで、CLIバイナリを取り込まずに型付けされたテスト可能なシークレット管理を実現します。```go
import "github.com/agberohq/keeper/x/keepcmd"
cmds := &keepcmd.Commands{ Store: func() (*keeper.Keeper, error) { return security.KeeperOpen(cfg) // your own config }, Out: keepcmd.PlainOutput{}, NoClose: false, // true in REPL / session contexts }
cmds.List() // all keys: scheme://namespace/key cmds.List("vault") // all keys in scheme vault cmds.List("vault", "system") // all keys in vault://system cmds.Get("vault://system/jwt_secret") cmds.Set("vault://system/jwt_secret", "newsecret", keepcmd.SetOptions{}) cmds.Rotate(newPassphraseBytes) // caller resolved the passphrase — no prompter dependency cmds.RotateSalt(currentPassBytes) // same
`keepcmd`は`prompter`を呼び出したり、標準入力から読み取ったりしません。パスフレーズの解決は完全に呼び出し元の責任です。これにより、ヘッドレスサーバー環境でもパッケージの安全性が保たれます。
`NoClose: true`を設定すると、各操作後に`Commands`が`store.Close()`を呼び出すのを防ぎます。これは、単一のストアが複数の呼び出し間で共有されるREPLやセッションコンテキストで使用します。
---
## x/keephandler
`x/keephandler`は、任意の`net/http` mux上にkeeper HTTPエンドポイントをマウントします。外部のルーター依存関係は不要で、Go 1.22以降のメソッド+パターンルーティングをstdlibの`http.ServeMux`で使用します。```go
import "github.com/agberohq/keeper/x/keephandler"
keephandler.Mount(mux, store,
keephandler.WithPrefix("/api/keeper"),
keephandler.WithGuard(func(w http.ResponseWriter, r *http.Request, route string) bool {
if !acl.Allow(r.Header.Get("X-Principal"), route) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return false
}
return true
}),
keephandler.WithHooks(
keephandler.Hook{
Route: keephandler.RouteGet,
CaptureBody: false,
After: func(r *http.Request, status int, _ []byte) {
audit.Log(r.Context(), route, status)
},
},
),
keephandler.WithEncoder(func(w http.ResponseWriter, route string, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]any{
"ok": status < 400,
"route": route,
"data": data,
})
}),
keephandler.WithRoutes(func(m *http.ServeMux) {
m.HandleFunc("POST /api/keeper/totp/{user}", myTOTPHandler)
}),
)
BeforeFunc は (allow bool, err error) を返します。
(true, nil) — リクエストを続行させる。(false, nil) — 中断する;フックは既に完全なレスポンスを書き込んでいる。(false, err) — 中断する;フレームワークは err.Error() を使用して 500 を書き込む。
フックは w に何も書き込んではいけません。Hook.CaptureBody bool は、AfterFunc がレスポンスボディを受け取るかどうかを制御します。false(デフォルト)は軽量な statusWriter ラッパーを1つ消費します;true は完全なボディを bytes.Buffer にバッファリングして AfterFunc に渡します — リクエストごとに1回の割り当てです。
フックは登録順に実行されます。複数の WithHooks 呼び出しは加算されます。特定のルート名に対して最初に登録されたフックのみが使用され — 同じルートへの後続の登録は無視されます。
store, err := keeper.New(keeper.Config{ DBPath: "/var/lib/agbero/keeper.db", AutoLockInterval: 30 * time.Minute, EnableAudit: true, AuditPruneInterval: 24 * time.Hour, AuditPruneKeepLastN: 10_000, AuditPruneOlderThan: 90 * 24 * time.Hour, DBLatencyThreshold: 200 * time.Millisecond, Logger: logger, }, keeper.WithJack(keeper.JackConfig{ Pool: jackPool, Shutdown: jackShutdown, })) defer store.Close()
// Shorthand (wraps DeriveMaster + UnlockDatabase): if err := store.Unlock([]byte(os.Getenv("KEEPER_PASSPHRASE"))); err != nil { log.Fatal(err) // ErrInvalidPassphrase on wrong passphrase }
`UnlockDatabase`は以下の順序で実行します:
1. 監査HMAC署名鍵を導出し、アクティブ化する
2. ポリシーHMAC鍵を導出し、アクティブ化する
3. `policyEncKey`と`auditEncKey`を導出し、アクティブ化する
4. `schemeRegistry`をクリアして再ロードする(すべてのポリシーブロブを復号する)
5. 中断されたローテーションWALを再開する
6. ポリシーHMACタグをアップグレードする
7. すべての`LevelPasswordOnly`バケットDEKをEnvelopeにシードする
8. バックグラウンドタスクを開始する(マイグレーションルーパー、自動ロック、ヘルスパシエント)
### LevelPasswordOnlyバケット — 完全なライフサイクル```go
err := store.CreateBucket("vault", "system", keeper.LevelPasswordOnly, "init")
store.Set("vault://system/jwt_secret", []byte("supersecret"))
val, err := store.Get("vault://system/jwt_secret")
// Namespaced convenience wrappers
store.SetNamespaced("admin", "jwt_secret", secretBytes)
val, err = store.GetNamespaced("admin", "jwt_secret")
err := store.CreateBucket("finance", "payroll", keeper.LevelAdminWrapped, "ops-team") err = store.AddAdminToPolicy("finance", "payroll", "alice", []byte("alicepass"))
store.SetNamespacedFull("finance", "payroll", "salary_key", []byte("AES256..."))
store.LockBucket("finance", "payroll") err = store.UnlockBucket("finance", "payroll", "bob", []byte("bobpass")) // ErrAuthFailed — does not distinguish wrong password from unknown admin (CWE-204)
err = store.RevokeAdmin("finance", "payroll", "alice") err = store.RotateAdminWrappedDEK("finance", "payroll", "bob", []byte("bobpass"))
needs, err := store.NeedsAdminRekey("finance", "payroll")
### LevelHSM / LevelRemote バケット```go
import (
"github.com/agberohq/keeper/pkg/hsm"
"github.com/agberohq/keeper/pkg/remote"
)
// SoftHSM — testing only
provider, _ := hsm.NewSoftHSM()
store.RegisterHSMProvider("secure", "keys", provider)
store.CreateBucket("secure", "keys", keeper.LevelHSM, "ops")
// Vault Transit
cfg := remote.VaultTransit("https://vault.corp:8200", vaultToken, "my-key")
cfg.TLSClientCert = "/etc/keeper/client.crt"
cfg.TLSClientKey = "/etc/keeper/client.key"
provider, _ = remote.New(cfg)
store.RegisterHSMProvider("tenant", "secrets", provider)
store.CreateBucket("tenant", "secrets", keeper.LevelRemote, "ops")
// Export the audit encryption key to allow a third-party auditor to decrypt // event details without access to the master passphrase. auditKey, err := store.ExportAuditKey() defer zero.Bytes(auditKey)
events, err := auditStore.LoadChain("vault", "system", auditKey)
### 鍵のローテーション```go
// Rotate passphrase — crash-safe WAL, resumes on next Unlock if interrupted
store.Rotate([]byte("new-passphrase"))
// Rotate KDF salt — re-derives master key, re-encrypts LevelPasswordOnly
store.RotateSalt([]byte("current-passphrase"))
err := store.CompareAndSwapNamespacedFull("vault", "system", "counter", []byte("old"), []byte("new")) // ErrCASConflict if current value does not match old
### バックアップ```go
f, _ := os.Create("keeper.db.bak")
info, err := store.Backup(f)
// info.Bytes, info.Timestamp, info.DBPath
すべてのセンチネルエラーは 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ミリ秒かかります。派生後の比較の差異は4桁以上小さく、リモートからは測定できません。人為的な均等化は適用されません。
DEK は CAS トランザクション境界内で取得されます。 CompareAndSwapNamespacedFull は bbolt 書き込みトランザクション内でバケットDEKを取得し、同時実行の Rotate が取得と使用の間にDEKを変更できるウィンドウを排除します。
パスフレーズは HTTP ハンドラ内で Go の文字列として決して保存されません。 3つのパスフレーズフィールド (passphrase、new_passphrase) はすべて、JSON から raw-map 抽出を介して直接 []byte にデコードされ、文字列バッキング配列を長期生存ヒープから遠ざけます。[]byte のコピーは使用後に wipeBytes でゼロ化されます。
CLI に --passphrase フラグはありません。 フラグは ps 出力やシェル履歴に表示されます。CLI は KEEPER_PASSPHRASE 環境変数またはインタラクティブなエコーなしプロンプトからのみパスフレーズを受け入れます。
REPL のシークレット値は決して表示されません。 REPL でインライン値なしの set <key> は term.ReadPassword を使用します。ターミナルのスクロールバック、シェル履歴、ps には表示されません。インライン値 (set key value) は、機密性の低いデータに対して便宜上指定できます。
SaltStore は意図的に暗号化されていません。 KDFソルトは UnlockDatabase の前に読み取り可能である必要があり、マスターキーを導出します。policyEncKey (他のすべてのメタデータ暗号化に使用される) はそれ自体マスターキーから派生します。ソルトを policyEncKey で暗号化することは循環的になります。KDFソルトは一意性を提供し、機密性は提供しません。ソルトを暗号化するセキュリティ上の価値はありません。
ポリシーバケットキーはハッシュ化されており、平文ではありません。 ディスク上のポリシーキーは hex(SHA-256("scheme:namespace"))[:32] — 128ビットのキースペース — であり、読み取り可能な文字列ではありません。bboltファイルを読むオフライン攻撃者は、ポリシーブロブを復号せずにバケット名を列挙できません。
メタデータ暗号化はシークレットと同じ暗号インターフェースを使用します。 すべての 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 を復号し、カーソルからローテーションを再開します。
| Method | Path | Description |
|---|
POST | {prefix}/unlock | パスフレーズでストアをアンロック |
POST | {prefix}/lock | ストアをロック |
GET | {prefix}/status | ロック状態 — 認証なしでポーリングしても安全 |
GET | {prefix}/keys | 全てのシークレットキーを一覧表示 |
GET | {prefix}/keys/{key} | シークレット値を取得 |
POST | {prefix}/keys | シークレットを保存(JSON または multipart) |
DELETE | {prefix}/keys/{key} | シークレットを削除 |
POST | {prefix}/rotate | マスターパスフレーズをローテーション |
POST | {prefix}/rotate/salt | KDF ソルトをローテーション |
GET | {prefix}/backup | データベーススナップショットをストリーム |
| エラー | 意味 |
|---|
ErrStoreLocked | ストアがロックされているときに操作が試行された |
ErrInvalidPassphrase | マスターパスフレーズが間違っている |
ErrAuthFailed | あらゆる UnlockBucket の失敗 — 間違ったパスワードと不明な管理者IDを区別しない (CWE-204) |
ErrKeyNotFound | シークレットキーが存在しない |
ErrBucketLocked | バケットがロック解除されていない |
ErrPolicyImmutable | 既存のバケットに対する2つ目のポリシー |
ErrPolicyNotFound | 指定されたスキーム/名前空間に対するポリシーがない |
ErrAdminNotFound | 管理者IDがポリシーに存在しない — RevokeAdmin のみ |
ErrHSMProviderNil | 登録されたプロバイダなしでHSM/リモートバケットが作成された |
ErrCheckLatency | DB読み取りレイテンシが DBLatencyThreshold を超えた |
ErrCASConflict | CompareAndSwap で現在の値が期待値と一致しない |
ErrSecurityDowngrade | セキュリティレベルの高いバケットから低いバケットへのクロスバケット移動 |
ErrAlreadyUnlocked | すでにロック解除されているストアで UnlockDatabase が呼び出された |
ErrMasterRequired | nilまたは破棄された Master で UnlockDatabase が呼び出された |
ErrChainBroken | 監査チェーン整合性検証に失敗した |
ErrMetadataDecrypt | 暗号化されたメタデータを復号できなかった |
ErrPolicySignature | ポリシーHMAC検証に失敗した — レコードが改ざんされた |
| パッケージ | 目的 |
|---|
go.etcd.io/bbolt | 組み込みキーバリューストア |
golang.org/x/crypto | Argon2id、XChaCha20-Poly1305、HKDF、scrypt |
github.com/awnumar/memguard | メモリセーフなキーエンクレーブ (マスターキー、DEK) |
github.com/vmihailenco/msgpack/v5 | シークレットとポリシーのバイナリシリアライズ |
github.com/olekukonko/jack | プロセス監視 (オプションのJack統合) |
github.com/olekukonko/ll | 構造化ロギング |
github.com/olekukonko/errors | スタックトレース付きセンチネルエラー |
github.com/olekukonko/zero | 安全なバイトスライスのゼロ化 |
github.com/olekukonko/prompter | エコーなしターミナルプロンプト (CLIのみ) |
github.com/integrii/flaggy | CLIフラグパース (cmd/keeperのみ) |
golang.org/x/term | TTY検出と生パスワード読み取り (CLIのみ) |