diff --git a/docs/API-13-PRUEFPROTOKOLL.md b/docs/API-13-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..b1ccb783 --- /dev/null +++ b/docs/API-13-PRUEFPROTOKOLL.md @@ -0,0 +1,138 @@ +# 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.