Gegenbuch: Gedaechtnis ins Substrat verlegt, Verlust laut behandelt (R180)
"Erste Beobachtung -> exit 3" galt genau einen Lauf. Danach baselinete der Waechter auf den aktuellen Kopf - nach einem Rewind also auf den gewaschenen Stand - und meldete dauerhaft gruen. Der Angreifer musste nur ein einziges exit 3 ueberstehen, ausgerechnet den Code, den der Betreiber bei Remote-Ausfaellen ohnehin staendig sieht. Damit war der Waechter, der Rewind ohne Notar-Host-Integritaet fangen sollte, wieder an genau diese gekoppelt. Fix: Jeder verankerte Checkpoint bekommt einen eigenen Ref refs/notary/seq-N. Der ueberlebt einen Force-Push auf den Zweig - die hoechste je existierende Nummer ist damit aus dem Server rekonstruierbar. Geprueft wird, ob der hoechste verankerte Checkpoint noch im aktuellen Kopf enthalten ist und ob die Reihe mindestens so lang ist wie verankert. Laute Verlustbehandlung mit Diskriminator "traegt der Remote schon Checkpoints?": keine Historie -> Genesis, einmalig NOTARY_GENESIS_ACK; Historie vorhanden aber kein Gedaechtnis -> Anomalie, Code 4, keine stille Adoption, erst nach NOTARY_ADOPT_ACK. Verifiziert: Genesis ohne Bestaetigung -> Code 4; mit Bestaetigung Kette aufgebaut samt refs/notary/seq-1..5; Rewind -> exit 2 auch nach Loeschen des lokalen Speichers und bei jedem Folgelauf (vorher: ein exit 3, danach dauerhaft gruen); zusaetzlich Anker-Refs geloescht -> Code 4 statt stiller Uebernahme. Dokumentiert: Die Anker-Refs muessen serverseitig ebenfalls vor Loeschen und Ueberschreiben geschuetzt sein, sonst verschiebt sich das Problem eine Ebene. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,36 @@ 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.
|
||||
|
||||
> **Diese Refs gehören ebenfalls serverseitig geschützt** (kein Löschen, kein
|
||||
> Überschreiben). Sonst verschiebt sich das Problem nur eine Ebene weiter.
|
||||
|
||||
### 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
|
||||
@@ -154,6 +184,7 @@ Deshalb gilt jetzt:
|
||||
| 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 |
|
||||
| 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,
|
||||
|
||||
@@ -228,7 +228,7 @@ const OHNE_ANKER = () =>
|
||||
// signierten Stand unerkennbar (R178-01). Das gehoert in die Erfolgsmeldung,
|
||||
// sonst liest sich „OK“ als mehr Gewissheit, als vorhanden ist.
|
||||
const REWIND_VORBEHALT = () =>
|
||||
REWIND_GESCHUETZT || ffBelegt
|
||||
REWIND_GESCHUETZT || ankerBelegt || ffBelegt
|
||||
? ''
|
||||
: '\n Offen: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' +
|
||||
'lässt sich hier nicht ausschließen.' +
|
||||
@@ -251,6 +251,9 @@ const REWIND_VORBEHALT = () =>
|
||||
// bewusst NICHT mit dem Manipulationsalarm (2) verschmolzen.
|
||||
const abschliessendSicher = (anzahl) =>
|
||||
REWIND_GESCHUETZT ||
|
||||
// Verankerte Refs sind das belastbarste Signal: sie liegen im Substrat und
|
||||
// ueberleben einen Force-Push auf den Zweig.
|
||||
ankerBelegt ||
|
||||
// Belegte Fast-Forward-Kette seit der ersten Beobachtung: das ist ein
|
||||
// Nachweis, kein Versprechen – deshalb zaehlt er (R179 (a)).
|
||||
ffBelegt ||
|
||||
@@ -325,9 +328,54 @@ if (ref !== 'HEAD') {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1b) Fast-Forward-Monitor: wurde der Remote-Kopf zurueckgespult?
|
||||
// 1a) Anker-Refs: das Gedaechtnis liegt im Substrat, nicht auf diesem Rechner.
|
||||
//
|
||||
// Jeder verankerte Checkpoint bekommt zusaetzlich einen eigenen Ref
|
||||
// `refs/notary/seq-N`. Der ueberlebt einen Force-Push auf `main`, weil er ein
|
||||
// anderer Ref ist. Damit ist die hoechste je existierende Nummer aus dem
|
||||
// Remote rekonstruierbar – auch dann, wenn der lokale Beobachtungsspeicher
|
||||
// fehlt.
|
||||
//
|
||||
// Das schliesst die Luecke, die ein reiner Host-Speicher offenlaesst: Ein
|
||||
// Angreifer, der ihn loescht, brauchte sonst nur EIN „erste Beobachtung“
|
||||
// zu ueberstehen – danach haette der Waechter den zurueckgespulten Stand als
|
||||
// Wahrheit adoptiert und dauerhaft gruen gemeldet. Ein gutartiger Default, den
|
||||
// ein Angreifer gezielt erzeugen kann, ist kein sicherer Default.
|
||||
// ---------------------------------------------------------------------------
|
||||
const ankerRefs = (() => {
|
||||
const r = gitStill('ls-remote', REMOTE, 'refs/notary/seq-*');
|
||||
if (!r.ok || !r.out) return [];
|
||||
return r.out
|
||||
.split('\n')
|
||||
.map((z) => z.trim().split(/\s+/))
|
||||
.filter((t) => t.length === 2)
|
||||
.map(([sha, name]) => ({ sha, seq: Number.parseInt(name.replace(/.*seq-/, ''), 10) }))
|
||||
.filter((e) => Number.isFinite(e.seq))
|
||||
.sort((a, b) => a.seq - b.seq);
|
||||
})();
|
||||
const hoechsterAnker = ankerRefs[ankerRefs.length - 1] || null;
|
||||
|
||||
const remoteKopf = git('rev-parse', ref);
|
||||
|
||||
let ankerBelegt = false;
|
||||
if (hoechsterAnker) {
|
||||
// Der beglaubigte Checkpoint muss im aktuellen Kopf noch enthalten sein.
|
||||
// Ist er es nicht, wurde `main` an ihm vorbei zurueckgespult – und das faellt
|
||||
// hier auf, ganz ohne lokalen Speicher.
|
||||
const drin =
|
||||
hoechsterAnker.sha === remoteKopf ||
|
||||
gitStill('merge-base', '--is-ancestor', hoechsterAnker.sha, remoteKopf).ok;
|
||||
if (!drin) {
|
||||
alarm(
|
||||
`Der verankerte Checkpoint ${hoechsterAnker.seq} ist im aktuellen Stand nicht mehr ` +
|
||||
`enthalten – die Reihe wurde an ihm vorbei zurückgespult.\n` +
|
||||
` verankert: ${hoechsterAnker.sha.slice(0, 12)} (refs/notary/seq-${hoechsterAnker.seq})\n` +
|
||||
` jetzt : ${remoteKopf.slice(0, 12)}\n` +
|
||||
`Diese Refs überleben einen Force-Push auf ${zweig} – deshalb fällt es auf.`,
|
||||
);
|
||||
}
|
||||
ankerBelegt = true;
|
||||
}
|
||||
const remoteKennung = gitStill('remote', 'get-url', REMOTE).out || zweig;
|
||||
|
||||
function beobachtungenLesen() {
|
||||
@@ -345,6 +393,49 @@ const beobachtungen = beobachtungenLesen();
|
||||
const letzteBeobachtung = beobachtungen[beobachtungen.length - 1];
|
||||
let ffBelegt = false;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1c) Fehlt das Waechter-Gedaechtnis – und ist das harmlos?
|
||||
//
|
||||
// „Kein Speicher“ ist zweierlei, und der Unterschied entscheidet alles:
|
||||
// * Es gab hier noch nie etwas → echte Erstinbetriebnahme, legitim.
|
||||
// * Es gibt bereits eine notarisierte Historie, aber kein Gedaechtnis mehr
|
||||
// → das Gedaechtnis ging NACH der Etablierung verloren. Das ist die
|
||||
// Anomalie, nicht der Normalfall – und genau der Zustand, den ein
|
||||
// Angreifer nach einem Rewind gezielt herstellt, um die naechste Baseline
|
||||
// auf seinen Stand zu setzen.
|
||||
//
|
||||
// Deshalb: kein stilles Neu-Baselinen. Eine Baseline zu setzen heisst „ich
|
||||
// verbuerge mich, DAS ist der wahre Kopf“ – dieser Akt gehoert bestaetigt.
|
||||
// ---------------------------------------------------------------------------
|
||||
if (!letzteBeobachtung && !ankerBelegt) {
|
||||
const remoteHatHistorie = gitStill('show', `${ref}:${LEDGER_NAME}`).ok;
|
||||
if (remoteHatHistorie && process.env.NOTARY_ADOPT_ACK !== 'true') {
|
||||
console.error(
|
||||
'Das Wächter-Gedächtnis fehlt, obwohl bereits eine notarisierte Historie existiert.\n' +
|
||||
'Das ist KEIN Normalzustand: Entweder wurde dieser Rechner neu aufgesetzt, oder der\n' +
|
||||
'Beobachtungsspeicher wurde entfernt – letzteres ist genau das, was nach einem\n' +
|
||||
'Zurückspulen passieren müsste, damit der nächste Lauf den gewaschenen Stand als\n' +
|
||||
'Wahrheit übernimmt.\n\n' +
|
||||
'Ohne Bestätigung wird hier nichts als Ausgangspunkt akzeptiert. Bitte erst klären:\n' +
|
||||
` * Stimmt der aktuelle Kopf ${remoteKopf.slice(0, 12)} mit dem überein, was zuletzt\n` +
|
||||
' beglaubigt wurde? (Protokoll, Monitoring, zweite Kopie des Gegenbuchs)\n' +
|
||||
' * Gibt es refs/notary/seq-* auf dem Server? Dann lässt sich der Stand daraus\n' +
|
||||
' rekonstruieren, statt ihn zu glauben.\n\n' +
|
||||
'Erst wenn das geklärt ist: NOTARY_ADOPT_ACK=true setzen.',
|
||||
);
|
||||
process.exit(4);
|
||||
}
|
||||
if (!remoteHatHistorie && process.env.NOTARY_GENESIS_ACK !== 'true') {
|
||||
console.error(
|
||||
'Erstinbetriebnahme: Es gibt weder ein Wächter-Gedächtnis noch eine notarisierte\n' +
|
||||
'Historie. Das ist plausibel – aber die erste Baseline ist ein Vertrauensakt und\n' +
|
||||
'wird deshalb einmalig bestätigt:\n' +
|
||||
' NOTARY_GENESIS_ACK=true',
|
||||
);
|
||||
process.exit(4);
|
||||
}
|
||||
}
|
||||
|
||||
if (letzteBeobachtung) {
|
||||
if (letzteBeobachtung.sha === remoteKopf) {
|
||||
ffBelegt = true;
|
||||
@@ -471,6 +562,14 @@ for (let i = 0; i < bisher.length; i++) {
|
||||
}
|
||||
}
|
||||
|
||||
if (hoechsterAnker && bisher.length < hoechsterAnker.seq) {
|
||||
alarm(
|
||||
`Die Checkpoint-Reihe ist kürzer als der Server beglaubigt hat: ` +
|
||||
`verankert bis ${hoechsterAnker.seq}, vorhanden ${bisher.length}.\n` +
|
||||
`Die vorhandenen Einträge sind korrekt signiert – es fehlen welche am Ende.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (Number.isFinite(MIN_SEQ) && bisher.length < MIN_SEQ) {
|
||||
alarm(
|
||||
`Die Checkpoint-Reihe ist kürzer als bekannt: erwartet mindestens ${MIN_SEQ}, ` +
|
||||
@@ -623,6 +722,19 @@ if (PUSH && !NUR_LOKAL) {
|
||||
);
|
||||
process.exit(3);
|
||||
}
|
||||
// Eigener, unveraenderlicher Ref je Checkpoint. Er ueberlebt einen
|
||||
// Force-Push auf den Zweig und macht das Gedaechtnis wiederherstellbar,
|
||||
// statt es an diesen Rechner zu binden.
|
||||
const ankerRef = `refs/notary/seq-${eintrag.seq}`;
|
||||
const a = gitStill('push', REMOTE, `HEAD:${ankerRef}`);
|
||||
if (!a.ok) {
|
||||
console.warn(
|
||||
`HINWEIS: Anker-Ref ${ankerRef} konnte nicht gesetzt werden.\n` +
|
||||
` Der Checkpoint ist gepusht, aber ohne diesen Ref bleibt das Gedächtnis an\n` +
|
||||
` diesen Rechner gebunden – geht es verloren, ist der Stand nicht mehr aus dem\n` +
|
||||
` Server rekonstruierbar. Grund: ${a.out}`,
|
||||
);
|
||||
}
|
||||
verankert = true;
|
||||
beobachtungMerken();
|
||||
} else if (NUR_LOKAL) {
|
||||
|
||||
Reference in New Issue
Block a user