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 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:14:24 +02:00
co-authored by Claude Opus 5
parent 93cb5ffd27
commit fb0915df12
3 changed files with 94 additions and 25 deletions
+47 -4
View File
@@ -341,12 +341,37 @@ Den Wert in die `.env` der jeweiligen Umgebung eintragen. Wichtig: **pro
Umgebung ein eigener Schlüssel** (Entwicklung, Test, Produktion) und Umgebung ein eigener Schlüssel** (Entwicklung, Test, Produktion) und
niemals ins Git-Repository. 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** **Häufige Fragen**
| Frage | Antwort | | 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 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. | | Muss ich ihn irgendwo eintragen außer in der `.env`? | Nein. Einmal setzen, Backup anlegen, fertig. |
| Verlangsamt das etwas? | Nein, spürbar nicht. | | Verlangsamt das etwas? | Nein, spürbar nicht. |
@@ -361,9 +386,27 @@ AUDIT_HMAC_KEY_OLD=<bisheriger Schlüssel>
``` ```
Neue Einträge werden ab sofort mit dem neuen Schlüssel gesiegelt, die 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 bisherigen bleiben über den alten Schlüssel weiterhin prüfbar.
Übergangszeit kann `AUDIT_HMAC_KEY_OLD` geleert werden danach sind die alten
Einträge allerdings nicht mehr 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=<aktueller Schlüssel>
AUDIT_HMAC_KEY_OLD=<vorheriger>,<davor>,<ganz alter>
```
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** **Prüfen, ob alles in Ordnung ist**
+28 -10
View File
@@ -46,20 +46,37 @@ NODE_ENV=development
# Nichts faellt aus. Das Audit-Log laeuft normal weiter, nur eben ohne # Nichts faellt aus. Das Audit-Log laeuft normal weiter, nur eben ohne
# dieses zusaetzliche Siegel. # 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? # Was passiert, wenn ich ihn verliere?
# Die damit gesiegelten Eintraege lassen sich nicht mehr pruefen. Sie # Laesst du das Feld LEER, gelten die gesiegelten Eintraege als
# gelten dann als "nicht pruefbar" - NICHT als gefaelscht. Es gibt also # "nicht pruefbar" - NICHT als gefaelscht, also kein Fehlalarm. Der
# keinen Fehlalarm, aber der Nachweis fuer diesen Zeitraum ist weg. # 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. # Deshalb: Schluessel sichern, so wie ein Passwort.
AUDIT_HMAC_KEY= 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, # Moechtest du den Schluessel oben austauschen (z. B. weil du vermutest,
# dass er in falsche Haende geraten ist), trage den ALTEN Schluessel hier # dass er in falsche Haende geraten ist), trage den ALTEN Schluessel hier
# ein und den NEUEN oben. Dann bleiben die bisherigen Eintraege pruefbar, # ein und den NEUEN oben. Dann bleiben die bisherigen Eintraege pruefbar,
# waehrend neue schon mit dem neuen Schluessel gesiegelt werden. # 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=<vorheriger>,<davor>,<ganz alter>
AUDIT_HMAC_KEY_OLD= AUDIT_HMAC_KEY_OLD=
# --- Technisch (fuer Entwickler/Admins) --------------------------------- # --- Technisch (fuer Entwickler/Admins) ---------------------------------
@@ -74,10 +91,11 @@ AUDIT_HMAC_KEY_OLD=
# Fail-safe : Ohne Schluessel schreibt der Dienst weiter Version 2. Bereits # Fail-safe : Ohne Schluessel schreibt der Dienst weiter Version 2. Bereits
# signierte Zeilen landen dann in `unverifiableEntries`, # signierte Zeilen landen dann in `unverifiableEntries`,
# ausdruecklich NICHT in `tamperedEntries`. # ausdruecklich NICHT in `tamperedEntries`.
# Rotation : AUDIT_HMAC_KEY_OLD wird bei der Pruefung zusaetzlich # Rotation : AUDIT_HMAC_KEY_OLD (kommagetrennte Liste) wird bei der
# akzeptiert - Wechsel ohne Rehash. Ein Rehash waere ohnehin zu # Pruefung zusaetzlich akzeptiert - Wechsel ohne Rehash. Ein
# vermeiden, er wuerde die Beweiskraft der Vergangenheit # falscher/fehlender Alt-Schluessel liefert einen HMAC-Mismatch
# ueberschreiben. # 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 # Grenze : Schuetzt gegen DB-Schreibzugriff ohne Schluessel. Wer Schluessel
# UND Datenbank hat, kann die Kette konsistent neu rechnen. # UND Datenbank hat, kann die Kette konsistent neu rechnen.
# Erzeugung : openssl rand -hex 32 (256 Bit) # Erzeugung : openssl rand -hex 32 (256 Bit)
+19 -11
View File
@@ -227,9 +227,16 @@ function auditHmacKey(): string | null {
return k && k.trim().length > 0 ? k : null; return k && k.trim().length > 0 ? k : null;
} }
function auditHmacKeyOld(): string | null { /**
const k = process.env.AUDIT_HMAC_KEY_OLD; * Frueher verwendete Schluessel. Kommagetrennt, damit auch ein ZWEITER Wechsel
return k && k.trim().length > 0 ? k : null; * 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 { function generateHashV3(data: AuditHashV2Input, key: string): string {
@@ -765,17 +772,18 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
}; };
if (erwartet === 3) { if (erwartet === 3) {
// HMAC-signiert: ohne Schluessel ist die Zeile nicht pruefbar. Das als // HMAC-signiert: ohne JEDEN Schluessel ist die Zeile nicht pruefbar.
// "manipuliert" zu melden waere ein Fehlalarm daher eigener Topf. // Das als "manipuliert" zu melden waere ein Fehlalarm daher eigener
const key = auditHmacKey(); // Topf. Ein FALSCHER Schluessel ist dagegen nicht von einer Faelschung
const keyOld = auditHmacKeyOld(); // zu unterscheiden und wird bewusst als Befund gemeldet.
if (!key && !keyOld) { const kandidaten = [auditHmacKey(), ...auditHmacKeysOld()].filter(
(k): k is string => !!k,
);
if (kandidaten.length === 0) {
unverifiableEntries.push(log.id); unverifiableEntries.push(log.id);
continue; continue;
} }
// keyOld deckt den Zeitraum vor einem Schluesselwechsel ab. hashOk = kandidaten.some((k) => log.hash === generateHashV3(inhalt, k));
hashOk = (!!key && log.hash === generateHashV3(inhalt, key))
|| (!!keyOld && log.hash === generateHashV3(inhalt, keyOld));
} else { } else {
hashOk = log.hash === generateHashV2(inhalt); hashOk = log.hash === generateHashV2(inhalt);
} }