Files
opencrm/tools/audit-notary
duffyduckandClaude Opus 5 375d4ae1e2 Gegenbuch: verifizierender Leser statt Absichtserklaerung (Pentest R175)
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>
2026-08-21 16:13:54 +02:00
..

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:

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:

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

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.