R179-b: Der Rewind-Vorbehalt stand in der Ausgabe, der Exit blieb 0. Die eigene README sagt "fuer Cron gilt: jeder Code ausser 0 gehoert gemeldet" - der Zustand "ich bin an dieser Stelle blind" erreichte die Ueberwachung also nie. Dieselbe Klasse wie R174, eine Ebene hoeher. Fix: Code 0 nur bei belegter Gewissheit (Rewind-Schutz zugesichert oder Mindesthoehe erfuellt), sonst Code 3 - bewusst nicht mit dem Manipulationsalarm 2 verschmolzen. R179-01: NOTARY_MIN_SEQ ist eine Untergrenze, kein Ist-Stand. Ein veralteter Wert liess einen Teil-Rewind darueber lautlos durch, und die blosse Praesenz einer Zahl blendete den Vorbehalt aus - MIN_SEQ=0 war ein Freibrief, ein Tippfehler wurde still verschluckt. Ein veralteter Anker erzeugte damit ein selbstbewussteres Ergebnis als gar keiner. Fix: Der Vorbehalt haengt allein an NOTARY_REWIND_PROTECTED=true und benennt bei gesetztem MIN_SEQ dessen Grenze; MIN_SEQ <= 0 oder unparsbar fuehrt zu exit 1 statt stiller Annahme. Verifiziert gegen den Pentest-Aufbau (10 Checkpoints, Rewind auf 7, DB passend gekuerzt, frischer Klon): nicht gesetzt -> exit 3 (vorher 0); 10 und 8 -> Alarm 2; 7 veraltet -> 0 mit Vorbehalt (vorher ohne); 0 und xyz -> exit 1 (vorher stilles 0); Rewind-Schutz zugesichert -> 0 ohne Vorbehalt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 – das ist Pflicht, nicht Empfehlung.
Konkret muss der Server non-fast-forward-Pushes verbieten (Rewind-Schutz), nicht nur „irgendeine" Branch-Protection. Der Grund ist nicht offensichtlich:
Das Skript prüft jede Signatur. Aber ein Angreifer mit Force-Push-Recht muss gar nichts fälschen – er kann die Reihe schlicht auf einen älteren, echt signierten Stand zurückspulen und die Datenbank passend kürzen. Alle Signaturen bleiben gültig, der Fingerabdruck stimmt, die Nummerierung ist lückenlos. Kryptographisch ist daran nichts auszusetzen; es fehlt nur das Ende.
Ein Notar-Rechner, der die höhere Nummer noch lokal kennt, merkt es. Ein frischer Klon merkt es nicht – und das ist ausgerechnet der Auditoren-Fall.
Deshalb zusätzlich:
# Die zuletzt bekannte Nummer mitgeben – dann fällt ein Rewind auch ohne
# lokalen Zustand auf. Das Skript nennt sie am Ende jedes Laufs.
NOTARY_MIN_SEQ=42 node notary.mjs --check
NOTARY_MIN_SEQ ersetzt die serverseitige Sperre nicht. Der Wert ist eine
Untergrenze und immer nur so frisch wie deine letzte Beobachtung. Wer
stündlich beglaubigt, aber wöchentlich prüft, läuft mit einem Wert herum, der
um rund 168 hinterherhinkt – ein Zurückspulen in dieses Fenster bliebe
unbemerkt und der gewaschene Stand würde sogar ausdrücklich bestätigt.
Ist der Rewind-Schutz auf dem Server tatsächlich eingerichtet, bestätige das:
NOTARY_REWIND_PROTECTED=true
Nur diese Zusicherung blendet den Vorbehalt aus – nicht die bloße Angabe einer Zahl. Solange sie fehlt, endet jeder Lauf mit Code 3 statt 0: „nicht abschließend feststellbar". Das ist Absicht, denn ein Cronjob liest den Rückgabecode, nicht den Fließtext – ein Hinweis, der nur in der Ausgabe steht, erreicht die Überwachung nie.
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
Voraussetzungen, die wirklich Voraussetzungen sind
-
allowed_signersmuss den Notar-Schlüssel enthalten. Das Skript akzeptiert ausschließlich Signaturen, die git alsGbewertet – bei SSH-Signaturen heißt das wörtlich „Schlüssel steht inallowed_signers". Ein fremder, selbst erzeugter Schlüssel liefertUund wird abgelehnt. (Bei GPG statt SSH liefert auch ein legitimer Schlüssel ohne Ownertrust einU– dann Ownertrust setzen, nichtUwieder zulassen.) -
Ein Remote muss erreichbar sein. Ist er es nicht, bricht das Skript mit Code 3 ab, statt dem lokalen Stand zu glauben. Ein Gegenbuch, dessen beglaubigter Stand sich nicht feststellen lässt, gibt kein grünes Licht.
-
Genau ein Schlüssel zählt, nicht die ganze Liste.
allowed_signerskann mehrere Principals enthalten – ein Kollege, ein Alt-Schlüssel, eine global geteilte Datei. Jeder davon würde sonst als „gültig" durchgehen und könnte das Gegenbuch umschreiben. Deshalb prüft das Skript zusätzlich den Fingerabdruck des Signierschlüssels.Er wird automatisch aus
user.signingkeyabgeleitet. Lässt er sich nicht bestimmen, bricht das Skript ab – statt stillschweigend die ganze Liste zu akzeptieren. Explizit setzen geht auch:export NOTARY_SIGNER_FINGERPRINT=$(git log -1 --format=%GF)Wichtig: exakt der
%GF-Wert (SHA256:…), nicht die vollständige Ausgabe vonssh-keygen -lf.
Rückgabecodes
| Code | Bedeutung |
|---|---|
| 0 | alles in Ordnung, Checkpoint angehängt (bzw. Prüfung bestanden) |
| 1 | Betriebsfehler (Konfiguration, Commit oder Push fehlgeschlagen) |
| 2 | Befund – Widerspruch zwischen CRM und Gegenbuch, oder ungültige Signatur |
| 3 | beglaubigter Stand nicht abschließend feststellbar – Remote fehlt/unerreichbar, oder ein Zurückspulen lässt sich nicht ausschließen |
Für Cron gilt: jeder Code außer 0 gehört gemeldet. Code 2 ist der Alarm, Code 3 heißt „ich weiß es nicht" – und das ist ausdrücklich kein Freibrief.
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=falseexistiert nur für Tests ohne hinterlegten Schlüssel und verlangt zusätzlichNOTARY_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.