
Simple gardien sécurisé de secrets
Keeper est un coffre-fort cryptographique pour Go. Il chiffre des données brutes arbitraires au repos en utilisant la dérivation de clé Argon2id et le chiffrement authentifié XChaCha20-Poly1305 (par défaut), et les stocke dans une base de données bbolt intégrée.
Il se décline en trois composants utilisables indépendamment :
x/keephandler) — montez des endpoints keeper sur n'importe quel
multiplexeur net/http en un seul appel, avec des hooks, des gardes et des encodeurs de réponse
enfichables pour le contrôle d'accès et la journalisation d'audit.cmd/keeper) — une interface terminal avec une session REPL persistante,
une saisie de secrets sans écho et une exposition zéro à l'historique du shell.Keeper a été conçu comme la couche de gestion de secrets fondamentale pour le répartiteur de charge Agbero mais n'a aucune dépendance envers Agbero et fonctionne dans tout projet Go.
Keeper partitionne les secrets en buckets. Chaque bucket possède une
BucketSecurityPolicy immuable qui régit la protection de sa clé de chiffrement de données (DEK).
Quatre niveaux sont disponibles.
Un schéma est un préfixe URI qui regroupe des buckets connexes (vault://, certs://,
space://, ou tout nom que vous enregistrez). Un niveau de sécurité est une propriété de la
politique du bucket définie à la création et immuable par la suite.
Vous pouvez librement mélanger les niveaux de sécurité au sein d'un même schéma. Par exemple,
vault://system pourrait être LevelPasswordOnly (déverrouillé automatiquement au démarrage), tandis que
vault://admin est LevelAdminWrapped (nécessite des identifiants explicites).
La DEK du bucket est dérivée de la clé maîtresse à l'aide de HKDF-SHA256 avec une
chaîne d'information séparée par domaine pour chaque bucket (keeper-bucket-dek-v1:scheme:namespace).
Tous les buckets LevelPasswordOnly sont déverrouillés automatiquement lorsque
UnlockDatabase est appelée avec la bonne phrase de passe maîtresse. Aucun identifiant
par bucket n'est nécessaire à l'exécution. Ce niveau convient aux secrets dont le processus
a besoin au démarrage sans intervention humaine.
Le bucket possède une DEK aléatoire de 32 octets unique à ce bucket. La DEK
n'est jamais stockée en clair. Pour chaque administrateur autorisé, une clé de chiffrement de clé (KEK)
est dérivée de HKDF(masterKey‖adminCred, dekSalt) et utilisée pour envelopper
la DEK via XChaCha20-Poly1305. Le bucket est inaccessible jusqu'à ce qu'un administrateur appelle
UnlockBucket avec ses identifiants. La phrase de passe maîtresse seule ne peut pas
déchiffrer le bucket. La révocation d'un administrateur n'affecte aucune copie enveloppée d'un autre
administrateur.
La DEK du bucket est générée lors de CreateBucket et immédiatement enveloppée par
un HSMProvider fourni par l'appelant. Le fournisseur effectue les opérations d'enveloppement et de
déballage — keeper ne manipule jamais la DEK brute après l'avoir transmise au fournisseur.
UnlockDatabase appelle automatiquement le fournisseur pour déballer et amorcer l'
Enveloppe pour tous les buckets HSM enregistrés. La rotation de la clé maîtresse ne
rechiffre pas ces buckets ; la DEK est contrôlée par le fournisseur.
Une implémentation intégrée SoftHSM basée sur une clé d'enveloppement protégée par memguard
est disponible dans pkg/hsm pour les tests et les environnements CI. Ne l'utilisez pas
en production.
Identique à LevelHSM en termes de comportement de gestion des clés, mais le HSMProvider est
implémenté par pkg/remote.Provider — un adaptateur HTTPS configurable qui
délègue l'enveloppement et le déballage à n'importe quel service KMS distant via TLS. Des
configurations pré-construites pour HashiCorp Vault Transit, AWS KMS et GCP Cloud KMS sont
fournies dans pkg/remote. Pour une utilisation en production, configurez TLSClientCert et
TLSClientKey pour activer l'authentification mutuelle 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
Un hash de vérification est stocké lors de la première dérivation :```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
Les appels ultérieurs à DeriveMaster recalculent ce hachage et le comparent avec crypto/subtle.ConstantTimeCompare. Une non-correspondance renvoie ErrInvalidPassphrase.
Le sel KDF est stocké non chiffré par conception. Il doit être lisible avant UnlockDatabase pour dériver la clé maîtresse — le chiffrer avec une clé dérivée de la clé maîtresse serait circulaire. Un sel KDF n'est pas un secret ; son but est l'unicité, pas la confidentialité.
Chaque valeur en clair est chiffrée avec XChaCha20-Poly1305 en utilisant la DEK du compartiment :``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
L'enregistrement stocké est une structure `Secret` encodée en msgpack contenant le texte chiffré, les métadonnées chiffrées et la version du schéma. L'authentification est implicite : un texte chiffré déchiffré avec la mauvaise clé produit une échec d'authentification AEAD avant qu'aucun texte en clair ne soit renvoyé.
### Dérivation de 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)
La KEK est dérivée à l'aide de HKDF plutôt que d'un second passage d'Argon2. La clé maîtresse a déjà été produite par une KDF à coût élevé ; une seconde invocation d'Argon2 ajouterait des centaines de millisecondes de latence à chaque appel UnlockBucket sans bénéfice de sécurité. HKDF-SHA256 opère en environ une microseconde.
Défense en profondeur : Un attaquant qui ne compromet que la base de données obtient la DEK enveloppée et le sel HKDF mais ne peut pas dériver la KEK sans la clé maîtresse. Un attaquant qui ne compromet que la clé maîtresse ne peut déchiffrer aucune DEK LevelAdminWrapped sans également connaître l'identifiant administrateur.
Les métadonnées secrètes (heure de création, heure de mise à jour, nombre d'accès, version) sont chiffrées séparément du texte chiffré :``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
Pour `LevelAdminWrapped`, `LevelHSM` et `LevelRemote` buckets, cela signifie que les métadonnées sont inaccessibles sans le credential du bucket, empêchant un attaquant ayant un accès en lecture au fichier de base de données d'apprendre les patterns d'accès ou les horodatages.