Files
opencrm/tools/audit-notary/README.md
T
duffyduckandClaude Opus 5 5952fb1894 Gegenbuch: nur vertrauenswuerdige Signaturen, fail-closed ohne Remote (R176)
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>
2026-08-21 20:26:55 +02:00

135 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
## Voraussetzungen, die wirklich Voraussetzungen sind
- **`allowed_signers` muss den Notar-Schlüssel enthalten.** Das Skript
akzeptiert ausschließlich Signaturen, die git als `G` bewertet bei
SSH-Signaturen heißt das wörtlich „Schlüssel steht in `allowed_signers`".
Ein fremder, selbst erzeugter Schlüssel liefert `U` und wird abgelehnt.
*(Bei GPG statt SSH liefert auch ein legitimer Schlüssel ohne Ownertrust ein
`U` dann Ownertrust setzen, nicht `U` wieder 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.
- Optional, aber empfohlen: `NOTARY_SIGNER_FINGERPRINT` auf den Fingerabdruck
des Notar-Schlüssels setzen (`git log -1 --format=%GF`). Dann muss jeder
Commit von genau diesem Schlüssel stammen.
## 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 feststellbar (Remote fehlt oder unerreichbar) |
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
```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.