diff --git a/README.md b/README.md index dfe0bd4a..2cdb9364 100644 --- a/README.md +++ b/README.md @@ -398,6 +398,27 @@ lässt das Siegel aus. | Muss ich ihn irgendwo eintragen außer in der `.env`? | Nein. Einmal setzen, Backup anlegen, fertig. | | Verlangsamt das etwas? | Nein, spürbar nicht. | +**Wichtig bei einer bestehenden Installation: den Altbestand versiegeln** + +Das Siegel gilt nur für Einträge, die **ab** dem Setzen des Schlüssels +geschrieben werden. Alles, was vorher im Protokoll steht, bleibt ungeschützt – +Änderungen daran wären nicht erkennbar. Die Integritätsprüfung sagt das auch: + + Hinweis: Der Altbestand ist nicht versiegelt – Änderungen daran wären + nicht erkennbar. Behebbar mit POST /api/audit-logs/seal-backlog + {"confirm":"SEAL"}. + +Das ist ein **einmaliger** Schritt: Er zieht ein Siegel über den vorhandenen +Bestand, sodass spätere Änderungen daran auffallen. Der Ablauf – wer es darf +(Haken **„Audit-Betrieb"** in der Benutzerverwaltung), was vorher zu prüfen ist +und wie man das Ergebnis gegenprüft – steht ausführlich in +**[tools/audit-notary/README.md](tools/audit-notary/README.md)**, Abschnitt +*„Wenn der erste Lauf `exit=2` meldet: den Altbestand versiegeln"*. Die +Anleitung gilt auch ohne Gegenbuch. + +Den Zustand siehst du jederzeit unter **Einstellungen → Audit-Protokoll**, ganz +oben. + **Schlüssel wechseln (`AUDIT_HMAC_KEY_OLD`)** Möchtest du den Schlüssel austauschen – etwa weil du vermutest, dass er in diff --git a/tools/audit-notary/README.md b/tools/audit-notary/README.md index da0bbb46..bd8cfea5 100644 --- a/tools/audit-notary/README.md +++ b/tools/audit-notary/README.md @@ -90,53 +90,124 @@ Welche laufen, steuert `COMPOSE_PROFILES` in der `.env`. Ein CRM, das schon länger läuft, hat fast immer einen **Altbestand** – Einträge aus der Zeit, bevor das Protokoll signiert wurde. Solange der nicht versiegelt -ist, meldet die Prüfung `valid: false`, und das Gegenbuch schlägt zu Recht -Alarm. Typischerweise steht dann im Log: +ist, meldet die Prüfung `valid: false`, das Gegenbuch schlägt zu Recht Alarm – +und, wichtig: **es beglaubigt so lange gar nichts.** Es bricht vor dem Anhängen +ab, weil ein Checkpoint über einen ungeklärten Zustand diesen mitbeglaubigen +würde. Ein unversiegeltes CRM ist also nicht „bewacht mit Warnung", sondern +unbewacht. - Hinweis: Der Altbestand ist nicht versiegelt – Änderungen daran wären - nicht erkennbar. +Das Siegeln ist **einmalig** und passiert im CRM, nicht hier. -Das ist **einmalig** zu erledigen, im CRM, nicht hier: +#### Wer darf das + +Das Recht `audit:admin`. Das bekommt man über den Haken **„Audit-Betrieb"** in +der Benutzerverwaltung – **nicht** über die Admin-Rolle (die hat bewusst keine +Audit-Rechte) und **nicht** über den DSGVO-Haken (der darf lesen, nicht +eingreifen). Das Gegenbuch-Dienstkonto hat es erst recht nicht. ```bash -TOKEN=$(curl -s -X POST https:///api/auth/login \ - -H 'Content-Type: application/json' \ - -d '{"email":"…","password":"…"}' | jq -r '.data.token') - -curl -s -X POST https:///api/audit-logs/seal-backlog \ - -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ - -d '{"confirm":"SEAL"}' | jq +CRM=https:// +read -s -p "Passwort: " PASS; echo +AT=$(curl -s -X POST $CRM/api/auth/login -H 'Content-Type: application/json' \ + -d "{\"email\":\"…\",\"password\":\"$PASS\"}" | jq -r '.data.token') +curl -s -X POST $CRM/api/auth/login -H 'Content-Type: application/json' \ + -d "{\"email\":\"…\",\"password\":\"$PASS\"}" | jq -r '.data.user.permissions[]' | grep audit ``` -Dafür braucht es das Recht `audit:admin` – das Gegenbuch-Dienstkonto hat es -absichtlich **nicht**. Nimm dein Administratorkonto. +Muss `audit:read` **und** `audit:admin` zeigen. -**Zuerst nachsehen, was versiegelt wird.** Das Siegel schreibt den aktuellen -Zustand fest, samt aller vorhandenen Lücken. Wer blind siegelt, beglaubigt -gegebenenfalls auch eine Lücke, die von einer Löschung stammt. Deshalb vorher: +#### Erst nachsehen, was du festschreibst + +Das Siegel hält den **aktuellen** Zustand fest, samt aller vorhandenen Lücken. +Wer blind siegelt, beglaubigt womöglich eine Lücke, die von einer Löschung +stammt. Der Schritt ist praktisch einwegs: Danach ist die Beglaubigung tragend, +und ein Siegel wieder zu entfernen erzeugt den Zustand `entfernt` – also selbst +einen Befund. ```bash -curl -s -X POST https:///api/audit-logs/verify \ - -H "Authorization: Bearer $TOKEN" \ - | jq '.data | {valid, chainGaps, unexplainedGaps, tamperedEntries}' +B="Authorization: Bearer $AT" +curl -s -X POST $CRM/api/audit-logs/verify -H "$B" \ + | jq '.data | {valid, checkedCount, rehashes, chainGaps, unexplainedGaps, + tamperedEntries, backlogSealStatus}' ``` -Stehen dort Lücken, sieh dir die betroffenen IDs und ihre Nachbarn an -(`GET /api/audit-logs/`). Liegen sie jeweils **innerhalb einer Sekunde** -zusammen mit ihren Nachbarn und fehlt kein Eintrag (kein `404`), sind es -Schreibkollisionen aus paralleln Anfragen – harmlos. Fehlt dagegen ein Eintrag -oder steht etwas in `tamperedEntries`, **erst klären, dann siegeln**. +Vier Dinge müssen stimmen: -Nach dem Siegeln meldet die Prüfung wieder `valid: true`, und die bekannten -Alt-Lücken erscheinen als **beglaubigt**: +| Feld | Erwartung | Warum | +|---|---|---| +| `rehashes` | `[]` | Eine Neuberechnung verknüpft alles neu – danach ist die Kette *zwangsläufig* stimmig, auch über Löschungen hinweg. Steht hier etwas, sagt `chainGaps` nichts über die Zeit davor aus. | +| `tamperedEntries` | `[]` | Veränderter Inhalt gehört geklärt, nicht beglaubigt. | +| `chainGaps` | erklärbar | siehe unten | +| `backlogSealStatus` | `kein_siegel` | sonst ist es kein Erst-Siegeln (siehe nächster Abschnitt) | - Alle Einträge sind unverändert. Seit dem Bestandssiegel ist keine neue - Lücke entstanden. 6 Lücken stammen aus der Zeit vor dem Bestandssiegel - und sind darin als Vorbefund beglaubigt (IDs …). +**Lücken einordnen.** Sieh dir die betroffenen IDs und ihre Nachbarn an: -Sie verschwinden also nicht aus dem Bericht – sie zählen nur nicht mehr als -offener Befund. Jede **neue** Lücke, jede veränderte oder entfernte Altzeile -und jedes gebrochene Siegel lösen weiterhin sofort Alarm aus. +```bash +for id in …; do + printf '%-6s ' "$id" + curl -s -o /tmp/r.json -w 'HTTP %{http_code} ' "$CRM/api/audit-logs/$id" -H "$B" + jq -r 'if .success then (.data|"\(.createdAt[0:19]) v\(.hashVersion) \(.action)/\(.resourceType)") else .error end' /tmp/r.json +done +``` + +> **Den HTTP-Code mit ausgeben, immer.** Zugangstoken laufen nach 15 Minuten ab. +> Ohne den Code liest sich eine abgelehnte Anfrage (401) wie ein fehlender +> Eintrag – ein Prüfwerkzeug, das ein verweigertes Lesen als Löschung meldet. +> Uns ist genau das passiert, mitten in der Vorbereitung eines Prod-Siegels. + +Liegen die Lücken jeweils **innerhalb einer Sekunde** zusammen mit ihren +Nachbarn, betreffen nur `Authentication` und fehlt kein Eintrag, sind es +Schreibkollisionen aus parallelen Anfragen – harmlos, historisch, nicht mehr +reproduzierbar. Fehlt dagegen ein Eintrag oder passt eine Lücke nicht in dieses +Bild: **erst klären, dann siegeln.** + +*(Ein abgeschnittener **Anfang** des Protokolls wird seit dem Anfangs-Detektor +mitgeprüft: Die erste Zeile eines Protokolls trägt einen leeren `previousHash`; +trägt die erste vorhandene Zeile einen Wert, fehlt eine Vorgängerin. Das +erscheint als ganz niedrige Lücke und kippt `valid` – du musst es nicht selbst +suchen.)* + +#### Siegeln + +```bash +curl -s -X POST $CRM/api/audit-logs/seal-backlog -H "$B" \ + -H 'Content-Type: application/json' -d '{"confirm":"SEAL"}' | jq +``` + +`SEAL` ist das Erst-Siegeln. Besteht bereits ein Siegel, verlangt der Endpunkt +stattdessen `RESEAL` – siehe nächster Abschnitt. Notiere `root` aus der Antwort. + +#### Danach: drei Punkte gegenprüfen + +```bash +curl -s -X POST $CRM/api/audit-logs/verify -H "$B" \ + | jq '.data | {valid, chainGaps, attestedGaps, backlogSealStatus, backlogSealCount}' + +SID=$(curl -s "$CRM/api/audit-logs?resourceType=AuditBacklogSeal&limit=1" -H "$B" | jq -r '.data[0].id') +curl -s "$CRM/api/audit-logs/$SID" -H "$B" \ + | jq '{vorbefund:.data.changesBefore.befund.ketten_luecken, siegel:.data.changesAfter}' +``` + +1. `valid: true`, und `chainGaps` = `attestedGaps` = die bekannten Lücken +2. `backlogSealStatus: "intakt"` – **nicht** `leer`. `leer` heißt: Das Siegel + umschließt nichts, es gab keinen Altbestand. Kein Fehler, aber auch keine + Zusage. +3. Die Lücken stehen im `changesBefore.befund.ketten_luecken` des Markers – + genau daran hängt die Beglaubigung. + +Die Lücken verschwinden also **nicht** aus dem Bericht. Sie zählen nur nicht +mehr als offener Befund. Jede **neue** Lücke, jede veränderte oder entfernte +Altzeile und jedes gebrochene Siegel lösen weiterhin sofort Alarm aus. + +#### Zuletzt: das Gegenbuch quittieren + +Der nächste Lauf meldet **einmal** `exit=2` („Erstmals ein Bestandssiegel +gesetzt") – erwartet, kein Befund. Wurzel aus `data//status.txt` in +`PROD_SEAL_ACK` bzw. `STAGING_SEAL_ACK` eintragen, `docker compose up -d`, nach +dem Lauf mit `exit=0` wieder leeren. Details im nächsten Abschnitt. + +Ab da schreibt das Gegenbuch wieder Checkpoints fort – und setzt im selben Lauf +die Grundlage für die Rehash-Überwachung. ### Wenn sich die Siegelwurzel ändert: Alarm, und warum