From d2460fa7c051ebcb87b6805776b384303abb254a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 26 Aug 2026 12:41:34 +0200 Subject: [PATCH] Rolle "Gegenbuch": Leserecht aufs Audit-Protokoll ohne audit:admin Beim Selbst-Nachpruefen eines Deploys auf Staging aufgefallen: Das Gegenbuch-Dienstkonto meldete beim Login audit:read, audit:export, audit:admin, gdpr:export, gdpr:delete und gdpr:admin. Es braucht genau eines davon - audit:read -, denn es ruft nur /audit-logs/checkpoint und /audit-logs/verify auf. Das war kein Bedienfehler, sondern ein Konstruktionsfehler: Es gab keine Rolle, die nur Leserecht aufs Protokoll gibt. Wer das wollte, musste den Haken "DSGVO-Zugriff" setzen - und der vergibt audit:* komplett, also auch audit:admin mit seal-backlog, rehash und cleanup. Das Label ("Audit-Logs, Datenschutz") legt Lesen nahe und liefert Vollzugriff. Warum das ernst ist: Das Passwort des Dienstkontos liegt im Klartext in tools/audit-notary/.env auf der Gegenbuch-Maschine. Mit audit:admin haette ein Einbruch dort nicht nur den Waechter gehabt, sondern gleich die Mittel zur Waesche aus R185-01 - und damit genau die Trennung aufgehoben, wegen der das Gegenbuch auf einer eigenen Maschine laeuft. Neue Rolle "Gegenbuch" in sync-roles.ts mit ausschliesslich audit:read. sync-roles laeuft beim Containerstart mit, die Rolle erscheint danach in der Benutzerverwaltung. README des Gegenbuchs umgeschrieben: Rolle statt "selbst anlegen", ausdrueckliche Warnung vor dem DSGVO-Haken, dazu eine Gegenprobe (checkpoint -> 200, seal-backlog -> 403). Bewusst NICHT angefasst: dass die DSGVO-Rolle selbst audit:admin traegt, ist ein Gewaltenteilungs-Problem - wer das Protokoll beaufsichtigt, kann seine Beweisgrundlage ersetzen. Das zu aendern entzieht bestehenden DSGVO-Konten Rechte und gehoert entschieden, nicht nebenbei gemacht. Co-Authored-By: Claude Opus 5 (1M context) --- backend/prisma/sync-roles.ts | 19 +++++++++++++++++++ docs/todo.md | 29 +++++++++++++++++++++++++++++ tools/audit-notary/README.md | 32 +++++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/backend/prisma/sync-roles.ts b/backend/prisma/sync-roles.ts index 8909971a..1c882601 100644 --- a/backend/prisma/sync-roles.ts +++ b/backend/prisma/sync-roles.ts @@ -98,6 +98,24 @@ async function main() { .filter((p) => p.resource === 'audit' || p.resource === 'gdpr') .map((p) => p.id); + // Gegenbuch: NUR audit:read. + // + // Der externe Notar ruft genau zwei Endpunkte auf, /audit-logs/checkpoint + // und /audit-logs/verify, und beide verlangen audit:read. Bis hierher gab es + // dafuer keine passende Rolle: Wer dem Dienstkonto Leserechte aufs Protokoll + // geben wollte, musste den DSGVO-Haken setzen - und der vergibt `audit:*` + // KOMPLETT, also auch `audit:admin` mit seal-backlog, rehash und cleanup. + // + // Damit haette ein Einbruch auf der Gegenbuch-Maschine nicht nur den + // Waechter gehabt, sondern gleich die Mittel, das Bewachte umzuschreiben - + // genau die Waesche aus R185-01, und genau die Trennung, wegen der das + // Gegenbuch ueberhaupt auf einer eigenen Maschine laeuft. Das Kennwort des + // Dienstkontos liegt dort im Klartext in der .env; es muss deshalb so wenig + // wert sein wie moeglich. + const gegenbuchPermIds = allPermissions + .filter((p) => p.resource === 'audit' && p.action === 'read') + .map((p) => p.id); + // Mitarbeiter: customers + contracts + read auf Stammdaten const employeePermIds = allPermissions .filter( @@ -137,6 +155,7 @@ async function main() { { name: 'DSGVO', description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung', permIds: gdprPermIds }, { name: 'Mitarbeiter', description: 'Kann Kunden und Verträge verwalten', permIds: employeePermIds }, { name: 'Mitarbeiter (Nur-Lesen)', description: 'Kann nur lesen, keine Änderungen', permIds: readOnlyPermIds }, + { name: 'Gegenbuch', description: 'Darf das Audit-Protokoll nur lesen und prüfen – sonst nichts', permIds: gegenbuchPermIds }, { name: 'Kunde', description: 'Kann nur eigene Daten lesen', permIds: readOnlyPermIds }, ]; diff --git a/docs/todo.md b/docs/todo.md index 6914c759..fb873384 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,35 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔑 Rolle „Gegenbuch": Leserecht aufs Audit-Protokoll ohne `audit:admin`** (2026-08-26) + - **Beim Selbst-Nachprüfen eines Deploys aufgefallen:** Das Gegenbuch- + Dienstkonto auf Staging meldete beim Login + `["audit:read","audit:export","audit:admin","gdpr:export","gdpr:delete","gdpr:admin"]`. + Es braucht **genau eines** davon – `audit:read` – denn es ruft nur + `/audit-logs/checkpoint` und `/audit-logs/verify` auf. + - **Kein Bedienfehler, ein Konstruktionsfehler:** Es gab keine Rolle, die + nur Leserecht aufs Protokoll gibt. Wer das wollte, musste den Haken + „DSGVO-Zugriff" setzen – und der vergibt `audit:*` komplett, also auch + `audit:admin` mit `seal-backlog`, `rehash`, `cleanup`. Das Label + („Audit-Logs, Datenschutz") legt Lesen nahe und liefert Vollzugriff. + - **Warum das ernst ist:** Das Passwort des Dienstkontos liegt im Klartext + in `tools/audit-notary/.env` auf der Gegenbuch-Maschine. Mit `audit:admin` + hätte ein Einbruch dort nicht nur den Wächter gehabt, sondern gleich die + Mittel zur Wäsche aus R185-01 – und damit genau die Trennung aufgehoben, + wegen der das Gegenbuch überhaupt auf einer eigenen Maschine läuft. + - Neue Rolle **`Gegenbuch`** in `sync-roles.ts` mit ausschließlich + `audit:read`. Läuft beim Containerstart mit, erscheint danach in der + Benutzerverwaltung. README des Gegenbuchs umgeschrieben: Rolle statt + „selbst anlegen", ausdrückliche Warnung vor dem DSGVO-Haken, plus eine + Gegenprobe (`checkpoint` → 200, `seal-backlog` → 403). + - **Offen, bewusst nicht angefasst:** Dass die DSGVO-Rolle selbst + `audit:admin` trägt, ist ein Gewaltenteilungs-Problem – wer das Protokoll + beaufsichtigt, kann seine Beweisgrundlage ersetzen. Ändern hieße + bestehenden DSGVO-Konten Rechte entziehen; gehört entschieden, nicht + nebenbei gemacht. An den Pentester gemeldet, dessen nächstes Thema das + Rechtemodell ist. + - Dateien: `backend/prisma/sync-roles.ts`, `tools/audit-notary/README.md` + - [x] **📤 Eingegrenzter Export lieferte alles (Pentest R186-01, MEDIUM)** (2026-08-26) - Der Tester fand: `GET /audit-logs/export?userId=…` filterte **nicht** – `userId=999999` gab alle 2761 Datensätze zurück, byte-identisch zum diff --git a/tools/audit-notary/README.md b/tools/audit-notary/README.md index 4bbd5b4f..b8f1ad3c 100644 --- a/tools/audit-notary/README.md +++ b/tools/audit-notary/README.md @@ -186,16 +186,34 @@ jedem Lauf selbst an. Im CRM, als Administrator: -1. **Rolle anlegen**, z. B. `Gegenbuch` – und ihr **ausschließlich** das Recht - `audit:read` geben. Sonst nichts. -2. **Benutzer anlegen**, z. B. `gegenbuch@deine-domain.de`, mit dieser Rolle - und einem langen, zufälligen Passwort. -3. E-Mail und Passwort in die `.env` des Gegenbuchs eintragen +1. **Benutzer anlegen**, z. B. `gegenbuch@deine-domain.de`, mit einem langen, + zufälligen Passwort. +2. Ihm die Rolle **`Gegenbuch`** geben – **nur diese**. Sie bringt genau ein + Recht mit: `audit:read`. +3. Zusätzlich **„Dienstkonto"** ankreuzen. Dann gelten seine Anmeldungen als + Routine statt als kritisches Ereignis – und sein *Ausbleiben* wird gemeldet. +4. E-Mail und Passwort in die `.env` des Gegenbuchs eintragen (`PROD_CRM_EMAIL` / `PROD_CRM_PASSWORD`). +> **Nicht den DSGVO-Haken benutzen.** Der sieht naheliegend aus („Audit-Logs, +> Datenschutz"), vergibt aber `audit:` **komplett** – einschließlich +> `audit:admin` mit `seal-backlog`, `rehash` und `cleanup`. Ein Einbruch auf +> dieser Maschine hätte damit nicht nur den Wächter, sondern gleich die Mittel, +> das Bewachte umzuschreiben. Das Passwort steht hier im Klartext in der +> `.env`; es muss so wenig wert sein wie möglich. + Mit `audit:read` allein kann dieses Konto **nur Prüfwerte lesen** – keine -Kundendaten, keine Verträge, nichts ändern. Selbst wenn die Zugangsdaten -abhandenkommen, ist damit nichts anzufangen. +Kundendaten, keine Verträge, nichts ändern und nichts versiegeln. Selbst wenn +die Zugangsdaten abhandenkommen, ist damit nichts anzufangen. + +Gegenprobe nach dem Einrichten – die erste Zeile muss 200 geben, die zweite 403: + +```bash +curl -s -o /dev/null -w '%{http_code}\n' https:///api/audit-logs/checkpoint \ + -H "Authorization: Bearer $TOKEN" +curl -s -o /dev/null -w '%{http_code}\n' -X POST https:///api/audit-logs/seal-backlog \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' +``` Für Produktion und Test jeweils ein eigenes Konto in der jeweiligen Instanz.