Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.8 KiB
API-16 – Prüfprotokoll: Persistente Modul-Signaturschlüssel und Rotation
Ticket: core-kanban/tickets/API-16.md (Planungs-Repo, Commit bfea8f8). Branch feature/api-16-persistente-modul-signaturschluessel, Basis integration/gesamtprojekt (6b9350b). Geprüft auf dem Entwicklungshost 204 (Wegwerf-Cluster 17/coredev, seriell über die gemeinsame Sperre; der archivdms-Cluster blieb unberührt).
Umsetzung
- Ablage: Tabelle
module_signing_keysin der Mandanten-Datenbank (migrations/tenant/0008_module_signing_keys,upunddown). Abweichung vom Auftragstext, der die Registry nannte: Der Kontodienst kennt nur die Mandanten-DSN und stellt Tokens je Mandant aus (Modell C, wie IAM-19). Alle Instanzen desselben Mandanten teilen damit dieselbe Tabelle, jeder Mandant hat eigene Schlüssel. - Verschlüsselung: Der private Schlüssel (Seed) wird mit
kek.EncryptWithTenantKEK(AES-256-GCM, Muster aus API-10) verpackt, der Schlüssel kommt ausNEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY(base64, 32 Byte). Beim Laden wird der öffentliche Schlüssel aus dem entpackten Seed neu berechnet und mit der Spalte verglichen (vertauschte Zeilen fallen auf). - Instanzen: Änderungen laufen in einer Transaktion unter
pg_advisory_xact_lock. Beim gleichzeitigen Start entsteht genau ein Schlüssel. Jede Instanz lädt den Satz alleREFRESH_SECONDSneu. - Rotation: Ein neuer Schlüssel signiert erst ab
active_from = jetzt + Aktivierungsverzögerung(Verifier-Caches kennen ihn dann; JWKS-Cache-Controlist 60 s). Der bisherige Schlüssel bekommtretire_after = active_from + Token-TTL + 30 s Toleranz, steht bis dahin in der JWKS und wird danach aus JWKS und Tabelle entfernt. Auslöser: automatisch nachROTATION_DAYS(unter der Sperre, nur eine Instanz rotiert) oder peraccount-api -rotate-module-key. - Fail closed: Ist der Speicher nicht lesbar oder ein Schlüssel nicht entschlüsselbar, gibt es weder Token (503) noch JWKS (503,
no-store). Ein veralteter Satz wird nach Ablauf des Aktualisierungsintervalls nicht weiterverwendet. - Code:
internal/moduletrust/{persist,pgstore,memstore}.go, Anpassungen inkeys.go/issuer.go,internal/moduletoken/token.go(503),cmd/account-api/{main,modulekeys}.go. Signaturen und Verifier unverändert,NewKeyManager()bleibt der Prozessspeicher-Variante vorbehalten.
Umgebungsvariablen (Kontodienst)
| Variable | Standard | Zulässig | Bedeutung |
|---|---|---|---|
NEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY |
– (Pflicht) | base64, 32 Byte | Verschlüsselung der privaten Schlüssel |
NEXARCH_ACCOUNT_MODULE_KEYS_EPHEMERAL |
false |
true/false |
Nur Prozessspeicher (Entwicklung), Tokens sind nach jedem Neustart ungültig |
NEXARCH_ACCOUNT_MODULE_KEY_ROTATION_DAYS |
30 | 0–3650 (0 = aus) | Automatische Rotation nach Alter |
NEXARCH_ACCOUNT_MODULE_KEY_ACTIVATION_DELAY_SECONDS |
120 | 60–3600, ≥ Aktualisierungsintervall | Verzögerung bis der neue Schlüssel signiert |
NEXARCH_ACCOUNT_MODULE_KEY_REFRESH_SECONDS |
30 | 1–300 | Neuladeintervall je Instanz |
Betriebshinweis (Bruch): Ein bestehender Kontodienst startet nicht mehr ohne NEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY (oder ausdrücklich ..._EPHEMERAL=true). Die Tenant-Migration 0008 muss vorher angewendet sein. Der Verschlüsselungsschlüssel gehört wie die anderen Betriebsgeheimnisse in /etc/nexarch/account-api.env (nicht ins Repo).
Prüfungen und Ergebnis
| Prüfung (Ticket) | Ergebnis |
|---|---|
| AK1/Prüfung 1: Neustart am echten Kontodienst, Token bleibt gültig, JWKS liefert dieselbe KID | bestanden (scripts/e2e-module-keys.sh, alter Prozess beendet und Port frei, neuer Prozess mit anderer PID) |
| AK2/Prüfung 3: zwei Instanzen gegen dieselbe Datenbank | bestanden: gleiche KID, Token der einen Instanz wird mit der JWKS der anderen verifiziert, genau eine Zeile nach Start beider Instanzen; Unit- und Postgres-Test: 8 bzw. 6 parallele Starts ergeben genau einen Schlüssel |
| AK3/Prüfung 2: Rotation mit Überlappung | bestanden am Prozess: nach der Rotation listen beide Instanzen 2 Schlüssel, signiert wird bis zur Aktivierung mit dem alten, danach mit dem neuen; der alte bleibt bis TTL + Toleranz in der JWKS und ist danach aus JWKS und Datenbank entfernt |
| AK4/Prüfung 3: Ausfall des Speichers | bestanden: Tabelle nicht lesbar ergibt 503 für JWKS und Token, nach Wiederherstellung 200; falscher oder fehlender Verschlüsselungsschlüssel: Dienst startet nicht |
| Klartext-Test | Chiffretext enthält weder Seed noch privaten Schlüssel (Unit- und Postgres-Test) |
| Manipulation | vertauschte Chiffretexte zweier Zeilen und falscher Schlüssel werden beim Laden abgewiesen (fail closed) |
| Automatische Rotation | drei Instanzen nach Ablauf des Intervalls ergeben genau eine Rotation |
| Migration | 0008 up, down und erneutes up grün; scripts/migrations-fresh-check.sh: Core Mandant 18 (vorher 17), gemeinsame Mandanten-DB 88 Tabellen ohne Kollision |
| Ende-zu-Ende Core→DMS | scripts/e2e-core-dms.sh grün (TestCoreE2E_TokenJWKSAndPolicy, 42,8 s) |
| Regression | moduletrust, moduletoken, oidc, contracttest, loadtest, resync, mtls, tenant, user, rbac, auth, kek je einzeln nach Cluster-Reset grün; go build ./..., go vet Core, check-port-defaults.sh, check-route-prefixes.sh sauber |
Neue Tests: internal/moduletrust/persist_test.go (9), pgstore_test.go (2, gegen echte Datenbank), internal/moduletoken/persistent_test.go (1), cmd/account-api/modulekeys_test.go (1).
Iterationen
- Produktivcode: 0 Fix-Runden (alle neuen Tests im ersten Lauf grün).
- Testaufbau: 1 Fix-Runde. Der erste Lauf von
e2e-module-keys.shmeldete den Neustart als bestanden, obwohl er keiner war: Das Skript startete den Kontodienst über eine Shell-Funktion im Hintergrund,killtraf nur den Zwischenprozess, der alte Kontodienst lief weiter (der zweite Start scheiterte am belegten Port), und der Test lief gegen den alten Prozess. Erkannt wurde das, alse2e-core-dms.shan „address already in use" scheiterte. Behoben: Start ohne Zwischenprozess, das Skript prüft jetzt, dass der alte Prozess beendet, der Port frei und die neue PID eine andere ist. Die Reste der ersten Läufe (zwei Prozesse auf 204) habe ich beendet, danach lief alles grün und ohne Reste.
Grenzen und offene Punkte
- Verschlüsselungsschlüssel: Eine Neuverpackung bei Wechsel von
..._KEY_ENCRYPTION_KEYfehlt (Nicht-Bestandteil laut Ticket). Ein Wechsel ohne Neuverpackung macht die Schlüssel unlesbar, der Dienst startet dann nicht. - Token-TTL: Die Aufbewahrungsfrist des alten Schlüssels berechnet sich aus der TTL der laufenden Konfiguration. Wird die TTL verkürzt und sofort rotiert, können früher mit der längeren TTL ausgestellte Tokens den alten Schlüssel überleben (sie sind dann nicht mehr verifizierbar). Nach einer TTL-Verkürzung deshalb erst nach Ablauf der alten TTL rotieren.
- Uhren: Aktivierung und Ablauf hängen an der Zeit der Instanzen. Bei Abweichungen über die 30 s Toleranz hinaus sind Randfehler möglich (Zeitsynchronisation der Hosts vorausgesetzt).
- Widerruf ausgestellter Modul-Tokens bleibt offen (Ticket API-15/IAM-17), ebenso eine Oberfläche zur Rotation (hier nur der Aufruf
account-api -rotate-module-keyund die Rotation nach Alter). - Speicherort: Die Schlüssel liegen je Mandanten-Datenbank; eine zentrale Registry-Ablage (Auftragstext) wäre eine eigene Architekturentscheidung, wenn ein Kontodienst mandantenübergreifend arbeiten soll.
- Nicht geprüft: Betrieb hinter dem Reverse Proxy und mit dem laufenden
dms-app-Prozess (nur In-Process-Kette und Kontodienst-Prozesse); der Gesamtsystem-Smoke im Token-Modus mit persistenten Schlüsseln steht aus. gofmt:internal/moduletrust/moduletrust_test.gowar schon vorher unformatiert und ist nicht Teil dieser Änderung.