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) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 12:41:34 +02:00
co-authored by Claude Opus 5
parent ef2411ebe4
commit d2460fa7c0
3 changed files with 73 additions and 7 deletions
+19
View File
@@ -98,6 +98,24 @@ async function main() {
.filter((p) => p.resource === 'audit' || p.resource === 'gdpr') .filter((p) => p.resource === 'audit' || p.resource === 'gdpr')
.map((p) => p.id); .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 // Mitarbeiter: customers + contracts + read auf Stammdaten
const employeePermIds = allPermissions const employeePermIds = allPermissions
.filter( .filter(
@@ -137,6 +155,7 @@ async function main() {
{ name: 'DSGVO', description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung', permIds: gdprPermIds }, { 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', description: 'Kann Kunden und Verträge verwalten', permIds: employeePermIds },
{ name: 'Mitarbeiter (Nur-Lesen)', description: 'Kann nur lesen, keine Änderungen', permIds: readOnlyPermIds }, { 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 }, { name: 'Kunde', description: 'Kann nur eigene Daten lesen', permIds: readOnlyPermIds },
]; ];
+29
View File
@@ -97,6 +97,35 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ 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) - [x] **📤 Eingegrenzter Export lieferte alles (Pentest R186-01, MEDIUM)** (2026-08-26)
- Der Tester fand: `GET /audit-logs/export?userId=…` filterte **nicht** - Der Tester fand: `GET /audit-logs/export?userId=…` filterte **nicht**
`userId=999999` gab alle 2761 Datensätze zurück, byte-identisch zum `userId=999999` gab alle 2761 Datensätze zurück, byte-identisch zum
+25 -7
View File
@@ -186,16 +186,34 @@ jedem Lauf selbst an.
Im CRM, als Administrator: Im CRM, als Administrator:
1. **Rolle anlegen**, z. B. `Gegenbuch` und ihr **ausschließlich** das Recht 1. **Benutzer anlegen**, z. B. `gegenbuch@deine-domain.de`, mit einem langen,
`audit:read` geben. Sonst nichts. zufälligen Passwort.
2. **Benutzer anlegen**, z. B. `gegenbuch@deine-domain.de`, mit dieser Rolle 2. Ihm die Rolle **`Gegenbuch`** geben **nur diese**. Sie bringt genau ein
und einem langen, zufälligen Passwort. Recht mit: `audit:read`.
3. E-Mail und Passwort in die `.env` des Gegenbuchs eintragen 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`). (`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 Mit `audit:read` allein kann dieses Konto **nur Prüfwerte lesen** keine
Kundendaten, keine Verträge, nichts ändern. Selbst wenn die Zugangsdaten Kundendaten, keine Verträge, nichts ändern und nichts versiegeln. Selbst wenn
abhandenkommen, ist damit nichts anzufangen. 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://<crm>/api/audit-logs/checkpoint \
-H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://<crm>/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. Für Produktion und Test jeweils ein eigenes Konto in der jeweiligen Instanz.