R175-01 (HIGH): Die erste Fassung signierte zwar, prueft aber nie. Sie las ihre Wahrheit per readFileSync aus der lokalen Arbeitsdatei, nirgends gab es ein git verify-commit - das -S war write-only ohne Konsument. Live reproduziert: DB-Tail abgeschnitten und die lokale Ledger-Zeile angepasst -> "OK, Checkpoint beglaubigt", exit 0, kein Alarm. Fix: Wahrheitsquelle ist der signierte Commit-Baum (bevorzugt der Remote-Kopf); jeder Commit mit Gegenbuch-Aenderung muss eine gueltige Signatur tragen; weicht die Arbeitsdatei vom signierten Stand ab, wird abgebrochen; lokale, nie gepushte Commits gelten nicht als beglaubigt; die Signatur des frisch erzeugten Commits wird gegengeprueft. Dazu ein Pruefmodus --check fuer Auditoren. R175-02 (MEDIUM): /checkpoint fuhr je Aufruf ein volles verifyIntegrity (O(n), 0,85 s bei 16k Zeilen) - authentifizierte DoS-Verstaerkung. Gebraucht wurde nur die Siegel-Wurzel. Jetzt Kopf-Hash aus der Kopfzeile, Wurzel aus dem juengsten gueltigen Marker, sealLeafCount statt des teuren Status. R175-03: writeFileSync lief vor dem Commit, eine verwaiste Zeile wurde vom Folgelauf zementiert. Jetzt Ruecknahme bei Fehlschlag, und NOTARY_SIGN=false verlangt zusaetzlich NOTARY_INSECURE_ACK. Verifiziert mit echtem SSH-Signaturschluessel: stilles Waschen -> Alarm; erfundene Zeile -> Alarm; unsignierter Commit -> Alarm; Commit-Fehlschlag -> zurueckgerollt; Reflex-Schalter verweigert; Pruefmodus haengt nichts an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
4.6 KiB
Markdown
108 lines
4.6 KiB
Markdown
# Gegenbuch (externe Notarisierung)
|
||
|
||
Alle Schutzmechanismen im CRM selbst – Hash-Kette, Versionsgrenze,
|
||
Löschungs-Manifest, Bestandssiegel – liegen in **derselben Datenbank**, die sie
|
||
absichern sollen. Wer dort schreiben kann, sitzt am Ende immer schon auf der
|
||
Ebene, die den Beweis führt. Genau das hat der Pentest über mehrere Runden
|
||
Schicht für Schicht gezeigt.
|
||
|
||
Das Gegenbuch durchbricht das: Ein zweiter Rechner holt regelmäßig einen kurzen
|
||
Kontrollwert vom CRM, prüft ihn gegen seine eigene Historie und hängt ihn
|
||
signiert an ein privates Repository an.
|
||
|
||
## Die eine nicht verhandelbare Bedingung
|
||
|
||
**Der Signaturschlüssel und das Push-Recht dürfen nicht in den Deploy-Secrets
|
||
des CRM liegen.** Sonst ist der zweite Schlüssel im selben Zugriffsbereich wie
|
||
der erste – es sieht nach doppeltem Boden aus und ist keiner. Das CRM braucht
|
||
für dieses Verfahren **gar nichts** zu wissen: Es liefert nur einen lesbaren
|
||
Kontrollwert, der keine Geheimnisse enthält.
|
||
|
||
## Einrichten
|
||
|
||
Auf einem **anderen** Rechner als dem CRM-Server:
|
||
|
||
```bash
|
||
git clone git@host:dein/audit-gegenbuch.git
|
||
cd audit-gegenbuch
|
||
|
||
export CRM_URL=https://crm.example.de
|
||
export CRM_TOKEN=<Access-Token eines Benutzers mit audit:read>
|
||
|
||
node /pfad/zu/notary.mjs
|
||
```
|
||
|
||
Als Cronjob, stündlich:
|
||
|
||
```cron
|
||
0 * * * * cd /pfad/zum/gegenbuch && CRM_URL=https://crm.example.de \
|
||
CRM_TOKEN=... node /pfad/notary.mjs >> notary.log 2>&1
|
||
```
|
||
|
||
**Force-Push serverseitig sperren.** Ohne das ist die Append-only-Eigenschaft
|
||
nur geliehen – wer Schreibrecht auf das Repository erlangt, schreibt die
|
||
Historie sonst einfach um. Bei GitHub/GitLab: Branch-Protection auf `main`.
|
||
|
||
## Der entscheidende Punkt: es wird tatsächlich geprüft
|
||
|
||
Die erste Fassung dieses Skripts hat signiert – aber **nie eine Signatur
|
||
geprüft**. Es las seine Wahrheit aus der lokalen Arbeitsdatei. Wer den
|
||
Notar-Rechner beschreiben konnte, hat ihm damit beliebige „beglaubigte"
|
||
Zeilen untergeschoben und einen abgeschnittenen Datenbestand als neue Wahrheit
|
||
festgeschrieben, ohne Alarm. Signieren ohne prüfenden Leser ist wertlos.
|
||
|
||
Deshalb gilt jetzt:
|
||
|
||
- Wahrheitsquelle ist der **signierte Commit-Baum** (bevorzugt der Remote-Kopf),
|
||
nicht die Arbeitsdatei
|
||
- **Jeder** Commit, der das Gegenbuch ändert, muss eine gültige Signatur tragen
|
||
- Weicht die Arbeitsdatei vom signierten Stand ab, wird abgebrochen
|
||
- Geschrieben wird erst nach erfolgreichem, signiertem Commit – scheitert er,
|
||
wird die Datei zurückgerollt
|
||
|
||
## Prüfmodus für Auditoren
|
||
|
||
```bash
|
||
node notary.mjs --check
|
||
```
|
||
|
||
Führt alle Kontrollen aus, hängt aber nichts an und braucht kein Schreibrecht.
|
||
Geeignet für jemanden, der die Kette unabhängig nachvollziehen will.
|
||
|
||
## Was das Skript erkennt
|
||
|
||
| Angriff | Erkennung |
|
||
|---|---|
|
||
| Beglaubigter Eintrag nachträglich verändert | Hash an der beglaubigten Position stimmt nicht mehr |
|
||
| Einträge am Ende abgeschnitten | aktuelle höchste ID kleiner als die beglaubigte |
|
||
| Bestandssiegel verschwunden | vorher beglaubigt, jetzt nicht mehr vorhanden |
|
||
| Gegenbuch selbst gekürzt | Lücke in der fortlaufenden Nummer |
|
||
| Gegenbuch lokal manipuliert | Arbeitsdatei weicht vom signierten Stand ab |
|
||
| Untergeschobener Commit | Commit ohne gültige Signatur in der Historie |
|
||
| Nie gepushte lokale Commits | Abgleich gegen den Remote-Kopf |
|
||
| Bestandssiegel-Blätter entfernt | beglaubigte Blattzahl auf null gefallen |
|
||
|
||
Bei jedem dieser Fälle bricht das Skript mit **Exit-Code 2** ab und **hängt
|
||
nichts an** – der manipulierte Zustand wird also nicht als neue Wahrheit
|
||
festgeschrieben. Für Cron heißt das: Exit-Code überwachen, sonst geht der Alarm
|
||
im Log unter.
|
||
|
||
## Was es nicht leistet – ehrlich
|
||
|
||
- **Restfenster:** Einträge zwischen dem letzten Kontrollwert und jetzt sind
|
||
noch nicht beglaubigt und am Ende fälsch- oder kürzbar. Häufigerer Lauf
|
||
verkleinert das Fenster, beseitigt es aber nicht. Das gilt für jedes
|
||
periodische Verfahren dieser Art.
|
||
- **Stiller Ausfall:** Läuft der Cronjob nicht mehr, entsteht keine Warnung im
|
||
CRM – dort ist ja nichts konfiguriert. Der Ausfall muss auf dem
|
||
Gegenbuch-Rechner überwacht werden (z. B. Alarm, wenn der jüngste Commit
|
||
älter als zwei Intervalle ist).
|
||
- **`NOTARY_SIGN=false`** existiert nur für Tests ohne hinterlegten Schlüssel
|
||
und verlangt zusätzlich `NOTARY_INSECURE_ACK=…`. Der Reflex „Cron hakt, also
|
||
Signatur abschalten" soll bewusst nicht ausreichen – er würde eine
|
||
ungeschützte Kette festschreiben.
|
||
- **Der Remote-Server muss Force-Push verhindern.** Das Skript prüft die
|
||
Signaturen, aber wenn jemand die Historie am Server überschreiben kann,
|
||
ersetzt er auch die signierten Commits durch eigene. Branch-Protection ist
|
||
Teil des Schutzes, nicht Zubehör.
|