Zwei Luecken, beide beim Nachlesen aufgefallen: 1. Die Haupt-README erwaehnte das Gegenbuch mit keinem Wort - ausgerechnet dort, wo der Betreiber nach einem git pull nachschaut. Jetzt ein Abschnitt direkt nach dem Audit-Siegel: wozu es gut ist, die Richtung der Verbindung (Gegenbuch holt, CRM kennt es nicht), die vier Befehle zum Einrichten und der Verweis auf die ausfuehrliche Anleitung. Ausdruecklich als optional gekennzeichnet - ohne Gegenbuch bleibt der Schutz in der Anwendung vollstaendig. Projektbaum ergaenzt. 2. Die Notar-README stammte aus der Zeit mit externem Git-Server. Fuer den lokalen Betrieb standen darin Anforderungen, die es dort gar nicht gibt - "Force-Push serverseitig sperren, das ist Pflicht" ist ohne Server sinnlos und verwirrt. Jetzt eine Entscheidungstabelle ganz oben (lokal = Normalfall vs. externes Repository = zusaetzliche Haertung), und alles Server-Bezogene gesammelt unter einer eigenen Ueberschrift mit dem Hinweis, dass es im lokalen Betrieb uebersprungen werden kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
404 lines
18 KiB
Markdown
404 lines
18 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 schreibt
|
||
ihn signiert fort. Wird später im CRM etwas nachträglich verändert,
|
||
widerspricht das dem Gegenbuch.
|
||
|
||
## Zwei Betriebsarten – erst hier entscheiden
|
||
|
||
| | **Lokal** (Normalfall) | **Mit externem Repository** |
|
||
|---|---|---|
|
||
| Das Buch liegt | auf dem Gegenbuch-Rechner | zusätzlich auf einem dritten Server |
|
||
| Aufwand | Docker starten, fertig | privates Git-Repo, Schlüssel, Server-Regeln |
|
||
| Schützt gegen | jemand verändert Daten **im CRM** | zusätzlich: jemand übernimmt den **Gegenbuch-Rechner** |
|
||
|
||
**Für die allermeisten Installationen ist „lokal" die richtige Wahl.** Der
|
||
Schutz, um den es geht – nachträgliche Änderungen im CRM auffliegen zu lassen –
|
||
steht damit vollständig. Die zweite Variante deckt einen Angreifer ab, der
|
||
zusätzlich den Gegenbuch-Rechner übernimmt; sie kostet spürbar mehr Einrichtung
|
||
und laufende Aufmerksamkeit.
|
||
|
||
Diese Anleitung beschreibt zuerst den lokalen Betrieb. Alles zur zweiten
|
||
Variante steht gesammelt unter **„Zusätzliche Härtung"** weiter unten – wer
|
||
lokal betreibt, kann diesen ganzen Teil überspringen.
|
||
|
||
## 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 mit Docker (empfohlen)
|
||
|
||
Auf dem **Gegenbuch-Rechner** – nicht auf dem CRM-Server:
|
||
|
||
```bash
|
||
git clone <dieses Repository> opencrm
|
||
cd opencrm/tools/audit-notary
|
||
cp .env.example .env
|
||
# .env ausfüllen: CRM-Adresse und Token eintragen
|
||
docker compose up -d
|
||
```
|
||
|
||
Beim ersten Start einmalig `PROD_GENESIS_ACK=true` setzen (und danach wieder
|
||
leeren) – die erste Eintragung legt fest, was als Ausgangszustand gilt, und das
|
||
soll nicht versehentlich passieren.
|
||
|
||
**Zwei Bücher auf einer Maschine** sind vorgesehen: `prod` und `staging` sind
|
||
getrennte Dienste mit getrennten Verzeichnissen und getrennten Schlüsseln.
|
||
Welche laufen, steuert `COMPOSE_PROFILES` in der `.env`.
|
||
|
||
### Wer redet mit wem
|
||
|
||
```
|
||
Gegenbuch ──holt lesend──> OpenCRM (HTTPS, Token nur mit audit:read)
|
||
OpenCRM ─────────────────> (kennt das Gegenbuch nicht)
|
||
```
|
||
|
||
Das ist der eigentliche Schutz. Das Gegenbuch **holt** – es lässt sich nichts
|
||
schicken. OpenCRM kennt weder Adresse noch Schlüssel des Gegenbuchs. Wer
|
||
OpenCRM übernimmt, kommt hier nicht heran.
|
||
|
||
Das Token kann ausschließlich Prüfwerte lesen: keine Kundendaten, keine
|
||
Änderungen. Selbst wenn es abhandenkommt, ist damit nichts anzufangen.
|
||
|
||
Der Signaturschlüssel wird beim ersten Start **auf dem Gegenbuch-Rechner
|
||
erzeugt** und verlässt ihn nie.
|
||
|
||
### Wo die Daten liegen
|
||
|
||
Im Projektverzeichnis, nicht in Docker-Volumes:
|
||
|
||
```
|
||
tools/audit-notary/data/prod/
|
||
buch/ das Gegenbuch selbst (ein Git-Repository)
|
||
schluessel/ Signaturschlüssel – wird beim ersten Start erzeugt
|
||
beobachtungen.jsonl was zuletzt gesehen wurde
|
||
status.txt Ergebnis des letzten Durchlaufs
|
||
```
|
||
|
||
Der Inhalt ist vom Repository ausgenommen – der Signaturschlüssel gehört dort
|
||
nicht hinein. Die Verzeichnisse selbst sind über `.gitkeep` vorhanden, damit
|
||
sie nach einem `git clone` schon existieren und Docker sie nicht als `root`
|
||
anlegt.
|
||
|
||
**Ins Backup gehört genau dieses Verzeichnis.** Geht es verloren, beginnt die
|
||
Beobachtung von vorn – und der nächste Lauf sagt das ausdrücklich, statt „alles
|
||
gut" zu melden.
|
||
|
||
### Überwachung
|
||
|
||
Jeder Durchlauf schreibt seinen Stand nach `data/<instanz>/status.txt`:
|
||
|
||
```
|
||
2026-08-22T16:49:50+00:00 exit=0 in Ordnung
|
||
```
|
||
|
||
**Alles außer `exit=0` gehört angesehen.** Wer eine Überwachung hat, greift
|
||
diese Datei ab; wer keine hat, schaut regelmäßig mit `docker compose logs`
|
||
hinein. Ein Alarm, den niemand liest, ist keiner.
|
||
|
||
### Was dieser Betrieb abdeckt – und was nicht
|
||
|
||
**Abgedeckt:** Jemand verändert oder löscht nachträglich Einträge im CRM –
|
||
auch mit direktem Datenbankzugriff. Das widerspricht dem Gegenbuch und fällt
|
||
beim nächsten Durchlauf auf.
|
||
|
||
**Nicht abgedeckt:** Jemand übernimmt den Gegenbuch-Rechner selbst. Dagegen
|
||
hülfe nur eine zusätzliche Ablage außerhalb (z. B. ein privates Git-Repository
|
||
auf einem dritten Server) – das ist vorbereitet, aber für die meisten
|
||
Installationen mehr Aufwand als Nutzen. Die Erfolgsmeldung benennt diese Grenze
|
||
bei jedem Lauf, damit sie nicht in Vergessenheit gerät.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
# Zusätzliche Härtung: externes Repository
|
||
|
||
> **Alles ab hier gilt nur für die zweite Betriebsart.** Wer das Gegenbuch
|
||
> lokal auf einer eigenen Maschine betreibt – der Normalfall, siehe oben – kann
|
||
> diesen gesamten Abschnitt überspringen. Die Anforderungen darin (Git-Server,
|
||
> Rewind-Sperre, geschützte Refs) beziehen sich auf ein zusätzliches Repository
|
||
> auf einem dritten Server und existieren im lokalen Betrieb nicht.
|
||
|
||
Sinn der Variante: Beim lokalen Betrieb liegt das Buch auf demselben Rechner
|
||
wie der Signaturschlüssel. Wer diesen Rechner übernimmt, kann beides
|
||
manipulieren. Ein zusätzliches Repository auf einem dritten Server, das nur
|
||
Anhängen erlaubt, schließt auch das – vorausgesetzt, dessen Regeln stimmen.
|
||
|
||
## Einrichten ohne Docker
|
||
|
||
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 – 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:
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
## Der Rewind-Wächter
|
||
|
||
Das Skript merkt sich bei jedem Lauf den beobachteten Remote-Kopf in einer
|
||
Datei **außerhalb des Klons** (Standard `~/.opencrm-notary/beobachtungen.jsonl`,
|
||
per `NOTARY_STATE_FILE` änderbar). Beim nächsten Lauf muss der neue Kopf ein
|
||
Nachfahre des alten sein. Ist er das nicht, wurde zurückgespult – und zwar
|
||
**unabhängig davon, ob es einen serverseitigen Schutz gibt**.
|
||
|
||
Das ist der Grund, warum hier *kein* Probe-Push stattfindet: Ein solcher Test
|
||
würde die eigene Push-Identität messen, nicht die des Angreifers (Bypass-Rechte
|
||
für Administratoren gehen genau dann auseinander, wenn es zählt), er gälte nur
|
||
für den geprobten Ref, und die bestätigende Beobachtung wäre derselbe Vorgang
|
||
wie der Schaden. Deshalb wird das **Ereignis** erkannt statt die Regel
|
||
abgefragt.
|
||
|
||
### Das Gedächtnis liegt im Server, nicht nur auf diesem Rechner
|
||
|
||
Jeder verankerte Checkpoint bekommt zusätzlich einen eigenen unveränderlichen
|
||
Ref `refs/notary/seq-N`. Der überlebt einen Force-Push auf `main`, weil er ein
|
||
**anderer Ref** ist. Damit lässt sich die höchste je existierende Nummer aus dem
|
||
Server rekonstruieren – auch wenn der lokale Beobachtungsspeicher fehlt.
|
||
|
||
Warum das nötig ist: Läge das Gedächtnis nur lokal, müsste ein Angreifer nach
|
||
einem Zurückspulen bloß diese Datei löschen. Der nächste Lauf meldete dann
|
||
„erste Beobachtung" – **ein einziges** Code-3, das im Alltag neben echten
|
||
Remote-Ausfällen untergeht – und würde anschließend den zurückgespulten Stand
|
||
als Ausgangspunkt übernehmen. Ab da wäre alles dauerhaft grün. Ein gutartiger
|
||
Standardzustand, den ein Angreifer gezielt herstellen kann, ist kein sicherer
|
||
Standardzustand.
|
||
|
||
> ### ⚠ Ohne serverseitigen Schutz dieser Refs bringt das Verfahren nichts
|
||
>
|
||
> `refs/notary/*` muss auf dem Server **gegen Löschen und Überschreiben**
|
||
> gesperrt sein. Das ist keine Empfehlung, sondern die tragende Voraussetzung –
|
||
> genauso wie die Rewind-Sperre auf `main`.
|
||
>
|
||
> Der Grund ist eine unangenehme Asymmetrie: Das Skript kennt nur den
|
||
> **höchsten noch vorhandenen** Anker, nicht den höchsten je erzeugten. Wer
|
||
> *alle* Anker löscht, fliegt auf (Code 4). Wer nur die **obersten** löscht und
|
||
> einen niedrigeren stehen lässt, senkt damit den Vergleichswert selbst – und
|
||
> in-repo ist das von „gab es nie" nicht zu unterscheiden.
|
||
>
|
||
> **Die Notar-Identität braucht dort eine eng gefasste Ausnahme:** Sie muss
|
||
> `refs/notary/*` **anlegen** dürfen, aber ausdrücklich **nicht löschen oder
|
||
> überschreiben**. Sonst hebelt eine kompromittierte Notar-Identität genau den
|
||
> Schutz aus, um den es hier geht. Server-seitig ist das unterscheidbar – ein
|
||
> `pre-receive`-Hook sieht beim Anlegen die Null-OID als alten Wert:
|
||
>
|
||
> ```sh
|
||
> # refs/notary/*: nur anlegen, niemals ändern oder löschen
|
||
> case "$ref" in refs/notary/*)
|
||
> [ "$old" = "0000000000000000000000000000000000000000" ] || exit 1 ;;
|
||
> esac
|
||
> ```
|
||
>
|
||
> Deshalb begründen Anker **keine** Unversehrtheit. Sie können ein
|
||
> Zurückspulen *widerlegen* (wenn ein hoher Anker nicht mehr im Kopf steckt),
|
||
> aber nie belegen, dass nichts fehlt. Ein `--check` aus frischem Klon erkennt
|
||
> eine Anker-Löschung **nicht** – es meldet dann Code 3 mit Vorbehalt, nicht 0.
|
||
|
||
### Wenn das Gedächtnis trotzdem fehlt
|
||
|
||
Das Skript unterscheidet zwei Fälle – der Unterschied entscheidet alles:
|
||
|
||
| Lage | Bedeutung | Verhalten |
|
||
|---|---|---|
|
||
| kein Speicher, **keine** notarisierte Historie | echte Erstinbetriebnahme | einmalig `NOTARY_GENESIS_ACK=true`, sonst Code 4 |
|
||
| kein Speicher, **aber** Historie vorhanden | Gedächtnis ging *nach* der Etablierung verloren | **Code 4**, keine stille Übernahme; erst nach `NOTARY_ADOPT_ACK=true` |
|
||
|
||
Eine Baseline zu setzen heißt „ich verbürge mich, *das* ist der wahre Kopf".
|
||
Dieser Akt wird nicht automatisch wiederholt.
|
||
|
||
Zwei Konsequenzen für den Betrieb:
|
||
|
||
- **Die Datei gehört nicht in den Klon** und sollte möglichst auf getrenntem
|
||
Speicher liegen. Geht sie verloren, beginnt die Beobachtung von vorn – der
|
||
erste Lauf danach meldet ehrlich „erste Beobachtung", nicht „alles gut".
|
||
- **Der allererste Lauf endet mit Code 3.** Was man nie gesehen hat, kann man
|
||
nicht vergleichen. Das ist kein Fehler, sondern die ehrliche Auskunft.
|
||
|
||
**`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:
|
||
|
||
```bash
|
||
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_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.
|
||
- **Genau ein Schlüssel zählt, nicht die ganze Liste.** `allowed_signers` kann
|
||
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.signingkey` abgeleitet. Lässt er sich nicht
|
||
bestimmen, bricht das Skript ab – statt stillschweigend die ganze Liste zu
|
||
akzeptieren. Explizit setzen geht auch:
|
||
|
||
```bash
|
||
export NOTARY_SIGNER_FINGERPRINT=$(git log -1 --format=%GF)
|
||
```
|
||
|
||
Wichtig: exakt der `%GF`-Wert (`SHA256:…`), nicht die vollständige Ausgabe
|
||
von `ssh-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 |
|
||
| 5 | **Anker unvollständig** – die Kette ist gültig, aber `refs/notary/seq-N` fehlt. Reparierbar durch einen Notar-Schreiblauf |
|
||
| 4 | **Wächter-Gedächtnis fehlt** – Erstinbetriebnahme unbestätigt, oder Speicher nach der Etablierung verloren |
|
||
| 3 | beglaubigter Stand nicht abschließend feststellbar – Remote fehlt/unerreichbar, erste Beobachtung, Zurückspulen nicht ausschließbar, **oder** Checkpoint erstellt aber nicht verankert |
|
||
|
||
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, **verändert nichts** und braucht kein Schreibrecht –
|
||
auch dann nicht, wenn dabei ein fehlender Anker auffällt. Der wird gemeldet
|
||
(Code 5), nicht repariert; Reparieren ist Sache des Notar-Laufs.
|
||
|
||
Das ist kein Detail: Die Serversperre für `refs/notary/*` lehnt Schreibzugriffe
|
||
ja gerade ab. Würde der Prüfmodus dort nachtragen wollen, bekäme jeder
|
||
read-only prüfende Auditor dauerhaft einen Fehler auf einer völlig gültigen
|
||
Kette – und könnte ihn nicht beheben.
|
||
|
||
## Anhängen ist selbst ein Beglaubigungsakt
|
||
|
||
Ein Schreiblauf erweitert nicht nur die Kette – er signiert damit zugleich über
|
||
alles darunter. Deshalb prüft das Skript **vor** dem Anhängen und verweigert
|
||
es, wenn die Grundlage nicht feststeht. Sonst wäre das Anhängen selbst das
|
||
Waschmittel: eine frische Signatur über einen ungeklärten Vorzustand beglaubigt
|
||
diesen mit.
|
||
|
||
Ebenso gilt: **erstellt ist nicht verankert.** Klappt der Push nicht, endet der
|
||
Lauf mit Code 3 und der ausdrücklichen Auskunft „erstellt, aber NICHT
|
||
verankert" – niemals mit 0. Der nächste Lauf hält den ungepushten Commit dann
|
||
an, bis er geklärt ist.
|
||
|
||
## 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 |
|
||
| **Reihe zurückgespult (Force-Push)** | **Remote-Kopf ist kein Nachfahre des zuletzt beobachteten** |
|
||
| 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.
|