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:
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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://<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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user