
Gestor Simple y Seguro de Secretos
Keeper es un almacén criptográfico de secretos para Go. Cifra cargas útiles de bytes arbitrarios en reposo usando derivación de clave Argon2id y cifrado autenticado XChaCha20-Poly1305 (predeterminado), y las almacena en una base de datos bbolt integrada.
Se distribuye como tres cosas que puedes usar de forma independiente:
x/keephandler) — monta los endpoints de keeper en cualquier mux net/http en una sola llamada, con ganchos, protectores y codificadores de respuesta enchufables para control de acceso y registro de auditoría.cmd/keeper) — una interfaz de terminal con una sesión REPL persistente, entrada de secretos sin eco y exposición cero del historial del shell.Keeper fue diseñado como la capa fundamental de gestión de secretos para el balanceador de carga Agbero pero no tiene dependencia de Agbero y funciona en cualquier proyecto Go.
Keeper particiona los secretos en buckets. Cada bucket tiene un BucketSecurityPolicy inmutable que gobierna cómo se protege su Clave de Cifrado de Datos (DEK). Hay cuatro niveles disponibles.
Un esquema es un prefijo URI que agrupa buckets relacionados (vault://, certs://, space:// o cualquier nombre que registres). Un nivel de seguridad es una propiedad de la política del bucket establecida en la creación e inmutable a partir de entonces.
Puedes mezclar niveles de seguridad libremente dentro del mismo esquema. Por ejemplo, vault://system podría ser LevelPasswordOnly (desbloqueado automáticamente al inicio), mientras que vault://admin es LevelAdminWrapped (requiere credencial explícita).
El DEK del bucket se deriva de la clave maestra usando HKDF-SHA256 con una cadena de información separada por dominio por bucket (keeper-bucket-dek-v1:scheme:namespace). Todos los buckets LevelPasswordOnly se desbloquean automáticamente cuando se llama a UnlockDatabase con la frase de contraseña maestra correcta. No se requiere ninguna credencial por bucket en tiempo de ejecución. Este nivel es apropiado para secretos que el proceso necesita al inicio sin interacción humana.
El bucket tiene un DEK de 32 bytes generado aleatoriamente único para ese bucket. El DEK nunca se almacena en texto plano. Para cada administrador autorizado, se deriva una Clave de Cifrado de Claves (KEK) de HKDF(masterKey‖adminCred, dekSalt) y se usa para envolver el DEK mediante XChaCha20-Poly1305. El bucket es inaccesible hasta que un administrador llama a UnlockBucket con su credencial. La frase de contraseña maestra por sí sola no puede descifrar el bucket. Revocar a un administrador no afecta la copia envuelta de ningún otro administrador.
El DEK del bucket se genera en el momento de CreateBucket y es envuelto inmediatamente por un HSMProvider proporcionado por el llamante. El proveedor realiza las operaciones de envoltura y desenvolvimiento — keeper nunca maneja el DEK en bruto después de entregarlo al proveedor. UnlockDatabase llama automáticamente al proveedor para desenvolver y sembrar el Envolvente para todos los buckets HSM registrados. La rotación de la clave maestra no recifra estos buckets; el DEK está controlado por el proveedor.
Una implementación SoftHSM integrada respaldada por una clave de envoltura protegida por memguard está disponible en pkg/hsm para entornos de prueba y CI. No la uses en producción.
Idéntico a LevelHSM en el comportamiento de gestión de claves, pero el HSMProvider es implementado por pkg/remote.Provider — un adaptador HTTPS configurable que delega la envoltura y desenvolvimiento a cualquier servicio KMS remoto sobre TLS. Se proporcionan configuraciones preconstruidas para HashiCorp Vault Transit, AWS KMS y GCP Cloud KMS en pkg/remote. Para uso en producción, configura TLSClientCert y TLSClientKey para habilitar la autenticación TLS mutua.
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
Se almacena un hash de verificación en la primera derivación:```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
Las llamadas posteriores a DeriveMaster recalculan este hash y lo comparan con
crypto/subtle.ConstantTimeCompare. Una discrepancia devuelve ErrInvalidPassphrase.
La sal KDF se almacena sin cifrar por diseño. Debe ser legible antes de
UnlockDatabase para derivar la clave maestra — cifrarla con una clave derivada
de la maestra sería circular. Una sal KDF no es un secreto; su propósito es la
unicidad, no la confidencialidad.
Cada valor de texto plano se cifra con XChaCha20-Poly1305 usando el DEK del bucket:``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
The stored record is a msgpack-encoded `Secret` struct containing the ciphertext, encrypted metadata, and schema version. Authentication is implicit: a ciphertext decrypted with the wrong key produces an AEAD authentication failure before any plaintext is returned.
### KEK derivation — 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)
La KEK se deriva usando HKDF en lugar de una segunda pasada de Argon2. La clave maestra ya fue producida por un KDF de alto costo; una segunda invocación de Argon2 agregaría cientos de milisegundos de latencia a cada llamada UnlockBucket sin beneficio de seguridad. HKDF-SHA256 opera en aproximadamente un microsegundo.
Defensa en profundidad: Un atacante que comprometa solo la base de datos obtiene el DEK envuelto y la sal HKDF pero no puede derivar la KEK sin la clave maestra. Un atacante que comprometa solo la clave maestra no puede desenvolver ningún DEK LevelAdminWrapped sin conocer también la credencial de administrador.
Los metadatos secretos (tiempo de creación, tiempo de actualización, recuento de accesos, versión) se cifran por separado del texto cifrado:``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
Para los buckets `LevelAdminWrapped`, `LevelHSM` y `LevelRemote`, esto significa que los metadatos son inaccesibles sin la credencial del bucket, evitando que un atacante con acceso de lectura al archivo de la base de datos aprenda patrones de acceso o marcas de tiempo.
**Nota sobre canal lateral de temporización:** XChaCha20-Poly1305 procesa el texto cifrado completo antes de devolver un error de autenticación. La ruta de descifrado de respaldo (nueva DEK derivada → antigua clave maestra como DEK) toma el mismo tiempo de reloj independientemente de qué clave tenga éxito. Ningún canal lateral de temporización filtra el estado de migración de un registro.