Doku: Audit-Siegel (AUDIT_HMAC_KEY) verstaendlich erklaert

README und .env.example erklaeren jetzt zuerst in Alltagssprache, was das
Audit-Log ist, wogegen der Schluessel schuetzt und was passiert, wenn man ihn
weglaesst oder verliert - inklusive Bild vom Notarstempel. Die technische
Ebene bleibt vollstaendig erhalten, in der README als aufklappbarer Block
(Hash-Versionen 1/2/3, Version-Floor, Loeschungs-Manifest, Rueckgabefelder von
/verify, bekannte Grenze) und in .env.example als eigener Abschnitt.

Ausserdem dokumentiert: Schluesselwechsel ueber AUDIT_HMAC_KEY_OLD ohne
Rehash, pro Umgebung ein eigener Schluessel, und die Warnung zu den beiden
Endpunkten mit Nebenwirkung (rehash/cleanup).

AUDIT_HMAC_KEY zusaetzlich in die Production-Checkliste aufgenommen.

Korrigiert: Die Integritaetspruefung ist NICHT ueber die Oberflaeche
erreichbar (verifyIntegrity wird von keiner Komponente genutzt) - die Doku
zeigt jetzt den API-Aufruf. Rechte, Endpunkte und Rueckgabefelder gegen den
Code geprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:06:35 +02:00
co-authored by Claude Opus 5
parent 9fcab6f17e
commit 93cb5ffd27
2 changed files with 203 additions and 13 deletions
+146
View File
@@ -281,6 +281,10 @@ LISTEN_ADDR=127.0.0.1
# Bei separatem Frontend-Host: erlaubte Origins
CORS_ORIGINS=https://crm.deine-domain.de
# Siegel fuer das Audit-Log schuetzt die Beweisspur vor nachtraeglicher
# Faelschung. Siehe Abschnitt "Audit-Siegel" weiter unten.
AUDIT_HMAC_KEY=$(openssl rand -hex 32)
```
### Deployment-Modus: On-Prem vs. Cloud
@@ -298,6 +302,148 @@ SSRF-Schalter:
SSRF_BLOCK_PRIVATE_IPS=true
```
### Audit-Siegel (`AUDIT_HMAC_KEY`)
**Worum geht es?**
OpenCRM führt ein Audit-Log: eine lückenlose Aufzeichnung, wer wann welche
Daten gesehen oder geändert hat. Das ist die Beweisspur, wenn es Streit gibt,
etwas verschwindet oder der Datenschutz nachfragt.
Damit diese Aufzeichnung etwas wert ist, muss man ihr ansehen können, ob
jemand nachträglich daran herumgeschrieben hat. Dafür trägt jeder Eintrag
einen Fingerabdruck, der auch den Fingerabdruck des vorherigen Eintrags
enthält wie eine Kette. Ändert jemand einen alten Eintrag, passen die
Fingerabdrücke nicht mehr und es fällt auf.
**Und wozu dann noch ein Schlüssel?**
Weil jemand mit Zugriff auf die Datenbank die ganze Kette neu berechnen
könnte. Er ändert also einen Eintrag zum Beispiel einen fehlgeschlagenen
Login-Versuch in einen erfolgreichen und zieht anschließend alle
Fingerabdrücke glatt. Danach sieht die Fälschung echt aus.
Mit `AUDIT_HMAC_KEY` bekommt jeder Eintrag zusätzlich ein **Siegel**, das sich
nur mit diesem Schlüssel erzeugen lässt. Der Schlüssel liegt in der
`.env`-Datei, also **außerhalb der Datenbank**. Wer nur die Datenbank in die
Hände bekommt, kann damit nichts fälschen, ohne dass es auffliegt.
> Bildlich: Die Fingerabdruck-Kette ist die fortlaufende Nummerierung der
> Aktenseiten. Der Schlüssel ist der Stempel des Notars. Die Akte steht zwar
> im Schrank aber den Stempel hat nur der Notar.
**Einrichten**
```bash
# Einmalig pro Umgebung einen Schlüssel erzeugen
openssl rand -hex 32
```
Den Wert in die `.env` der jeweiligen Umgebung eintragen. Wichtig: **pro
Umgebung ein eigener Schlüssel** (Entwicklung, Test, Produktion) und
niemals ins Git-Repository.
**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. |
| Muss ich ihn irgendwo eintragen außer in der `.env`? | Nein. Einmal setzen, Backup anlegen, fertig. |
| Verlangsamt das etwas? | Nein, spürbar nicht. |
**Schlüssel wechseln (`AUDIT_HMAC_KEY_OLD`)**
Möchtest du den Schlüssel austauschen etwa weil du vermutest, dass er in
falsche Hände geraten ist geht das ohne Datenverlust:
```env
AUDIT_HMAC_KEY=<neuer Schlüssel>
AUDIT_HMAC_KEY_OLD=<bisheriger Schlüssel>
```
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.
**Prüfen, ob alles in Ordnung ist**
Die Prüfung läuft derzeit nur über die API (eine Schaltfläche in der Oberfläche
gibt es dafür noch nicht) als angemeldeter Benutzer mit dem Recht
`audit:read`:
```bash
curl -X POST https://crm.deine-domain.de/api/audit-logs/verify \
-H "Authorization: Bearer <Access-Token>"
```
Die Antwort enthält einen Klartext-Satz im Feld `message` und unterscheidet
zwei Dinge:
- **„Manipulierte Einträge"** jemand hat einen bestehenden Eintrag
nachträglich verändert. Das ist ernst.
- **„Strukturelle Lücken"** die Kette hat eine Unterbrechung, die Inhalte
sind aber unverändert. Meist harmlos (z. B. gelöschte alte Einträge).
Notiere dir die Zahl nach dem ersten Deploy: Bleibt sie konstant, ist alles
in Ordnung. Steigt sie, lohnt ein Blick.
<details>
<summary><b>Technische Details</b> (für Entwickler/Admins)</summary>
**Verfahren.** Jeder Audit-Eintrag trägt einen Hash über seine Inhaltsspalten
plus den Hash des Vorgängers (`previousHash`) daraus entsteht die Kette. Es
gibt drei Hash-Versionen, die Spalte `hashVersion` hält fest, welche verwendet
wurde:
| Version | Verfahren | Abdeckung |
|---|---|---|
| 1 | SHA-256 | 7 Felder Altbestand vor der Härtung |
| 2 | SHA-256 | alle 24 Inhaltsspalten, unsigniert |
| 3 | **HMAC-SHA256** mit `AUDIT_HMAC_KEY` | alle 24 Inhaltsspalten, signiert |
Bestandsdaten werden **nicht** nachträglich neu berechnet ein Rehash würde die
Beweiskraft der Vergangenheit überschreiben. Alte Einträge bleiben mit ihrer
Version gültig.
**Version-Floor.** Die zu erwartende Version wird aus der Kette abgeleitet
(erste je mit Version *n* geschriebene Zeile), **nicht** aus der Selbstauskunft
der Zeile. Sonst ließe sich per Downgrade (`hashVersion` 3 → 1) die schwächere
Prüfung erzwingen und anschließend ein gültiger Hash über die wenigen
abgedeckten Felder nachziehen. Zusätzlich gilt: Eine unerklärte Lücke direkt vor
einer signierten Zeile ist ein Befund, kein struktureller Zufall deren
`previousHash` lässt sich ohne Schlüssel nicht fälschen.
**Löschungen.** `POST /api/audit-logs/cleanup` schreibt ein Manifest
(ID-Bereich, Anzahl, Policy, Cutoff) als eigenen verketteten Eintrag. Die
Prüfung liest es aus und meldet nur Lücken **ohne** dokumentierte Löschung als
erklärungsbedürftig (`unexplainedGaps`) sonst könnte sich eine böswillige
Löschung als harmlose Lücke tarnen.
**Rückgabe von `POST /api/audit-logs/verify`:**
| Feld | Bedeutung |
|---|---|
| `tamperedEntries` | Inhalt nachträglich verändert, Versions-Downgrade oder gebrochener Anker **ernst** |
| `chainGaps` | Verkettung unterbrochen (alle) |
| `unexplainedGaps` | Teilmenge davon ohne dokumentierte Löschung |
| `unverifiableEntries` | signiert, aber kein Schlüssel konfiguriert **kein** Manipulationsverdacht |
**Fail-safe.** Ohne `AUDIT_HMAC_KEY` schreibt der Dienst weiter Version 2; das
Logging fällt nie wegen fehlender Konfiguration aus.
**Bekannte Grenze.** Der Anker schützt gegen reinen Datenbank-Schreibzugriff.
Wer Schlüssel *und* Datenbank kontrolliert, kann die Kette konsistent neu
rechnen. Eine Off-Site-Notarisierung (regelmäßiger Export/Versiegelung des
Kettenkopfes außerhalb des Systems) wäre die nächste Stufe.
**Achtung zwei Endpunkte mit Nebenwirkung:**
`POST /api/audit-logs/rehash` berechnet alle Hashes neu; danach meldet die
Prüfung überall „gültig", aber eine bestehende Fälschung würde mitbesiegelt.
`POST /api/audit-logs/cleanup` löscht nach Aufbewahrungsregeln, jede gelöschte
Zeile erzeugt eine Lücke. Beide laufen nur manuell und schreiben einen Marker
ins Log.
</details>
Cloud-Metadata-Endpoints (`169.254.169.254`, `metadata.google.internal` etc.)
sind UNABHÄNGIG vom Flag **immer** geblockt das ist Mindestschutz gegen
AWS/GCP/Azure-IMDS-Diebstahl.
+57 -13
View File
@@ -20,20 +20,64 @@ ENCRYPTION_KEY="32-byte-hex-key-for-aes-256-gcm"
PORT=3001
NODE_ENV=development
# ==================== AUDIT-ANKER ====================
# Signiert Audit-Log-Eintraege per HMAC-SHA256 mit einem Schluessel, der NICHT
# in der Datenbank liegt. Damit reicht ein reiner DB-Schreibzugriff nicht mehr
# aus, um die Hash-Kette glaubwuerdig umzuschreiben (Pentest R166/R167).
# ==================== AUDIT-SIEGEL ====================
# Was ist das Audit-Log?
# OpenCRM protokolliert luekenlos, wer wann welche Daten gesehen oder
# geaendert hat. Das ist die Beweisspur, wenn es Streit gibt oder der
# Datenschutz nachfragt.
#
# Erzeugen: openssl rand -hex 32
# Fail-safe: Ohne Schluessel schreibt das Audit-Log weiter die unsignierte
# Version 2 - es faellt nichts aus, der Anker fehlt nur.
# ACHTUNG: Schluessel sichern. Geht er verloren, sind alle damit signierten
# Eintraege nicht mehr pruefbar (sie gelten dann als "nicht pruefbar",
# NICHT als manipuliert).
# Wogegen schuetzt dieser Schluessel?
# Ohne ihn koennte jemand mit Zugriff auf die Datenbank einen Eintrag
# nachtraeglich umschreiben - zum Beispiel einen fehlgeschlagenen
# Login-Versuch in einen erfolgreichen verwandeln - und die Faelschung so
# glattziehen, dass die Pruefung sie fuer echt haelt.
# Mit dem Schluessel bekommt jeder Eintrag ein Siegel, das sich nur mit
# diesem Schluessel erzeugen laesst. Wer ihn nicht hat, kann nichts
# faelschen, ohne dass es auffliegt. Bildlich: Der Schluessel ist der
# Stempel des Notars - die Akte liegt zwar im Schrank, aber den Stempel
# hat nur der Notar.
#
# Wie einrichten?
# Einmalig einen Schluessel erzeugen und hier eintragen:
# openssl rand -hex 32
# Pro Umgebung ein EIGENER Schluessel (Entwicklung, Test, Produktion).
#
# Was passiert, wenn ich ihn weglasse?
# Nichts faellt aus. Das Audit-Log laeuft normal weiter, nur eben ohne
# dieses zusaetzliche Siegel.
#
# 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.
# Deshalb: Schluessel sichern, so wie ein Passwort.
AUDIT_HMAC_KEY=
# Nur waehrend eines Schluesselwechsels setzen: der vorherige Schluessel wird
# bei der Pruefung zusaetzlich akzeptiert, damit Altbestand ohne Rehash gueltig
# bleibt. Nach dem Wechsel wieder leeren.
# Nur voruebergehend beim Schluesselwechsel setzen.
# 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.
AUDIT_HMAC_KEY_OLD=
# --- Technisch (fuer Entwickler/Admins) ---------------------------------
# Verfahren : HMAC-SHA256 ueber alle Inhaltsspalten eines Audit-Eintrags,
# inkl. previousHash (Verkettung). Entspricht Hash-Version 3.
# Abdeckung : Version 1 = 7 Felder (Altbestand), Version 2 = alle 24
# Inhaltsspalten (SHA-256, unsigniert), Version 3 = wie 2, aber
# HMAC-signiert. Die erwartete Version wird aus der Kette
# abgeleitet (Version-Floor), nicht aus der Selbstauskunft der
# Zeile - sonst liesse sich per Downgrade die schwaechere
# Pruefung erzwingen.
# 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.
# 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)