(a) Probe-Push verworfen. Er haette nur den geprobten Ref beurteilt, die eigene
Push-Identitaet gemessen statt die des Angreifers (Bypass-Rechte fuer Admins
gehen genau dann auseinander, wenn es zaehlt), nur einen Zeitpunkt abgedeckt -
und einen zerstoerungsfreien Force-Push gibt es nicht: die bestaetigende
Beobachtung waere derselbe Vorgang wie der Schaden.
Stattdessen ein Fast-Forward-Waechter: Der beobachtete Remote-Kopf wird
ausserhalb des Klons festgehalten; beim naechsten Lauf muss der neue Kopf ein
Nachfahre des alten sein. Das erkennt das Ereignis statt die Regel abzufragen
und wirkt unabhaengig von serverseitigem Schutz. Ein belegter Fast-Forward
gilt als Nachweis und blendet den Rewind-Vorbehalt aus.
(b) Code 3 als Tor vor dem Anhaengen statt als Status danach: Der Schreiblauf
signiert mit dem neuen Checkpoint zugleich ueber den Bestand darunter - ist die
Basis ungeklaert, waere das Anhaengen selbst das Waschmittel. Grundlage nicht
feststellbar -> nichts anhaengen, exit 3. Erster Lauf -> Basislinie, ehrlich
gemeldet, exit 3. Anhaengen geklappt, Push gescheitert -> exit 3 mit "erstellt,
aber NICHT verankert". Nur Anhaengen + Push + belegte Verankerung -> exit 0.
Verifiziert: Basislinie exit 3; Folgelauf exit 0 ohne Vorbehalt; Rewind aus
einem frischen Auditoren-Klon ohne MIN_SEQ und ohne Zusicherung -> Alarm exit 2
(bisher stilles Gruen); kaputtes Push-Ziel -> "erstellt, aber nicht verankert"
exit 3, Folgelauf haelt den ungepushten Commit fail-closed an.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Das Signatur-Gate faengt Force-Push mit fremder oder unsignierter Historie -
aber ein Rewind auf einen aelteren, echt signierten Stand ist signaturseitig
einwandfrei. Angreifer spult origin/main auf einen frueheren Checkpoint zurueck
und kuerzt die Datenbank passend: alle Signaturen G, Pin korrekt, Reihe
lueckenlos. Ein Notar-Klon mit lokalem Vorlauf merkt es, ein frischer
Auditoren-Klon meldete OK - ausgerechnet im dokumentierten Pruef-Fall.
Das laesst sich im Skript nicht kryptographisch erkennen, die Historie ist echt.
Deshalb zwei Dinge statt eines Scheinfixes: NOTARY_MIN_SEQ als Bezugspunkt (ist
die Reihe kuerzer, Alarm; jeder Lauf nennt die Nummer), und ohne diesen
Bezugspunkt sagt die Erfolgsmeldung ausdruecklich, dass ein Zurueckspulen nicht
erkennbar war.
README: serverseitiger Rewind-Schutz (non-fast-forward verbieten) jetzt als
Pflicht formuliert, samt Begruendung und dem Hinweis, dass ein frischer Klon
den Rewind nicht sieht.
Kleinkram: NOTARY_SIGNER_FINGERPRINT wird beim Einlesen getrimmt (ein
Zeilenumbruch loeste 4/4 Fehlalarme aus, die auf den korrekten Fingerabdruck
zeigten); die "nie gepusht"-Meldung priorisiert Untersuchen statt Pushen.
Beim Testen selbst gefunden: der erste git-Aufruf war ungeschuetzt und warf bei
kaputtem Klon einen Stacktrace.
Verifiziert: Rewind 3->1 mit gekuerzter DB -> frischer Klon ohne Bezugspunkt OK
mit Vorbehalt, mit NOTARY_MIN_SEQ=3 -> Alarm exit 2; Pin mit Zeilenumbruch kein
Fehlalarm; regulaerer Lauf unveraendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R177-01 (MEDIUM): Der Pin war optional. Ohne ihn war der Vertrauensanker die
gesamte allowed_signers-Menge, nicht der eine Notar-Schluessel: ein zweiter
dort gelisteter Schluessel konnte das Gegenbuch waschen und force-pushen, und
%G? war G. R176-01 hatte "jeder selbst erzeugte Schluessel" geschlossen, "jeder
erlaubte Schluessel" blieb offen. Fix: Pin wird aus user.signingkey abgeleitet;
laesst er sich nicht bestimmen, wird abgebrochen statt die ganze Liste zu
akzeptieren.
R177-02 (MEDIUM): Der Schreib-Lauf prueft den frischen Commit nur auf %G?,
nicht auf den Pin. Ein Notar-Host mit falsch konfiguriertem Schluessel meldete
"beglaubigt" und pushte - und ab da war die Kette dauerhaft rot, behebbar nur
per Force-Push, den die Branch-Protection gerade verhindern soll. Fix:
Pin-Abgleich am frischen Commit vor dem Push, bei Abweichung Ruecknahme.
Kleinkram: NOTARY_ALLOW_LOCAL faerbt Erfolgsmeldungen ein und pusht nicht mehr
ins Leere; CRM-Fehler liefern eine erklaerende Zeile statt Node-Stacktrace.
Verifiziert mit drei SSH-Schluesseln gegen echten Remote: keyC-Angriff -> Alarm
exit 2 auch ohne gesetzten Pin; Schreiblauf mit falschem Schluessel ->
zurueckgerollt, nichts gepusht; CRM nicht erreichbar/401 -> saubere Meldung;
Lokalmodus eingefaerbt; saubere Historie ohne Fehlalarm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R176-01 (HIGH): Das Signatur-Gate akzeptierte %G? = G ODER U. Bei
SSH-Signaturen bedeutet U woertlich "gute Signatur, aber kein passender
Principal" - der Schluessel steht also NICHT in allowed_signers. Damit
passierte jeder selbst erzeugte Schluessel das Gate und der einzige
In-System-Vertrauensanker war wirkungslos. End-to-end reproduziert: Gegenbuch
mit fremdem Schluessel re-signiert und force-gepusht -> "OK", exit 0.
Vorbedingung war nur Remote-Schreibrecht, kein Host-Zugriff.
Fix: nur G an beiden Stellen, dazu optionales Pinnen des erwarteten
Signierschluessels ueber NOTARY_SIGNER_FINGERPRINT (%GF).
R176-02 (MEDIUM): War der Remote unerreichbar, fiel der Ablauf still auf HEAD
zurueck und die "nie gepusht"-Pruefung wurde uebersprungen - ausgerechnet unter
der Bedingung, die einen Push-Fehlschlag verursacht. --check meldete waehrend
eines Ausfalls gruenes Licht auf nicht notarisiertem Zustand. Fix: fail-closed
mit Code 3, ebenso ohne konfigurierten Remote (Testlauf nur mit
NOTARY_ALLOW_LOCAL=true).
Nebenbei: git-eigene Fehlermeldungen standen vor der eigenen Erklaerung, stderr
wird jetzt abgefangen und gezielt weitergereicht.
Verifiziert mit zwei SSH-Schluesseln gegen echten Remote: Angriff mit fremdem
Key + Force-Push -> Alarm exit 2 (vorher OK); Remote unerreichbar -> exit 3;
kein Remote -> exit 3; falscher Fingerabdruck-Pin -> Alarm; saubere Historie
ohne Fehlalarm. Rueckgabecodes in der README dokumentiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Abschluss der Anker-Kette. Alle bisherigen Schutzebenen liegen in derselben
Datenbank, die sie absichern sollen - der Pentest hat das ueber mehrere Runden
Schicht fuer Schicht gezeigt, zuletzt in R174-01 am Siegel-Marker selbst.
Aufteilung nach der Analyse des Pentesters (der Schutz kommt vom Ort, nicht von
der Signatur): Das CRM liefert nur einen lesbaren Kontrollwert ohne Geheimnisse
(GET /api/audit-logs/checkpoint, audit:read). Signiert, zeitgestempelt und
angehaengt wird auf einem anderen Rechner - Schluessel und Push-Recht liegen
nicht in den Deploy-Secrets des CRM. Ohne diese Trennung waere es D1 nochmal,
nur schlimmer: sieht nach doppeltem Boden aus, tut still nichts.
Der Kontrollwert enthaelt bewusst maxId. Ein blosser Kopf-Hash erkennt
Umschreiben, aber kein Abschneiden am Ende - genau die R174-01-Klasse, eine
Ebene hoeher. atId erlaubt der Gegenstelle, einen frueher beglaubigten Kopf
erneut abzufragen und nachzurechnen.
Gegenstelle: tools/audit-notary/notary.mjs (Cron auf zweitem Rechner, privates
Git-Repo als Append-only-Ablage, signierte Commits). Prueft vor dem Anhaengen
und bricht bei Widerspruch mit Exit-Code 2 ab, ohne zu schreiben.
Verifiziert gegen eine CRM-Attrappe mit echter DB: beglaubigte Zeile veraendert
-> Alarm; am Ende abgeschnitten (maxId 5->4) -> Alarm; Gegenbuch selbst
gekuerzt (seq-Luecke) -> Alarm; in allen Faellen nichts angehaengt.
Ehrlich dokumentiert: Restfenster zwischen zwei Laeufen bleibt und ist
inhaerent; ein stiller Cron-Ausfall erzeugt im CRM keine Warnung und muss auf
dem Gegenbuch-Rechner ueberwacht werden; Force-Push muss serverseitig gesperrt
sein, sonst ist Append-only nur geliehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>