Versiegeln dokumentiert: Abschnitt aktualisiert und im Haupt-README verlinkt
Der Abschnitt in tools/audit-notary/README.md war an drei Stellen ueberholt. Er sagte "nimm dein Administratorkonto" - seit der Aufteilung von DSGVO und Audit-Betrieb hat die Admin-Rolle gar keine Audit-Rechte mehr, das Recht kommt jetzt ueber den Haken "Audit-Betrieb". Er kannte weder das RESEAL-Gate noch die Rehash-Anzeige noch den Zustand "leer". Neu geschrieben, mit dem, was die Praxis gezeigt hat: - Wer darf es, und warum nicht ueber DSGVO oder Admin - Vier Felder, die vor dem Siegeln stimmen muessen - allen voran rehashes:[], denn nach einer Neuberechnung sagt chainGaps nichts mehr ueber die Zeit davor - Den HTTP-Code beim Pruefen IMMER mit ausgeben: Token laufen nach 15 Minuten ab, und ohne den Code liest sich eine 401 wie ein fehlender Eintrag. Genau das ist uns bei der Vorbereitung des Prod-Siegels passiert und haette fast zu einem falschen Stopp gefuehrt. - Die drei Punkte zur Gegenpruefung danach, inklusive "intakt" statt "leer" - Der Hinweis, dass ein unversiegeltes CRM nicht "bewacht mit Warnung" ist, sondern unbewacht: Das Gegenbuch bricht vor dem Anhaengen ab. Ausserdem im Haupt-README verlinkt. Wer AUDIT_HMAC_KEY auf einer bestehenden Installation setzt, hat zwangslaeufig einen Altbestand - erfuhr davon aber nur, wenn er ein Gegenbuch betreibt. Die Anleitung gilt auch ohne. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+104
-33
@@ -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://<crm>/api/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"…","password":"…"}' | jq -r '.data.token')
|
||||
|
||||
curl -s -X POST https://<crm>/api/audit-logs/seal-backlog \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"confirm":"SEAL"}' | jq
|
||||
CRM=https://<crm>
|
||||
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://<crm>/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/<id>`). 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 <lücke-1> <nachbar> …; 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/<instanz>/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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user