Signaturschluessel der Modul-JWTs liegen verschluesselt in der Mandanten-Datenbank
(module_signing_keys), werden von mehreren Instanzen geteilt und rotieren mit
Ueberlappung; nicht lesbarer Speicher ergibt 503 statt Token/JWKS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POST /authorize nimmt optional den Mandanten entgegen und wertet modulgebundene Rechte ueber AuthorizeForTenant aus (fail closed). Mandant ist Pflicht fuer Rechte mit Modul-Bindung, das Service-Token laesst sich auf Mandanten einschraenken. Migration 0012 bindet die dms.*-Rechte an das Flag dms. Der DMS-policyclient sendet den Mandanten mit und trennt den Cache je Mandant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Core exponierte bislang nur GET /audit/export (AUD-03) - physisch
getrennte Module (Archive, DMS, Mail) hatten keinen Weg, eigene
sicherheitsrelevante Vorgaenge im zentralen Audit-Log zu vermerken.
IngestHandler (POST /audit/ingest, X-Service-Token) ruft denselben
alleinigen Schreibpfad wie core-interne Aufrufer auf (audit.Log.Record) -
kein zweiter, laxerer Validierungspfad ueber REST.
Notwendige Grundlage fuer Archive CMP-04 (Audit-Trail-Kopplung an
Core-Audit-Log), das per REST an dieses Modul anbindet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestVerifyWithContext_RateSpikeTriggersAuditEvent schlief unbedingt nach
JEDEM Basislinien-Durchlauf, auch nach dem letzten — der nachfolgende
"Burst"-Aufruf landete dadurch selbst ~120ms nach dem letzten
Basislinien-Zugriff und war damit gar kein Burst mehr (kein Alarm). Fix:
Sleep nur zwischen den Durchlaeufen, nicht danach. Auf root@192.168.1.131
verifiziert: PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
internal/serviceaccount/anomaly.go: rein additive Ergaenzung zu IAM-09
(serviceaccount.go bleibt unveraendert) — VerifyWithContext ruft Verify
unveraendert auf und beobachtet danach Herkunfts-IP und Zugriffsrate eines
gueltigen Tokens. Eine bisher nicht beobachtete IP-Range (grob gebuckelt auf
/24 bzw. /64) oder ein gegenueber der EWMA-Basislinie stark abweichender
Zugriffsratenanstieg (>5x schneller als gewohnt, erst ab 3 Beobachtungen
bewertet) erzeugt ein Audit-Event mit metadata.severity=elevated ueber die
zentrale AUD-01-Schreibschnittstelle (Akzeptanzkriterium 1 + 2). Optional ist
ein CFG-02-Dispatcher anschliessbar (WithNotify) fuer eine Benachrichtigung
des Token-Besitzers/Tenant-Admins.
Keine automatische Sperre: eine erkannte Anomalie ruft niemals Revoke auf,
das Token bleibt gueltig (Akzeptanzkriterium 3) — reines Hinweissignal, wie
im Auftrag gefordert (bewusst vermeiden: Keycloak, wo Anomalie-Erkennung nur
optionale Zusatzkonfiguration ist).
migrations/tenant/0007_api_token_anomaly.{up,down}.sql: zwei neue Tabellen
(api_token_ip_ranges, api_token_rate_state), keine Aenderung an api_tokens/
audit_events selbst.
Pruefungen (ausgefuehrt auf root@192.168.1.131, go test -p 1 PASS):
1. Zugriff aus neuer IP-Range erzeugt erwarteten Audit-Event —
TestVerifyWithContext_NewIPRangeTriggersAuditEvent: erster Zugriff legt
Basislinie an (kein Alarm), zweiter Zugriff aus anderer Range erzeugt
genau 1 Event mit severity=elevated. PASS.
2. Kuenstlich stark erhoehte Zugriffsrate erzeugt erwarteten Audit-Event —
TestVerifyWithContext_RateSpikeTriggersAuditEvent: Basislinie mit
~120ms-Abstand etabliert, Burst-Zugriff ohne Abstand erzeugt genau
1 Raten-Anomalie-Event. PASS.
3. Regressionstest: normale, unauffaellige Nutzung erzeugt keine
Falsch-Alarme — TestVerifyWithContext_NormalUsageNoFalseAlarms: 8 Zugriffe
gleiche IP-Range, gleichmaessiger Abstand, 0 Events. PASS.
Zusaetzlich: TestVerifyWithContext_AnomalyDoesNotRevokeToken belegt
Akzeptanzkriterium 3 direkt (Token nach erkannter Anomalie weiterhin
gueltig). PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Readiness-Warnung (internal/health.Registry.RegisterWarning) plus
internal/mtls.GateCheck erkennen verteilten Betrieb (>1 Instanz/Host) ohne
aktives mTLS und machen das als "mtls-gate"-Warnung im Readiness-Antwort-
koerper sichtbar, ohne den Dienst als "nicht bereit" zu melden. Neue
Betriebs-Checkliste (docs/BETRIEBS-CHECKLISTE-STUFENWECHSEL.md) haelt die
mTLS-Aktivierung als Pflichtschritt beim Wechsel Stufe 1 -> Stufe 2 fest,
SKALIERUNGSKONZEPT.md verweist darauf.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Verbindliches Fail-Closed fuer Lizenzpruefungen (internal/license.Service):
weder Cache noch Registry erreichbar -> Zugriff verweigert
(ErrRegistryUnavailable), kein impliziter Fail-Open-Zustand. Anders als bei
Feature-Flags gibt es fuer Lizenzpruefungen keinen Fallback auf einen
veralteten Cache-Stand.
Fuer reine Feature-Flags (internal/flag.Service) bleibt der bestehende
Stale-Cache-Fallback erhalten, aber neu durch eine dokumentierte
Gnadenfrist (DefaultStaleGracePeriod) begrenzt, statt unbegrenzt gueltig
zu sein.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
AUD-02 hat bereits einen Trigger, der UPDATE/DELETE auf audit_events per
RAISE EXCEPTION ablehnt (Akzeptanzkriterium 3 aus AUD-07 war damit schon
erfuellt). Zwei Luecken blieben: (1) die Anwendungsrolle hatte weiterhin
UPDATE/DELETE-Rechte auf DB-Ebene, der Schutz war nur ein Code-Pfad
(Trigger), keine fehlenden Rechte; (2) ein abgelehnter Aenderungsversuch
wurde nirgends selbst protokolliert.
- migrations/0010_audit_events_role_enforcement.{up,down}.sql: Gruppenrolle
nexarch_app ohne LOGIN/Passwort, REVOKE ALL + GRANT INSERT, SELECT auf
audit_events fuer PUBLIC/nexarch_app; Trigger-Funktion um RAISE WARNING
mit Akteur/Session/Client-Kontext ergaenzt, bevor der Versuch weiterhin
per RAISE EXCEPTION abgelehnt wird (RAISE WARNING ist nicht transaktional,
landet also auch bei Rollback im Postgres-Server-Log).
- internal/audit/append_only_test.go: Tests je Akzeptanzkriterium -
TestAppendOnly_AppRoleHasNoUpdateOrDeletePrivilege (AC1/Pruefung1,
erwartet "permission denied" statt Trigger-Fehler),
TestAppendOnly_TamperAttemptIsLogged (AC2/Pruefung2, faengt die
RAISE-WARNING-Notice ueber pgx OnNotice ab),
TestAppendOnly_NormalWriteAndReadStillWork (AC3/Pruefung3, Regression).
Pruefungen auf 192.168.1.131 durchgefuehrt:
1. UPDATE/DELETE mit Anwendungsrolle (Mitglied nexarch_app) -> "permission
denied for table audit_events" statt Trigger-Fehler: PASS.
2. Ablehnungsversuch selbst protokolliert (RAISE WARNING mit
Akteur/Session/Zeitpunkt, unabhaengig von Transaktions-Rollback) via
pgx-Notice-Capture nachgewiesen: PASS.
3. Regression Schreiben/Lesen (Record + CountByTenant, mehrere Eintraege):
PASS.
Gesamtlauf go test ./... -p 1 -count=1: internal/audit vollstaendig gruen
(alle 22 Tests inkl. der 3 neuen). Bestehende, dokumentierte Altlasten
unveraendert beobachtet (nicht Bestandteil dieser Kachel): internal/migrate
(TestRolloutAll_* reihenfolgeabhaengig), internal/notify
(TestDispatcher_EnqueueAndProcess flakt), internal/pentest
(TestPentest_KEK_CompromisedMasterKeyCannotDecryptAfterRotation), internal/user
(verwaiste test_iam01_users/test_iam01_superadmins-DBs).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Zwei kleine Nacharbeiten aus dem Testlauf auf 131:
- ConfigStore.Save nutzte einen Multi-Statement-Exec (DELETE; INSERT), den
pgx im extended protocol ablehnt ("cannot insert multiple commands into a
prepared statement") -- jetzt als Transaktion mit zwei Exec-Aufrufen.
- Das Test-Setup fuer die tenants-Tabelle in config_store_test.go war
schlanker als das in internal/kek/kek_test.go verwendete Vollschema
(fehlende Spalten previous_status/deletion_scheduled_at/
retention_block_reason/retention_checked_at). Da beide Pakete im
Gesamttestlauf dieselbe Test-Datenbank per CREATE TABLE IF NOT EXISTS
teilen, hat das schlankere Schema (wenn zuerst angelegt) den kek-Test
TestTenantKEKHandler_AllowsActiveModuleForTenant zum Fehlschlagen
gebracht. Jetzt identisches Vollschema wie kek_test.go.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Speichert das LDAP-Bind-Passwort je Tenant ausschliesslich verschluesselt
mit dem Tenant-KEK aus API-10 (AES-256-GCM, kek.EncryptWithTenantKEK/
DecryptWithTenantKEK) statt in einem separaten Secret-Schema oder Klartext
(Keycloak-Antipattern bewusst vermieden). Neue Tabelle ldap_bind_config in
der Tenant-Datenbank (Migration 0009), neuer internal/ldapsync.ConfigStore
fuer Save/Get, rein additiv zu IAM-05 (Syncer/RoleMappingStore unveraendert).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Testaufbau fuer den Router legte die tenants-Tabelle ohne die aus
TEN-04/TEN-08 hinzugekommenen Spalten (previous_status,
deletion_scheduled_at, retention_block_reason, retention_checked_at)
an, die Registry.GetBySlug aber immer mitliest — jeder Resolve schlug
dadurch fehl. Kleiner Test-Infra-Fix, kein Scope-Creep (bekanntes,
wiederkehrendes Muster laut Team-Workflow). Zusaetzlich Fatalf statt
Fatal mit Format-String (go vet).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Ergaenzt TEN-06 (Connection-Routing & Pooling) um ein konfigurierbares
Rate-Limit auf die Tenant-Wechsel-Rate pro Client/Service-Credential,
additiv ueber Router.SetSwitchRateLimit + Router.ResolveForClient. Die
bestehende harte Obergrenze fuer gleichzeitig offene Tenant-Pools samt
LRU-Verdraengung aus TEN-06 bleibt unveraendert (bereits konfigurierbar
per NewRouter(maxOpen)) und wird durch neue Tests fuer AC1/AC3 im
Zusammenspiel mit ResolveForClient zusaetzlich abgesichert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Vorbestehende Luecke: das Test-Setup in internal/migrate erzeugte die
tenants-Tabelle ohne die von TEN-08 hinzugefuegten Spalten, wodurch
tenant.Registry.List() in jedem Test dieses Pakets fehlschlug.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Ergaenzt das aus RBAC-01 fehlende Pflichtgate: ValidateNoTenantToSuperadminEscalation
prueft Rollenhierarchie/Rechtezuordnung gegen die Invariante, dass keine
Nicht-Superadmin-Rolle (weder direkt noch geerbt) Superadmin-exklusive Rechte
erreichen kann. Vier neue Tests in internal/rbac/policy_invariant_test.go
decken die aktuelle Policy sowie zwei unabhaengig konstruierte, absichtlich
fehlerhafte Testfixtures ab (nie die produktiv genutzten parent/direct-Werte).
CI-Pflichtgate .gitea/workflows/rbac-policy-gate.yml laeuft bei jeder Aenderung
unter internal/rbac/** sowie bei jedem Pull Request. QA-03/QA-05 um den
entsprechenden Nachtrag ergaenzt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Ergaenzt IAM-06 (SSO ueber OIDC) um strikte Issuer-/Audience-Pruefung,
JWKS-Pinning gegen den konfigurierten Issuer und eine technisch
erzwungene Sperre gegen Superadmin-Vergabe aus externen Claims.
- internal/oidc/verify.go: Verifier.audience wird per Tenant fest
konfiguriert (NewVerifier), niemals aus dem Token gelesen; Verify
lehnt Tokens mit unerwarteter Audience zusaetzlich zum bestehenden
Issuer-Abgleich ab. Signatur wird weiterhin ausschliesslich gegen die
beim Verifier hinterlegte JWKS-Schluesselmenge des konfigurierten
Issuers geprueft (kid waehlt nur INNERHALB dieser Menge aus).
- internal/oidc/login.go: capToTenantScopedRole() kappt eine aus
RoleMappingStore.HighestRoleFor gelieferte superadmin-Rolle
unabhaengig von deren Quelle auf tenant_admin, statt sie zu vergeben
oder den Login mit einem Fehler abzubrechen. Zusaetzliche, von der
bestehenden Erlaubnis-Matrix (SetMapping, rbac.Store.Assign)
unabhaengige Verteidigungsebene direkt im Login-Pfad.
- Tests: TestVerify_RejectsWrongAudience (Pruefung 1),
TestVerify_RejectsWrongSigningKey (Pruefung 2, bereits vorhanden),
TestCapToTenantScopedRole_NeverGrantsSuperadmin und
TestCompleteOIDCLogin_CapsSuperadminClaimToTenantAdmin (Pruefung 3,
inkl. Integrationstest mit direkt an der Erlaubnis-Matrix vorbei
manipulierter Zuordnungstabelle).
- internal/pentest, internal/saml: bestehende Tests an neue
NewVerifier-Signatur (audience-Parameter) angepasst.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
Fix: Massenwiderrufs-Stichtagsvergleich nutzt jetzt IssuedAtNano statt
claims.IssuedAt, da RegisteredClaims.IssuedAt beim JWT-Roundtrip auf volle
Sekunden rundet und sonst ein Login in derselben Sekunde wie ein
RevokeAllForUser-Aufruf faelschlich als widerrufen gelten koennte.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP