
Simple Secure Keeper for Secrets
Keeper ist ein kryptografischer Geheimnisspeicher für Go. Er verschlüsselt beliebige Byte- Nutzlasten im Ruhezustand mittels Argon2id-Schlüsselableitung und XChaCha20-Poly1305 (Standard) authentifizierter Verschlüsselung und speichert sie in einer eingebetteten bbolt-Datenbank.
Er wird in drei unabhängig nutzbaren Komponenten ausgeliefert:
x/keephandler) — binde Keeper-Endpunkte in einem Aufruf an einen
beliebigen net/http-Mux, mit steckbaren Hooks, Guards und Response-
Encodern für Zugriffskontrolle und Audit-Logging.cmd/keeper) — ein Terminal-Interface mit persistenter REPL-Sitzung,
geheimnisvoller (echo-freier) Eingabe und keiner Shell-Verlaufsbelastung.Keeper wurde als grundlegende Geheimnisverwaltungsschicht für den Agbero-Lastenausgleicher entworfen, hat aber keine Abhängigkeit von Agbero und funktioniert in jedem Go-Projekt.
Keeper unterteilt Geheimnisse in Buckets. Jeder Bucket hat eine unveränderliche
BucketSecurityPolicy, die festlegt, wie sein Datenverschlüsselungsschlüssel (DEK) geschützt wird.
Vier Stufen sind verfügbar.
Ein Schema ist ein URI-Präfix, das zusammengehörige Buckets gruppiert (vault://, certs://,
space:// oder ein beliebiger von dir registrierter Name). Eine Sicherheitsstufe ist eine Eigenschaft der
Bucket-Richtlinie, die bei der Erstellung festgelegt wird und danach unveränderlich ist.
Du kannst Sicherheitsstufen innerhalb desselben Schemas frei mischen. Zum Beispiel
könnte vault://system LevelPasswordOnly sein (beim Start automatisch entsperrt), während
vault://admin LevelAdminWrapped ist (erfordert explizite Anmeldeinformationen).
Der Bucket-DEK wird aus dem Masterschlüssel mit HKDF-SHA256 und einem
domänenseparierten Info-String pro Bucket (keeper-bucket-dek-v1:scheme:namespace) abgeleitet.
Alle LevelPasswordOnly-Buckets werden automatisch entsperrt, wenn
UnlockDatabase mit der korrekten Master-Passphrase aufgerufen wird. Es sind keine
pro-Bucket-Anmeldeinformationen zur Laufzeit erforderlich. Diese Stufe eignet sich für Geheimnisse, die der
Prozess beim Start ohne menschliche Interaktion benötigt.
Der Bucket hat einen zufällig generierten 32-Byte-DEK, der für diesen Bucket eindeutig ist. Der DEK
wird niemals im Klartext gespeichert. Für jeden autorisierten Administrator wird ein Schlüsselverschlüsselungsschlüssel
(KEK) aus HKDF(masterKey‖adminCred, dekSalt) abgeleitet und verwendet, um
den DEK mittels XChaCha20-Poly1305 zu verpacken. Der Bucket ist unzugänglich, bis ein Administrator
UnlockBucket mit seinen Anmeldeinformationen aufruft. Die Master-Passphrase allein kann
den Bucket nicht entschlüsseln. Das Widerrufen eines Administrators hat keine Auswirkungen auf die verpackte Kopie
eines anderen Administrators.
Der Bucket-DEK wird zum Zeitpunkt von CreateBucket generiert und sofort von einem
aufruferseitig bereitgestellten HSMProvider verpackt. Der Provider führt die Ver- und Entpackoperationen
durch — Keeper hat nach der Übergabe an den Provider nie Zugriff auf den rohen DEK.
UnlockDatabase ruft automatisch den Provider auf, um das
Envelope für alle registrierten HSM-Buckets zu entpacken und zu initialisieren. Die Rotation des Masterschlüssels
verschlüsselt diese Buckets nicht neu; der DEK wird vom Provider kontrolliert.
Eine integrierte SoftHSM-Implementierung, die von einem memguard-geschützten Verpackungsschlüssel
unterstützt wird, ist in pkg/hsm für Test- und CI-Umgebungen verfügbar. Verwende sie nicht
in der Produktion.
Identisch zu LevelHSM im Schlüsselverwaltungsverhalten, aber der HSMProvider wird
von pkg/remote.Provider implementiert — einem konfigurierbaren HTTPS-Adapter, der
Ver- und Entpacken an jeden entfernten KMS-Dienst über TLS delegiert. Vorgefertigte
Konfigurationen für HashiCorp Vault Transit, AWS KMS und GCP Cloud KMS sind
in pkg/remote enthalten. Für den Produktionseinsatz konfiguriere TLSClientCert und
TLSClientKey, um gegenseitige TLS-Authentifizierung zu aktivieren.
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
Ein Verifikations-Hash wird bei der ersten Ableitung gespeichert:```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
Nachfolgende DeriveMaster-Aufrufe berechnen diesen Hash neu und vergleichen ihn mit crypto/subtle.ConstantTimeCompare. Bei einer Nichtübereinstimmung wird ErrInvalidPassphrase zurückgegeben.
Das KDF-Salz wird absichtlich unverschlüsselt gespeichert. Es muss vor UnlockDatabase lesbar sein, um den Masterschlüssel abzuleiten — eine Verschlüsselung mit einem vom Master abgeleiteten Schlüssel wäre zirkulär. Ein KDF-Salz ist kein Geheimnis; sein Zweck ist Einzigartigkeit, nicht Vertraulichkeit.
Jeder Klartextwert wird mit XChaCha20-Poly1305 unter Verwendung des Bucket-DEK verschlüsselt:``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
Der gespeicherte Datensatz ist eine msgpack-kodierte `Secret`-Struktur, die den Chiffretext, verschlüsselte Metadaten und die Schema-Version enthält. Die Authentifizierung ist implizit: Ein mit dem falschen Schlüssel entschlüsselter Chiffretext führt zu einem AEAD-Authentifizierungsfehler, bevor ein Klartext zurückgegeben wird.
### 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)
Der KEK wird mittels HKDF und nicht mit einem zweiten Argon2-Durchlauf abgeleitet. Der Hauptschlüssel wurde bereits durch eine KDF mit hohem Kostenaufwand erzeugt; ein zweiter Argon2-Aufruf würde jedem UnlockBucket-Aufruf Hunderte von Millisekunden Latenz hinzufügen, ohne Sicherheitsvorteil. HKDF-SHA256 arbeitet in etwa einer Mikrosekunde.
Verteidigung in der Tiefe: Ein Angreifer, der nur die Datenbank kompromittiert, erhält den umhüllten DEK und das HKDF-Salz, kann aber den KEK ohne den Hauptschlüssel nicht ableiten. Ein Angreifer, der nur den Hauptschlüssel kompromittiert, kann keinen LevelAdminWrapped-DEK entschlüsseln, ohne auch die Admin-Anmeldeinformationen zu kennen.
Geheime Metadaten (Erstellungszeit, Aktualisierungszeit, Zugriffszahl, Version) werden getrennt vom Chiffretext verschlüsselt:``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
Für `LevelAdminWrapped`, `LevelHSM` und `LevelRemote`-Buckets bedeutet dies, dass Metadaten ohne die Bucket-Berechtigung unzugänglich sind, was einen Angreifer mit Lesezugriff auf die Datenbankdatei daran hindert, Zugriffsmuster oder Zeitstempel zu erfahren.
**Hinweis zu Timing-Seitenkanälen:** XChaCha20-Poly1305 verarbeitet den vollständigen Chiffretext, bevor ein Authentifizierungsfehler zurückgegeben wird. Der Fallback-Entschlüsselungspfad (neuer abgeleiteter DEK → alter Master-Key-als-DEK) benötigt unabhängig davon, welcher Schlüssel erfolgreich ist, die gleiche Wanduhrzeit. Es gibt keinen Timing-Seitenkanal, der den Migrationsstatus eines Datensatzes preisgibt.
### Metadatenverschlüsselung – Richtlinien, WAL und Audit
Alle strukturellen Metadaten werden ebenfalls ruhend verschlüsselt. Zwei Schlüssel werden zum Zeitpunkt von `UnlockDatabase` aus dem Masterschlüssel abgeleitet:```
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 verschlüsselt: BucketSecurityPolicy-Werte und das Rotations-WAL.
auditEncKey verschlüsselt: die Felder Scheme, Namespace und Details jedes Audit-Ereignisses.
Beide Schlüssel werden bei Lock() aus dem Speicher gelöscht. Die für die Metadatenverschlüsselung verwendete Chiffre ist die gleiche konfigurierbare crypt.Cipher-Schnittstelle, die auch für Geheimnisse verwendet wird — die Wahl des Benutzers (AES-256-GCM für FIPS, standardmäßig XChaCha20-Poly1305) wird automatisch übernommen.
Drahtformat für alle verschlüsselten Metadaten-Blobs:``` nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
### Hashing von Policy-Bucket-Schlüsseln
Auf der Festplatte gespeicherte Policy-Schlüssel sind undurchsichtige Hashes anstelle von Klartext-`scheme:namespace`-Strings, was die Offline-Enumeration von Bucket-Namen verhindert:```
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)
Der In-Memory-schemeRegistry verwendet weiterhin "scheme:namespace" als Schlüssel — nur die On-Disk-Repräsentation ändert sich.
Jeder Policy-Datensatz trägt zwei Integritäts-Tags, die atomar in einer einzigen bbolt-Transaktion geschrieben werden:``` 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
Vor `UnlockDatabase` ist nur der SHA-256-Hash verfügbar. Nach dem Entsperren,
`loadPolicy` überprüft den HMAC-Tag. `UnlockDatabase` ruft `upgradePolicyHMACs` auf,
um HMAC-Tags für Richtlinien nachträglich zu ergänzen, die vor der Einführung dieser Funktion erstellt wurden.
### Prüfung der HMAC-Signierung```
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)
Der Signierschlüssel wird bei UnlockDatabase aktiviert und bei Lock gelöscht. Wenn
der Hauptschlüssel rotiert wird, fügt Rotate ein Schlüsselrotations-Checkpoint-Ereignis an
jede aktive Audit-Kette an, signiert mit dem alten Audit-Schlüssel als letztes Ereignis der
alten Epoche. Die Historie wird nie umgeschrieben; der Checkpoint ist die Vertrauensbrücke
zwischen den Epochen.
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)
All intermediate keys are zeroed immediately after use. The master key is
never written to disk in any form.
---
## Storage schema
The underlying database is bbolt. All buckets and their contents:
| bbolt bucket | Key | Value |
|---|---|---|
| `__meta__` | `salt` | msgpack — SaltStore (unencrypted; circular dependency if encrypted) |
| `__meta__` | `verify` | raw bytes — Argon2id verification hash |
| `__meta__` | `rotation_wal` | `nonce‖AEAD(msgpack(RotationWAL))` |
| `__meta__` | `bucket_dek_done` | `"1"` — DEK migration completion marker |
| `__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 Event |
| `__audit__/scheme/namespace` | `__chain_index__` | JSON — chainIndex |
| `scheme/namespace` | key string | msgpack — Secret struct |
### Secret struct (msgpack)```go
type Secret struct {
Ciphertext []byte `msgpack:"ct"`
EncryptedMeta []byte `msgpack:"em,omitempty"`
SchemaVersion int `msgpack:"sv"` // always 1
}
Die Event-Struktur verwendet separate Klartext-Routing-Felder (Scheme,
Namespace) neben verschlüsselten Nutzdatenfeldern (EncScheme, EncNamespace,
EncDetails). Prüfsummen werden über die Klartext-Routing-Felder und die
verschlüsselten EncDetails-Bytes berechnet, sodass die Kettenintegrität auf
drei Ebenen ohne Schlüssel überprüft werden kann:
| Stufe | Hat | Kann überprüfen |
|---|---|---|
| Öffentlich | Nichts | SHA-256-Prüfsummenkette (erkennt Manipulation und Einfügung) |
| Audit-Schlüsselinhaber | auditEncKey |
Beispiel: Ein Compliance-Prüfer erhält nur auditEncKey. Er kann die
vollständige HMAC-Kette über Schlüsselrotationen hinweg überprüfen und alle
Ereignisdetails lesen, kann jedoch keine geheimen Werte entschlüsseln. Ein
öffentlicher Beobachter, der nur die Datenbankdatei besitzt, kann dennoch
erkennen, ob ein Ereignis nachträglich geändert oder eingefügt wurde.
Das KDF-Salz wird als msgpack-codierte SaltStore unter dem Metadatenschlüssel
salt gespeichert. Jede Salzrotation fügt einen neuen SaltEntry hinzu und
erhöht CurrentVersion. Alte Einträge werden als Prüfpfad aufbewahrt. Der
SaltStore wird unverschlüsselt gespeichert — siehe Sicherheitsentscheidungen.
Rotate schreibt ein WAL, bevor es einen Datensatz berührt. Das WAL trägt
WrappedOldKey: den vor der Rotation verschlüsselten Masterschlüssel mit dem
neuen Masterschlüssel. Nach einem Absturz ist die alte Passphrase verloren;
WrappedOldKey ist der einzige korrekte Weg, den alten Schlüssel über die
Grenze zu tragen. Bei UnlockDatabase, wenn ein WAL vorhanden ist,
entschlüsselt der neue Masterschlüssel WrappedOldKey und die Rotation wird
ab dem WAL-Cursor fortgesetzt. Das WAL selbst ist mit policyEncKey
verschlüsselt.
Jeder bedeutende Vorgang hängt ein manipulationssicheres Ereignis an die Audit-Kette des Buckets an. Die Kettenintegrität basiert auf zwei Mechanismen.
Prüfsumme. SHA-256 über prevChecksum, ID, BucketID, Scheme, Namespace,
EncDetails, EventType und Timestamp. Die Verwendung von Scheme/Namespace
als Klartext (immer zusammen mit den verschlüsselten Formen erhalten) stellt
sicher, dass die Prüfsumme über Ladepfade hinweg stabil ist. EncDetails
bietet Integrität über die verschlüsselten Nutzdaten.
HMAC. HMAC-SHA256 über alle Felder einschließlich Seq. Ein Angreifer, der
in die Datenbank schreiben kann, aber den Audit-Schlüssel nicht kennt, kann
keinen gültigen HMAC erzeugen. VerifyIntegrity überprüft beide Schichten für
jedes Ereignis.
Epochengrenze der Schlüsselrotation. Bei Rotate wird ein
Checkpoint-Ereignis an jede aktive Kette angehängt, das Fingerabdrücke sowohl
des ausgehenden als auch des eingehenden Audit-Schlüssels trägt. Der Checkpoint
wird mit dem ausgehenden Schlüssel signiert. Prüfer, die einen beliebigen
Epochenschlüssel besitzen, können nachfolgende Epochenschlüssel aus dem
wrapped_new_key-Feld wiederherstellen und die HMAC-Kontinuität über die
gesamte Kette hinweg überprüfen.
Automatische Bereinigung. Wenn AuditPruneInterval in Config gesetzt
ist, läuft ein jack.Scheduler periodisch und ruft PruneEvents für jeden
registrierten Bucket auf. LevelHSM- und LevelRemote-Bucket werden
unabhängig von dieser Einstellung nie bereinigt.
Jack ist eine optionale Prozessüberwachungsbibliothek. Wenn ein JackConfig
über WithJack bereitgestellt wird, aktiviert Keeper automatisch
Hintergrundkomponenten:
LevelAdminWrapped-Bucket-DEKs nach
AutoLockInterval. LevelPasswordOnly-Buckets bleiben entsperrt, sodass
Hintergrundjobs ununterbrochen fortgesetzt werden können. Das
Single-Write-Lock-Muster innerhalb der Looper-Aufgabe beseitigt die
RUnlock→Lock-Race-Condition, die in früheren Entwürfen vorhanden war.LevelAdminWrapped-DEKs.jack.Doctor.PruneEvents auf allen
Nicht-HSM-Buckets.Wenn JackConfig nicht bereitgestellt wird, läuft Keeper ohne diese
Hintergrundaufgaben. Keeper ruft nie pool.Shutdown auf — der
Pool-Lebenszyklus gehört dem Aufrufer.
x/keepcmd bietet wiederverwendbare Keeper-Operationen, die von jedem
CLI-Framework entkoppelt sind. Betten Sie es in Ihre eigene Anwendung ein, um
typisierte, testbare Geheimnisverwaltung zu erhalten, ohne das CLI-Binärprogramm
mitziehen zu müssen.```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` ruft niemals `prompter` auf oder liest von stdin. Die Passphrase-Auflösung liegt vollständig in der Verantwortung des Aufrufers – dadurch bleibt das Paket in headless server contexts sicher.
`NoClose: true` verhindert, dass `Commands` nach jedem Vorgang `store.Close()` aufruft. Verwenden Sie dies in REPL-/Sitzungskontexten, in denen ein gemeinsamer Store über viele Aufrufe hinweg verwendet wird.
---
## x/keephandler
`x/keephandler` bindet Keeper-HTTP-Endpunkte an jeden `net/http`-Mux. Keine externe Router-Abhängigkeit – es verwendet Go 1.22+ Method- und Pattern-Routing mit der 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 gibt (allow bool, err error) zurück.
(true, nil) — die Anfrage fortsetzen lassen.(false, nil) — abbrechen; der Hook hat bereits eine vollständige Antwort geschrieben.(false, err) — abbrechen; das Framework schreibt einen 500 mit err.Error(). Der Hook darf nichts an w geschrieben haben.Hook.CaptureBody bool steuert, ob AfterFunc den Antworttext erhält. false (Standard) kostet einen leichten statusWriter-Wrapper; true puffert den vollständigen Text in einen bytes.Buffer für den AfterFunc — eine Zuweisung pro Anfrage.
Hooks werden in der Reihenfolge der Registrierung ausgeführt. Mehrere WithHooks-Aufrufe sind additiv. Nur der zuerst für einen bestimmten Routennamen registrierte Hook wird verwendet – spätere Registrierungen für dieselbe Route werden ignoriert.
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` führt die folgenden Schritte in dieser Reihenfolge aus:
1. Leitet den Audit-HMAC-Signaturschlüssel ab und aktiviert ihn
2. Leitet den Policy-HMAC-Schlüssel ab und aktiviert ihn
3. Leitet `policyEncKey` und `auditEncKey` ab und aktiviert sie
4. Löscht und lädt `schemeRegistry` neu (entschlüsselt alle Policy-Blobs)
5. Setzt alle unterbrochenen Rotations-WALs fort
6. Aktualisiert Policy-HMAC-Tags
7. Initialisiert alle `LevelPasswordOnly`-Bucket-DEKs im Envelope
8. Startet Hintergrundaufgaben (Migrations-Looper, Auto-Lock, Health-Patienten)
### LevelPasswordOnly-Bucket – vollständiger Lebenszyklus```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 buckets```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)
### Schlüsselrotation```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
### Sicherung```go
f, _ := os.Create("keeper.db.bak")
info, err := store.Backup(f)
// info.Bytes, info.Timestamp, info.DBPath
Alle Sentinel-Fehler arbeiten mit errors.Is und errors.As. Stack-Traces
werden zum Zeitpunkt der Erstellung über github.com/olekukonko/errors erfasst.
ErrAuthFailed fasst alle UnlockBucket-Fehler zusammen (CWE-204 / CVSS 5.3). Sowohl eine unbekannte Admin-ID als auch ein falsches Passwort geben ErrAuthFailed zurück. Dies verhindert die Aufzählung von Admin-IDs durch Timing- oder Fehlerzeichenfolgenvergleiche. RevokeAdmin behält ErrAdminNotFound bei, da es sich um eine administrative Operation an einem bereits entsperrten Store handelt. Der konstante Zeitvergleich für das Vorhandensein der Admin-ID wird absichtlich weggelassen. Ein Angreifer, der sub-Mikrosekunden-Unterschiede bei bbolt-Bucket-Lookups messen kann, benötigt lokalen Dateisystemzugriff – an diesem Punkt kann er den Richtlinien-Bucket direkt lesen. Das Bedrohungsmodell geht davon aus, dass die Datenbankdatei kompromittiert sein könnte; Zeitabwehr gegen entfernte Aufzählung ist das primäre Anliegen.
Argon2id dominiert das Timing. Argon2id benötigt 200–500 ms auf typischer Hardware. Unterschiede nach der Ableitung sind um vier oder mehr Größenordnungen kleiner und nicht aus der Ferne messbar. Es wird keine künstliche Gleichstellung angewendet.
DEK innerhalb der CAS-Transaktionsgrenze abgerufen. CompareAndSwapNamespacedFull ruft den Bucket-DEK innerhalb der bbolt-Schreibtransaktion ab, wodurch das Fenster eliminiert wird, in dem ein gleichzeitiges Rotate den DEK zwischen Abruf und Verwendung ändern könnte.
Passphrase wird im HTTP-Handler nie als Go-String gespeichert. Alle drei Passphrase-Felder (passphrase, new_passphrase) werden aus JSON direkt in []byte durch Roh-Map-Extraktion dekodiert, wobei das String-Backing-Array vom langlebigen Heap ferngehalten wird. Die []byte-Kopie wird nach der Verwendung mit wipeBytes genullt.
Kein --passphrase-Flag in der CLI. Flags erscheinen in der ps-Ausgabe und der Shell-Historie. Die CLI akzeptiert die Passphrase nur aus der Umgebungsvariablen KEEPER_PASSPHRASE oder einer interaktiven Echo-freien Eingabeaufforderung.
REPL-Geheimwerte sind nie sichtbar. set <key> in der REPL ohne Inline-Wert verwendet term.ReadPassword – es erscheint nicht im Terminal-Scrollback, der Shell-Historie oder ps. Ein Inline-Wert (set key value) kann für nicht sensible Daten bei Bedarf angegeben werden.
SaltStore ist absichtlich unverschlüsselt. Das KDF-Salt muss vor UnlockDatabase lesbar sein, um den Master-Schlüssel abzuleiten. policyEncKey (der für alle anderen Metadatenverschlüsselungen verwendet wird) wird selbst aus dem Master-Schlüssel abgeleitet – das Verschlüsseln des Salts mit policyEncKey wäre zirkulär. Ein KDF-Salt stellt Einzigartigkeit, nicht Vertraulichkeit sicher; es gibt keinen Sicherheitswert darin, es zu verschlüsseln.
Richtlinien-Bucket-Schlüssel werden gehasht, nicht als Klartext gespeichert. On-Disk-Richtlinien-Schlüssel sind hex(SHA-256("scheme:namespace"))[:32] – 128 Bit Schlüsselraum – anstelle von lesbaren Zeichenfolgen. Ein Offline-Angreifer, der die bbolt-Datei liest, kann keine Bucket-Namen aufzählen, ohne die Richtlinien-Blobs zu entschlüsseln.
Metadatenverschlüsselung verwendet dieselbe Chiffre-Schnittstelle wie Geheimnisse. Alle policyEncKey- und auditEncKey-Operationen laufen über s.config.NewCipher(key) – dieselbe crypt.Cipher-Schnittstelle, die für Geheimwerte konfiguriert ist. Die vom Benutzer gewählte Chiffre (AES-256-GCM für FIPS 140, standardmäßig XChaCha20-Poly1305) fließt automatisch in Richtlinien-, WAL- und Audit-Verschlüsselung ein. Kein Codepfad kodiert einen bestimmten Algorithmus fest.
LevelHSM- und LevelRemote-Buckets werden bei der Master-Schlüsselrotation übersprungen. reencryptAllWithKey und RotateSalt überspringen diese Buckets explizit. Der DEK wird vom Anbieter gesteuert; die Rotation des Master-Salts hat keine Auswirkungen darauf.
Absturzsichere Rotation mit WrappedOldKey. Rotate schreibt ein WAL, bevor es einen Datensatz berührt. Das WAL trägt WrappedOldKey: den vor der Rotation mit dem neuen Master-Schlüssel verschlüsselten Master-Schlüssel. Nach einem Absturz entschlüsselt UnlockDatabase WrappedOldKey mit dem verifizierten neuen Schlüssel und setzt die Rotation ab dem Cursor fort.
| Vollständige Kette + entschlüsselt Scheme/Namespace/Details |
| Betreiber | Master-Passphrase | Alles |
| Methode | Pfad | Beschreibung |
|---|
POST | {prefix}/unlock | Entsperren des Tresors mit einer Passphrase |
POST | {prefix}/lock | Sperren des Tresors |
GET | {prefix}/status | Sperrzustand – sicher ohne Authentifizierung abzufragen |
GET | {prefix}/keys | Alle geheimen Schlüssel auflisten |
GET | {prefix}/keys/{key} | Einen geheimen Wert abrufen |
POST | {prefix}/keys | Ein Geheimnis speichern (JSON oder Multipart) |
DELETE | {prefix}/keys/{key} | Ein Geheimnis löschen |
POST | {prefix}/rotate | Master-Passphrase rotieren |
POST | {prefix}/rotate/salt | KDF-Salz rotieren |
GET | {prefix}/backup | Eine Datenbank-Snapshot streamen |
| Error | Bedeutung |
|---|
ErrStoreLocked | Vorgang versucht, während der Store gesperrt ist |
ErrInvalidPassphrase | Falsche Master-Passphrase |
ErrAuthFailed | Jeder UnlockBucket-Fehler — unterscheidet nicht zwischen falschem Passwort und unbekannter Admin-ID (CWE-204) |
ErrKeyNotFound | Geheimer Schlüssel existiert nicht |
ErrBucketLocked | Bucket wurde nicht entsperrt |
ErrPolicyImmutable | Zweite Richtlinie für einen vorhandenen Bucket |
ErrPolicyNotFound | Keine Richtlinie für das angegebene Schema/Namespace |
ErrAdminNotFound | Admin-ID nicht in der Richtlinie — nur für RevokeAdmin |
ErrHSMProviderNil | HSM/Remote-Bucket ohne registrierten Anbieter erstellt |
ErrCheckLatency | DB-Lese-Latenz überschritt DBLatencyThreshold |
ErrCASConflict | Aktueller Wert entspricht nicht dem erwarteten Wert bei CompareAndSwap |
ErrSecurityDowngrade | Bucket-übergreifende Verschiebung von höherer zu niedrigerer Sicherheitsstufe |
ErrAlreadyUnlocked | UnlockDatabase auf einem bereits entsperrten Store aufgerufen |
ErrMasterRequired | UnlockDatabase mit nil oder zerstörtem Master aufgerufen |
ErrChainBroken | Überprüfung der Integrität der Audit-Kette fehlgeschlagen |
ErrMetadataDecrypt | Verschlüsselte Metadaten konnten nicht entschlüsselt werden |
ErrPolicySignature | Richtlinien-HMAC-Überprüfung fehlgeschlagen — Datensatz wurde manipuliert |
| Paket | Zweck |
|---|
go.etcd.io/bbolt | Eingebetteter Schlüssel-Wert-Speicher |
golang.org/x/crypto | Argon2id, XChaCha20-Poly1305, HKDF, scrypt |
github.com/awnumar/memguard | Speichersichere Schlüsselenklave (Master-Schlüssel, DEKs) |
github.com/vmihailenco/msgpack/v5 | Binäre Serialisierung für Geheimnisse und Richtlinien |
github.com/olekukonko/jack | Prozessüberwachung (optionale Jack-Integration) |
github.com/olekukonko/ll | Strukturierte Protokollierung |
github.com/olekukonko/errors | Sentinel-Fehler mit Stack-Traces |
github.com/olekukonko/zero | Sicheres Nullsetzen von Byte-Slices |
github.com/olekukonko/prompter | Echo-freie Terminal-Eingabeaufforderungen (nur CLI) |
github.com/integrii/flaggy | CLI-Flag-Parsing (nur cmd/keeper) |
golang.org/x/term | TTY-Erkennung und rohes Passwort-Lesen (nur CLI) |