# API-13 – Prüfprotokoll: Master-Key-Rotation & Zugriffs-Audit **Datum:** 2026-10-15 **Prüfer:** Claude Sonnet 5 (Subagent, im Auftrag von bundyxl@gmx.de) **Umgebung:** Host 192.168.1.204, frisches Wegwerf-Arbeitsverzeichnis `/root/api-13-work-1792096067`, Postgres 17 (Rolle `nexarch_test`, Wartungs-DB `postgres`, Port 5433) **Branch:** `feature/api-13-master-key-rotation-zugriffs-audit` ## Befund vor Prüfung API-13 war im Core-Repo bereits vollständig implementiert (Commits `bc2126f` API-10, `99001a5` API-13, `d2e5904` API-13-Testfix, alle bereits auf `main` gemergt). Es gab keinen offenen Feature-Branch und keine Pull-Request-Spur für API-13 — der Code existiert, wurde aber nie formal durch dieses Prüfprotokoll abgenommen. Diese Session hat daher keinen neuen Code geschrieben, sondern den bestehenden Code gegen alle drei Akzeptanzkriterien real verifiziert (echte Postgres-Testdatenbank, keine Mocks) und dieses Protokoll nachgetragen. Betroffene Dateien: `internal/kek/master.go`, `internal/kek/store.go`, `internal/kek/handler.go`, `internal/kek/rotation_monitoring.go`, `internal/kek/kek_test.go`, `migrations/0008_kek_master_key_rotation_state.up.sql`. ## Prüfung 1 — Master-Key-Rotationstest (Akzeptanzkriterium 1) **Ziel:** Nach Rotation des Master-Keys bleiben alle Tenant-KEKs mit demselben Klartext entschlüsselbar; Objekt-DEKs werden nicht angefasst. **Vorgehen:** `Store.RotateMasterKey(ctx, oldKey, newKey)` entpackt jeden `tenant_keks.wrapped_kek`-Datensatz mit dem alten Master-Key und verpackt denselben Klartext-KEK mit dem neuen Master-Key neu — der Tenant-KEK-Klartext selbst ändert sich nie. Objekt-DEKs liegen strukturell außerhalb dieser Tabelle/dieses Moduls (DMS/Mail-eigene Schemas) und werden von `RotateMasterKey` gar nicht berührt (kein Zugriff auf andere Tabellen/Module im Code). **Test:** `TestRotateMasterKey_AllTenantKEKsRemainDecryptableWithSamePlaintext` (`internal/kek/kek_test.go`) — legt mehrere Tenant-KEKs mit altem Master-Key an, rotiert, prüft dass `GetDecrypted` mit dem neuen Master-Key denselben Klartext wie vor der Rotation liefert. **Ergebnis:** PASS (echte DB, 0.18s). Zielwert erreicht. ``` === RUN TestRotateMasterKey_AllTenantKEKsRemainDecryptableWithSamePlaintext --- PASS: TestRotateMasterKey_AllTenantKEKsRemainDecryptableWithSamePlaintext (0.18s) ``` ## Prüfung 2 — Audit-Event bei Tenant-KEK-Zugriff (Akzeptanzkriterium 2) **Ziel:** Jede Entschlüsselung eines Tenant-KEK durch den Master-Key erzeugt einen eigenen, hochprivilegierten Audit-Event-Typ (`kek.master_key_access`), getrennt von normalen Audit-Events abrufbar, mit Akteur, Zeitpunkt und betroffenem Tenant. **Vorgehen:** `Store.GetDecrypted(ctx, tenantID, masterKey, actor)` schreibt bei konfiguriertem `WithAudit(log)` einen `audit.Event` mit `Action = MasterKeyAccessAction`, `Actor = `, `Target = tenantID` (Zeitpunkt wird vom zentralen Audit-Log selbst gesetzt, AUD-01). `Store. StreamMasterKeyAccessEvents` erzwingt `filter.Action = MasterKeyAccessAction` und liest ausschließlich diese Events — kein Vermischen mit normalen Audit-Events möglich. **Tests:** - `TestTenantKEKHandler_RecordsPrivilegedMasterKeyAccessAuditEvent` — ruft den HTTP-Handler mit einem authentifizierten, für den Tenant aktivierten Modul auf, prüft danach den geschriebenen Audit-Event (Akteur = Modulname, Target = Tenant, Action = `kek.master_key_access`). - `TestStreamMasterKeyAccessEvents_OnlyReturnsPrivilegedEvents` — schreibt sowohl privilegierte als auch normale Audit-Events, prüft dass der Stream ausschließlich die privilegierten liefert. - `TestStreamMasterKeyAccessEvents_RequiresAudit` — ohne konfiguriertes Audit-Log liefert der Store `ErrAuditNotConfigured` statt stillschweigend leer zu bleiben. **Ergebnis:** PASS (alle drei Tests, echte DB). Zielwert erreicht. ``` === RUN TestTenantKEKHandler_RecordsPrivilegedMasterKeyAccessAuditEvent --- PASS: TestTenantKEKHandler_RecordsPrivilegedMasterKeyAccessAuditEvent (0.14s) === RUN TestStreamMasterKeyAccessEvents_OnlyReturnsPrivilegedEvents --- PASS: TestStreamMasterKeyAccessEvents_OnlyReturnsPrivilegedEvents (0.13s) === RUN TestStreamMasterKeyAccessEvents_RequiresAudit --- PASS: TestStreamMasterKeyAccessEvents_RequiresAudit (0.06s) ``` Zusätzlich geprüft (Zugriffskontrolle vor dem eigentlichen Zugriff, Voraussetzung für einen aussagekräftigen Akteur im Audit-Event): ``` === RUN TestTenantKEKHandler_RejectsMissingCredential --- PASS: TestTenantKEKHandler_RejectsMissingCredential (0.16s) === RUN TestTenantKEKHandler_RejectsModuleNotActiveForTenant --- PASS: TestTenantKEKHandler_RejectsModuleNotActiveForTenant (0.12s) === RUN TestTenantKEKHandler_AllowsActiveModuleForTenant --- PASS: TestTenantKEKHandler_AllowsActiveModuleForTenant (0.12s) ``` ## Prüfung 3 — Konfigurierbares Rotationsintervall & OPS-Monitoring-Warnung (Akzeptanzkriterium 3) **Ziel:** Rotationsintervall ist über Umgebungsvariable konfigurierbar; bei Überschreitung erscheint eine Warnung im bestehenden OPS-Monitoring statt einer parallelen Alarmierungslösung. **Vorgehen:** `LoadRotationIntervalFromEnv` liest das Intervall (Tage) aus einer Umgebungsvariable, Default 90 Tage. `Store.MasterKeyAgeSeconds` liefert die Sekunden seit letzter Rotation (Singleton-Zeile aus Migration `0008_kek_master_key_rotation_state`). `NewMasterKeyAgeGauge` exportiert diese Kennzahl als Prometheus-Collector (`nexarch_core_kek_master_key_age_seconds`, OPS-03-Muster). `EnsureRotationAlertRule` registriert — idempotent — eine Schwellwertregel im bestehenden `internal/alerting`-Regelspeicher (OPS-05), die warnt, sobald die Kennzahl das konfigurierte Intervall überschreitet. Kein eigener, paralleler Alarmierungsweg. **Tests:** - `TestLoadRotationIntervalFromEnv` — Default-Wert und explizite Umgebungsvariable, inkl. Fehlerfälle (keine Zahl, negativer/0-Wert). - `TestMasterKeyAgeSeconds_ResetsAfterRotation` — Alter wächst mit der Zeit und wird durch `RotateMasterKey` auf ~0 zurückgesetzt. - `TestEnsureRotationAlertRule_FiresWhenIntervalExceeded` — legt die Alert-Regel an, setzt die Rotationszeit künstlich in die Vergangenheit (älter als Intervall), prüft dass die reale `internal/alerting`-Infrastruktur die Regel als überschritten/ausgelöst meldet. - `TestEnsureRotationAlertRule_IdempotentForSameInterval` — zweiter Aufruf mit gleichem Intervall legt keine doppelte Regel an; geändertes Intervall aktualisiert die bestehende Regel statt eine zweite anzulegen. **Ergebnis:** PASS (alle vier Tests, echte DB). Zielwert erreicht. ``` === RUN TestLoadRotationIntervalFromEnv --- PASS: TestLoadRotationIntervalFromEnv (0.00s) === RUN TestMasterKeyAgeSeconds_ResetsAfterRotation --- PASS: TestMasterKeyAgeSeconds_ResetsAfterRotation (0.14s) === RUN TestEnsureRotationAlertRule_FiresWhenIntervalExceeded --- PASS: TestEnsureRotationAlertRule_FiresWhenIntervalExceeded (0.12s) === RUN TestEnsureRotationAlertRule_IdempotentForSameInterval --- PASS: TestEnsureRotationAlertRule_IdempotentForSameInterval (0.08s) ``` ## Gesamt-Testlauf ``` go build ./... → OK go vet ./internal/kek/... → OK go test ./internal/kek/... -v -p 1 -count=1 → 13/13 PASS, 1.670s ``` Zusätzlich: `TestLoadMasterKeyFromEnv` und `TestCreateForTenant_EachTenantGetsDistinctKEK` (Grundlagen aus API-10, als Regressionsschutz mitgeprüft) ebenfalls PASS. ## Fazit Alle drei Akzeptanzkriterien und alle drei geforderten Prüfungen sind real gegen eine echte Postgres-Testdatenbank (keine Mocks) nachgewiesen. Kein offener Punkt. Keine Code-Änderung in dieser Session erforderlich — bestehende Implementierung erfüllt das Ticket vollständig.