Audit-Siegel: Platzhalter zaehlt nicht als Schluessel + Betriebsfalle dokumentiert

.env.example zeigt jetzt wie bei den anderen Secrets einen sichtbaren
Platzhalter an der Variablen statt eines leeren Werts - verstaendlicher, aber
mit Schutz dagegen: ein nicht ersetzter Platzhalter (enthaelt < oder >, oder
beginnt mit change/dein/your/hier) gilt NICHT als Schluessel. Sonst wuerde mit
einem oeffentlich im Repository stehenden Wert gesiegelt, was Sicherheit
vortaeuscht. Das Backend warnt in dem Fall im Log und laesst das Siegel aus.
Verifiziert: mit Platzhalter geschriebene Zeile bleibt hashVersion 2.

Dabei aufgefallen und dokumentiert: Wird das Siegel nach Aktivierung wieder
abgeschaltet (Schluessel fehlt, z. B. beim Rebuild verloren), sind die in
dieser Zeit entstandenen Eintraege ungesiegelt und werden beanstandet, sobald
der Schluessel zurueck ist. Das ist Absicht - ein ungesiegelter Eintrag
inmitten gesiegelter ist von einer Faelschung nicht zu unterscheiden. Warnung
in README und .env.example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:24:59 +02:00
co-authored by Claude Opus 5
parent 6de3a91aa7
commit ab0d6214f2
3 changed files with 52 additions and 10 deletions
+13
View File
@@ -365,6 +365,19 @@ 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 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. den Schlüssel setzt, desto größer der geschützte Zeitraum.
> ⚠️ **Einmal gesetzt, nicht wieder abschalten.** Fehlt der Schlüssel später
> (z. B. weil er beim Container-Rebuild aus der `.env` verlorenging), läuft die
> Anwendung normal weiter aber die in dieser Zeit entstandenen Einträge sind
> ungesiegelt und werden von der Prüfung **beanstandet**, sobald der Schlüssel
> wieder da ist. Das ist Absicht: Ein ungesiegelter Eintrag inmitten
> gesiegelter ist von einer Fälschung nicht zu unterscheiden. Nimm den
> Schlüssel deshalb in deine Deploy-Checkliste auf.
Ein noch nicht ersetzter Platzhalter (`<hier-eigenen-wert-eintragen>`) zählt
bewusst **nicht** als Schlüssel sonst würde mit einem öffentlich bekannten
Wert gesiegelt. Das Backend schreibt in diesem Fall eine Warnung ins Log und
lässt das Siegel aus.
**Häufige Fragen** **Häufige Fragen**
| Frage | Antwort | | Frage | Antwort |
+13 -7
View File
@@ -53,6 +53,13 @@ NODE_ENV=development
# dadurch KEINE Fehlalarme. Rueckwirkend siegeln geht nicht - je frueher # dadurch KEINE Fehlalarme. Rueckwirkend siegeln geht nicht - je frueher
# gesetzt, desto groesser der geschuetzte Zeitraum. # gesetzt, desto groesser der geschuetzte Zeitraum.
# #
# ACHTUNG: einmal gesetzt, nicht wieder abschalten.
# Fehlt der Schluessel spaeter (z. B. beim Container-Rebuild verlorengegangen),
# laeuft alles weiter - aber die in dieser Zeit entstandenen Eintraege sind
# ungesiegelt und werden beanstandet, sobald der Schluessel wieder da ist.
# Ein ungesiegelter Eintrag inmitten gesiegelter ist von einer Faelschung
# nicht zu unterscheiden. Also: in die Deploy-Checkliste aufnehmen.
#
# Was passiert, wenn ich ihn verliere? # Was passiert, wenn ich ihn verliere?
# Laesst du das Feld LEER, gelten die gesiegelten Eintraege als # Laesst du das Feld LEER, gelten die gesiegelten Eintraege als
# "nicht pruefbar" - NICHT als gefaelscht, also kein Fehlalarm. Der # "nicht pruefbar" - NICHT als gefaelscht, also kein Fehlalarm. Der
@@ -62,11 +69,11 @@ NODE_ENV=development
# ein falscher Schluessel ist von einer Faelschung nicht zu unterscheiden. # ein falscher Schluessel ist von einer Faelschung nicht zu unterscheiden.
# Deshalb: Schluessel sichern, so wie ein Passwort. # Deshalb: Schluessel sichern, so wie ein Passwort.
# #
# Hier steht bewusst KEIN Beispielwert: leer = Siegel aus (nichts faellt aus). # Eigenen Wert erzeugen mit: openssl rand -hex 32
# Ein Beispielwert waere gefaehrlicher als keiner - er stuende oeffentlich im # Der Platzhalter unten zaehlt NICHT als Schluessel - laesst du ihn stehen,
# Repository, und jeder koennte damit Eintraege siegeln. Trage deinen eigenen, # bleibt das Siegel aus (es wird also nicht versehentlich mit einem oeffentlich
# selbst erzeugten Wert ein: # bekannten Wert gesiegelt). Leer lassen ist ebenfalls in Ordnung.
AUDIT_HMAC_KEY= AUDIT_HMAC_KEY=<hier-eigenen-wert-eintragen>
# Frueher verwendete Schluessel - beim Wechsel hier eintragen. # 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,
@@ -83,8 +90,7 @@ AUDIT_HMAC_KEY=
# Mehrfach gewechselt? Mehrere Schluessel kommagetrennt, juengster zuerst: # Mehrfach gewechselt? Mehrere Schluessel kommagetrennt, juengster zuerst:
# AUDIT_HMAC_KEY_OLD=<vorheriger>,<davor>,<ganz alter> # AUDIT_HMAC_KEY_OLD=<vorheriger>,<davor>,<ganz alter>
# #
# Beim ersten Einrichten bleibt das Feld leer - es wird erst beim Wechsel # Beim ersten Einrichten leer lassen - erst beim Schluesselwechsel noetig.
# gebraucht.
AUDIT_HMAC_KEY_OLD= AUDIT_HMAC_KEY_OLD=
# --- Technisch (fuer Entwickler/Admins) --------------------------------- # --- Technisch (fuer Entwickler/Admins) ---------------------------------
+26 -3
View File
@@ -222,9 +222,32 @@ export interface AuditHashV2Input {
* `AUDIT_HMAC_KEY_OLD` erlaubt einen Schluesselwechsel ohne Rehash bei der * `AUDIT_HMAC_KEY_OLD` erlaubt einen Schluesselwechsel ohne Rehash bei der
* Pruefung wird zusaetzlich gegen den alten Schluessel getestet. * Pruefung wird zusaetzlich gegen den alten Schluessel getestet.
*/ */
/**
* Ein nicht ersetzter Platzhalter aus `.env.example` (z. B.
* `<hier-eigenen-wert-eintragen>`) darf NICHT als Schluessel gelten. Sonst
* wuerde mit einem oeffentlich im Repository stehenden Wert gesiegelt das
* waere schlechter als gar kein Siegel, weil es Sicherheit vortaeuscht.
*/
function istPlatzhalter(k: string): boolean {
return k.includes('<') || k.includes('>') || /^(change|dein|your|hier)/i.test(k);
}
let platzhalterGemeldet = false;
function auditHmacKey(): string | null { function auditHmacKey(): string | null {
const k = process.env.AUDIT_HMAC_KEY; const k = process.env.AUDIT_HMAC_KEY?.trim();
return k && k.trim().length > 0 ? k : null; if (!k) return null;
if (istPlatzhalter(k)) {
if (!platzhalterGemeldet) {
platzhalterGemeldet = true;
console.warn(
'[Audit] AUDIT_HMAC_KEY enthält noch den Platzhalter aus .env.example ' +
'das Audit-Siegel bleibt deaktiviert. Eigenen Wert erzeugen: openssl rand -hex 32',
);
}
return null;
}
return k;
} }
/** /**
@@ -236,7 +259,7 @@ function auditHmacKeysOld(): string[] {
return (process.env.AUDIT_HMAC_KEY_OLD || '') return (process.env.AUDIT_HMAC_KEY_OLD || '')
.split(',') .split(',')
.map((k) => k.trim()) .map((k) => k.trim())
.filter((k) => k.length > 0); .filter((k) => k.length > 0 && !istPlatzhalter(k));
} }
function generateHashV3(data: AuditHashV2Input, key: string): string { function generateHashV3(data: AuditHashV2Input, key: string): string {