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:
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user