From fb0915df1217826a48ac82babfdcb768b350d440 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 19 Aug 2026 19:14:24 +0200 Subject: [PATCH] Audit-Siegel: Umstiegsweg dokumentiert + Rotations-Fussangel entschaerft Frage aus dem Betrieb: bestehende Installation hat noch keinen AUDIT_HMAC_KEY - was passiert beim nachtraeglichen Setzen? Antwort jetzt in README und .env.example: setzen, neu starten, fertig. Bestehende Eintraege bleiben unveraendert gueltig, neue werden gesiegelt, alt und neu koexistieren ohne Fehlalarm. Nachgemessen auf gemischtem Bestand (4903 x V1, 57 x V2, 67 x V3): 0 Beanstandungen. Rueckwirkend siegeln ist nicht moeglich. Dabei zwei Fehler in der eigenen Doku gefunden und korrigiert: 1. Behauptet war, nach dem Leeren von AUDIT_HMAC_KEY_OLD seien alte Eintraege "nicht mehr pruefbar". Tatsaechlich werden sie als MANIPULIERT gemeldet - ein falscher Schluessel ist von einer Faelschung nicht zu unterscheiden. Gemessen: 67 Eintraege als manipuliert. Nur wenn GAR KEIN Schluessel gesetzt ist, gilt "nicht pruefbar". Doku entsprechend korrigiert, inkl. Warnung, das Feld nicht voreilig zu leeren. 2. AUDIT_HMAC_KEY_OLD bot nur einen Platz. Beim ZWEITEN Wechsel waeren alle mit dem ersten Schluessel gesiegelten Eintraege faelschlich als manipuliert erschienen (reproduziert: 67). Das Feld nimmt jetzt eine kommagetrennte Liste entgegen; verifiziert: mit beiden Alt-Schluesseln 0 Beanstandungen, mit nur dem juengsten 67. Co-Authored-By: Claude Opus 5 --- README.md | 51 ++++++++++++++++++++++++--- backend/.env.example | 38 ++++++++++++++------ backend/src/services/audit.service.ts | 30 ++++++++++------ 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index af111c7f..c28e6084 100644 --- a/README.md +++ b/README.md @@ -341,12 +341,37 @@ Den Wert in die `.env` der jeweiligen Umgebung eintragen. Wichtig: **pro Umgebung ein eigener Schlüssel** (Entwicklung, Test, Produktion) – und niemals ins Git-Repository. +**Bestehende Installation: bisher kein Schlüssel gesetzt?** + +Genau so ist es gedacht – der Schlüssel kam mit einem Update dazu. Du kannst +ihn jederzeit nachträglich setzen: + +1. Schlüssel erzeugen (`openssl rand -hex 32`), in die `.env` eintragen +2. Backend neu starten +3. Fertig – mehr ist nicht nötig + +Was dabei mit deinen **bereits vorhandenen** Einträgen passiert: + +- Sie bleiben **unverändert** und weiterhin gültig. Es wird nichts neu + berechnet und nichts nachträglich gesiegelt. +- Ab dem Neustart werden **neue** Einträge gesiegelt. Alt und neu liegen + friedlich nebeneinander, die Prüfung erkennt für jeden Eintrag automatisch, + nach welchem Verfahren er zu prüfen ist. +- Es gibt **keine** Fehlalarme dadurch. (Nachgemessen auf einer Installation + mit 4903 alten, 57 mittleren und 67 gesiegelten Einträgen: 0 Beanstandungen.) + +Rückwirkend lässt sich der Schutz nicht herstellen – ältere Einträge bleiben +ungesiegelt. Das ist kein Fehler, sondern liegt in der Natur der Sache: Ein +Siegel kann man nur beim Anlegen vergeben, nicht nachträglich. Je früher du +den Schlüssel setzt, desto größer der geschützte Zeitraum. + **Häufige Fragen** | Frage | Antwort | |---|---| | Was, wenn ich den Schlüssel gar nicht setze? | Nichts fällt aus. Das Audit-Log läuft normal weiter, nur ohne dieses zusätzliche Siegel. | -| Was, wenn ich ihn verliere? | Die damit gesiegelten Einträge lassen sich nicht mehr prüfen. Sie gelten dann als **„nicht prüfbar"** – ausdrücklich nicht als gefälscht. Kein Fehlalarm, aber der Nachweis für diesen Zeitraum ist weg. Deshalb: sichern wie ein Passwort. | +| Was, wenn ich ihn verliere? | Lässt du das Feld dann **leer**, gelten die gesiegelten Einträge als **„nicht prüfbar"** – ausdrücklich nicht als gefälscht, also kein Fehlalarm. Der Nachweis für diesen Zeitraum ist aber weg. Deshalb: sichern wie ein Passwort. | +| Und wenn ich stattdessen einen **neuen** Schlüssel eintrage? | Dann werden die alten Einträge als **„manipuliert" gemeldet** – das System kann einen falschen Schlüssel nicht von einer echten Fälschung unterscheiden. Trage den alten Schlüssel deshalb zusätzlich unter `AUDIT_HMAC_KEY_OLD` ein (siehe unten). | | Muss ich ihn irgendwo eintragen außer in der `.env`? | Nein. Einmal setzen, Backup anlegen, fertig. | | Verlangsamt das etwas? | Nein, spürbar nicht. | @@ -361,9 +386,27 @@ AUDIT_HMAC_KEY_OLD= ``` Neue Einträge werden ab sofort mit dem neuen Schlüssel gesiegelt, die -bisherigen bleiben über den alten Schlüssel weiterhin prüfbar. Nach einer -Übergangszeit kann `AUDIT_HMAC_KEY_OLD` geleert werden – danach sind die alten -Einträge allerdings nicht mehr prüfbar. +bisherigen bleiben über den alten Schlüssel weiterhin prüfbar. + +> ⚠️ **`AUDIT_HMAC_KEY_OLD` nicht voreilig leeren.** Solange Einträge +> existieren, die mit einem alten Schlüssel gesiegelt wurden, muss dieser dort +> stehen bleiben. Entfernst du ihn, werden diese Einträge als **„manipuliert" +> gemeldet** – nicht als „nicht prüfbar". Das System kann einen falschen +> Schlüssel nicht von einer echten Fälschung unterscheiden. Leeren kannst du +> das Feld gefahrlos erst, wenn die betroffenen Einträge durch die +> Aufbewahrungsfristen ohnehin gelöscht sind. + +**Mehrfach gewechselt?** Das Feld nimmt mehrere Schlüssel kommagetrennt auf – +vom jüngsten zum ältesten: + +```env +AUDIT_HMAC_KEY= +AUDIT_HMAC_KEY_OLD=,, +``` + +Ohne das würde beim zweiten Wechsel der zuerst genutzte Schlüssel verloren +gehen – und alle damit gesiegelten Einträge fälschlich als manipuliert +erscheinen. **Prüfen, ob alles in Ordnung ist** diff --git a/backend/.env.example b/backend/.env.example index 4bc520f6..a11c68f3 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -46,20 +46,37 @@ NODE_ENV=development # Nichts faellt aus. Das Audit-Log laeuft normal weiter, nur eben ohne # dieses zusaetzliche Siegel. # +# Ich habe bisher keinen Schluessel - kann ich ihn nachtraeglich setzen? +# Ja. Schluessel erzeugen, eintragen, Backend neu starten - fertig. +# Bestehende Eintraege bleiben unveraendert gueltig, ab dem Neustart +# werden neue gesiegelt. Alt und neu liegen nebeneinander, es gibt +# dadurch KEINE Fehlalarme. Rueckwirkend siegeln geht nicht - je frueher +# gesetzt, desto groesser der geschuetzte Zeitraum. +# # Was passiert, wenn ich ihn verliere? -# Die damit gesiegelten Eintraege lassen sich nicht mehr pruefen. Sie -# gelten dann als "nicht pruefbar" - NICHT als gefaelscht. Es gibt also -# keinen Fehlalarm, aber der Nachweis fuer diesen Zeitraum ist weg. +# Laesst du das Feld LEER, gelten die gesiegelten Eintraege als +# "nicht pruefbar" - NICHT als gefaelscht, also kein Fehlalarm. Der +# Nachweis fuer diesen Zeitraum ist aber weg. +# Traegst du stattdessen einen NEUEN Schluessel ein, ohne den alten unten +# zu hinterlegen, werden die alten Eintraege als "manipuliert" gemeldet - +# ein falscher Schluessel ist von einer Faelschung nicht zu unterscheiden. # Deshalb: Schluessel sichern, so wie ein Passwort. AUDIT_HMAC_KEY= -# Nur voruebergehend beim Schluesselwechsel setzen. +# Frueher verwendete Schluessel - beim Wechsel hier eintragen. # Moechtest du den Schluessel oben austauschen (z. B. weil du vermutest, # dass er in falsche Haende geraten ist), trage den ALTEN Schluessel hier # ein und den NEUEN oben. Dann bleiben die bisherigen Eintraege pruefbar, # waehrend neue schon mit dem neuen Schluessel gesiegelt werden. -# Nach ein paar Wochen kann dieses Feld wieder geleert werden - danach -# sind die alten Eintraege allerdings nicht mehr pruefbar. +# +# ACHTUNG: Dieses Feld NICHT voreilig leeren. Solange Eintraege existieren, +# die mit einem alten Schluessel gesiegelt wurden, muessen sie hier stehen. +# Sonst werden diese Eintraege als "manipuliert" gemeldet (nicht als +# "nicht pruefbar"). Gefahrlos leeren kannst du erst, wenn die betroffenen +# Eintraege durch die Aufbewahrungsfristen ohnehin geloescht sind. +# +# Mehrfach gewechselt? Mehrere Schluessel kommagetrennt, juengster zuerst: +# AUDIT_HMAC_KEY_OLD=,, AUDIT_HMAC_KEY_OLD= # --- Technisch (fuer Entwickler/Admins) --------------------------------- @@ -74,10 +91,11 @@ AUDIT_HMAC_KEY_OLD= # Fail-safe : Ohne Schluessel schreibt der Dienst weiter Version 2. Bereits # signierte Zeilen landen dann in `unverifiableEntries`, # ausdruecklich NICHT in `tamperedEntries`. -# Rotation : AUDIT_HMAC_KEY_OLD wird bei der Pruefung zusaetzlich -# akzeptiert - Wechsel ohne Rehash. Ein Rehash waere ohnehin zu -# vermeiden, er wuerde die Beweiskraft der Vergangenheit -# ueberschreiben. +# Rotation : AUDIT_HMAC_KEY_OLD (kommagetrennte Liste) wird bei der +# Pruefung zusaetzlich akzeptiert - Wechsel ohne Rehash. Ein +# falscher/fehlender Alt-Schluessel liefert einen HMAC-Mismatch +# und damit einen Manipulations-Befund; das ist nicht von einer +# echten Faelschung unterscheidbar und daher Absicht. # Grenze : Schuetzt gegen DB-Schreibzugriff ohne Schluessel. Wer Schluessel # UND Datenbank hat, kann die Kette konsistent neu rechnen. # Erzeugung : openssl rand -hex 32 (256 Bit) diff --git a/backend/src/services/audit.service.ts b/backend/src/services/audit.service.ts index 1fc39ed8..4dffea1c 100644 --- a/backend/src/services/audit.service.ts +++ b/backend/src/services/audit.service.ts @@ -227,9 +227,16 @@ function auditHmacKey(): string | null { return k && k.trim().length > 0 ? k : null; } -function auditHmacKeyOld(): string | null { - const k = process.env.AUDIT_HMAC_KEY_OLD; - return k && k.trim().length > 0 ? k : null; +/** + * Frueher verwendete Schluessel. Kommagetrennt, damit auch ein ZWEITER Wechsel + * moeglich ist, ohne die zuerst signierten Eintraege zu verlieren - mit nur + * einem Platz wuerden die aeltesten sonst als "manipuliert" gemeldet. + */ +function auditHmacKeysOld(): string[] { + return (process.env.AUDIT_HMAC_KEY_OLD || '') + .split(',') + .map((k) => k.trim()) + .filter((k) => k.length > 0); } function generateHashV3(data: AuditHashV2Input, key: string): string { @@ -765,17 +772,18 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ }; if (erwartet === 3) { - // HMAC-signiert: ohne Schluessel ist die Zeile nicht pruefbar. Das als - // "manipuliert" zu melden waere ein Fehlalarm – daher eigener Topf. - const key = auditHmacKey(); - const keyOld = auditHmacKeyOld(); - if (!key && !keyOld) { + // HMAC-signiert: ohne JEDEN Schluessel ist die Zeile nicht pruefbar. + // Das als "manipuliert" zu melden waere ein Fehlalarm – daher eigener + // Topf. Ein FALSCHER Schluessel ist dagegen nicht von einer Faelschung + // zu unterscheiden und wird bewusst als Befund gemeldet. + const kandidaten = [auditHmacKey(), ...auditHmacKeysOld()].filter( + (k): k is string => !!k, + ); + if (kandidaten.length === 0) { unverifiableEntries.push(log.id); continue; } - // keyOld deckt den Zeitraum vor einem Schluesselwechsel ab. - hashOk = (!!key && log.hash === generateHashV3(inhalt, key)) - || (!!keyOld && log.hash === generateHashV3(inhalt, keyOld)); + hashOk = kandidaten.some((k) => log.hash === generateHashV3(inhalt, k)); } else { hashOk = log.hash === generateHashV2(inhalt); }