Keeper 是一个用于 Go 的加密秘密存储器。它使用 Argon2id 密钥派生和 XChaCha20-Poly1305(默认)认证加密来加密静态的任意字节负载,并将它们存储在嵌入式 bbolt 数据库中。
它打包成三个独立可用的部分:
x/keephandler) — 通过一次调用将 keeper 端点挂载到任何 net/http 多路复用器上,并带有可插拔的钩子、守卫和响应编码器,用于访问控制和审计日志记录。cmd/keeper) — 一个终端界面,具有持久 REPL 会话、无回显秘密输入和零 shell 历史暴露。Keeper 被设计为 Agbero 负载均衡器的基础秘密管理层,但独立于 Agbero,适用于任何 Go 项目。
Keeper 将秘密分区到桶中。每个桶都有一个不可变的 BucketSecurityPolicy,用于管理其数据加密密钥 (DEK) 的保护方式。提供四个级别。
方案是一个 URI 前缀,用于对相关桶进行分组(vault://、certs://、space:// 或您注册的任何名称)。安全级别是桶策略的一个属性,在创建时设置且此后不可更改。
您可以在同一方案内自由混合安全级别。例如,vault://system 可能是 LevelPasswordOnly(启动时自动解锁),而 vault://admin 是 LevelAdminWrapped(需要明确凭据)。
桶的 DEK 是使用 HKDF-SHA256 从主密钥派生的,带有每个桶的域分离信息字符串(keeper-bucket-dek-v1:scheme:namespace)。当使用正确的主密码调用 UnlockDatabase 时,所有 LevelPasswordOnly 桶都会自动解锁。运行时不需要每个桶的凭据。此级别适用于进程在启动时需要且无需人工交互的秘密。
桶有一个随机生成的 32 字节 DEK,对该桶唯一。DEK 永远不以明文形式存储。对于每个授权管理员,从 HKDF(masterKey‖adminCred, dekSalt) 派生一个密钥加密密钥 (KEK),并用于通过 XChaCha20-Poly1305 封装 DEK。在管理员使用其凭据调用 UnlockBucket 之前,该桶不可访问。仅凭主密码无法解密该桶。撤销一个管理员不会影响任何其他管理员的封装副本。
桶的 DEK 在 CreateBucket 时生成,并立即由调用者提供的 HSMProvider 封装。该提供程序执行封装和解封装操作 —— keeper 在将 DEK 交给提供程序后永远不会处理原始 DEK。UnlockDatabase 自动调用提供程序为所有注册的 HSM 桶解封装并播种信封。主密钥轮换不会重新加密这些桶;DEK 由提供程序控制。
一个内置的 SoftHSM 实现(由 memguard 保护的封装密钥支持)位于 pkg/hsm 中,用于测试和 CI 环境。不要在生产中使用它。
在密钥管理行为上与 LevelHSM 相同,但 HSMProvider 由 pkg/remote.Provider 实现 —— 一个可配置的 HTTPS 适配器,通过 TLS 将封装和解封装委托给任何远程 KMS 服务。预构建的配置适用于 HashiCorp Vault Transit、AWS KMS 和 GCP Cloud KMS,位于 pkg/remote 中。对于生产用途,配置 TLSClientCert 和 TLSClientKey 以启用双向 TLS 认证。
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
后续的 DeriveMaster 调用重新计算此哈希值,并与 crypto/subtle.ConstantTimeCompare 进行比较。如果匹配失败,则返回 ErrInvalidPassphrase。
KDF salt 按设计是不加密存储的。它必须在 UnlockDatabase 之前可读,以便派生主密钥——使用从主密钥派生的密钥对其进行加密将导致循环。KDF salt 不是一个秘密;它的目的是唯一性,而非机密性。
每个明文值使用 XChaCha20-Poly1305 通过 bucket DEK 进行加密:``` 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 是使用 HKDF 而非第二次 Argon2 传递来派生的。主密钥已经由高成本 KDF 生成;第二次调用 Argon2 会给每次 UnlockBucket 调用增加数百毫秒的延迟,且无安全收益。HKDF-SHA256 大约在一微秒内完成操作。
纵深防御: 仅攻破数据库的攻击者能获取包装后的 DEK 和 HKDF 盐值,但若无主密钥则无法派生 KEK。仅攻破主密钥的攻击者无法解包任何 LevelAdminWrapped DEK,除非同时知晓管理员凭证。
秘密元数据(创建时间、更新时间、访问次数、版本)与密文分开加密:``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
For `LevelAdminWrapped`, `LevelHSM`, and `LevelRemote` buckets this means
metadata is inaccessible without the bucket credential, preventing an attacker
with read access to the database file from learning access patterns or
timestamps.
对于 `LevelAdminWrapped`、`LevelHSM` 和 `LevelRemote` 存储桶,这意味着没有存储桶凭证就无法访问元数据,从而阻止了对数据库文件具有读访问权限的攻击者了解访问模式或时间戳。
**Timing side-channel note:** XChaCha20-Poly1305 processes the full ciphertext
before returning an authentication error. The fallback decrypt path (new
derived DEK → old master-key-as-DEK) takes the same wall-clock time regardless
of which key succeeds. No timing side-channel leaks the migration state of a
record.
**时序侧信道说明:** XChaCha20-Poly1305 在处理完整个密文后才返回认证错误。回退解密路径(新派生的 DEK → 旧主密钥作为 DEK)无论哪个密钥成功都消耗相同的挂钟时间。没有时序侧信道泄露记录的迁移状态。
### Metadata encryption — policies, WAL, and audit
### 元数据加密 — 策略、WAL 和审计
All structural metadata is also encrypted at rest. Two keys are derived from
the master key at `UnlockDatabase` time:
所有结构元数据也处于静态加密状态。在 `UnlockDatabase` 时从主密钥派生两个密钥:```
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" 作为其
键——只有磁盘上的表示形式发生变化。
每条策略记录携带两个完整性标签,在一个 bbolt 事务中原子性地写入:``` 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
### 审计 HMAC 签名
在 `UnlockDatabase` 之前,仅有 SHA-256 哈希可用。
解锁后,`loadPolicy` 验证 HMAC 标签。
`UnlockDatabase` 调用 `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 会向每个活动审计链附加一个密钥轮换检查点事件,使用旧审计密钥签名,作为旧周期(epoch)的最后一个事件。历史永远不会被重写;检查点是周期之间的信任桥梁。