feat(archive): BAK-08 Checksum-basierte Objekt-Integritaetspruefung

Stichprobenbasierter Scrub-Job: nimmt BAK-05s existing_in_storage,
priorisiert nach eigenem scrub_state.last_scrubbed_at (nicht
file_revisions.created_at, sonst kein echtes Rotationsverhalten),
prueft Inhalt per SHA-256 gegen file_revisions.checksum_sha256.
Meldung ueber echten dauerhaften /metrics-Endpunkt (Pull-Modell,
OPS-03 scrapt, kein Push), Counter monoton steigend. Real registriert
in Core metrics_sources, End-zu-Ende ueber OPS-03-Aggregator bestaetigt,
realer Befund-Durchlauf mit absichtlich falscher Pruefsumme durchgefuehrt.
This commit is contained in:
sysops
2026-08-29 23:55:24 +02:00
parent 8100fa3d14
commit 67bcdd1833
14 changed files with 883 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
# BAK-08 Prüfprotokoll: Checksum-basierte Objekt-Integritätsprüfung
Voraussetzungen BAK-05, FDN-04, FDN-09, OPS-05 alle erledigt, siehe
eigene Protokolle. Vor Start zwei offene Rückfragen geklärt (siehe unten).
## Grundsatzentscheidung: eigener Zustand statt file_revisions.created_at
`created_at` als Alterskriterium hätte immer dieselben "ältesten" Objekte
gescrubbt und den Rest nie erreicht — kein echtes Rotationsverhalten.
Stattdessen eigene Archive-Tabelle `scrub_state` (`storage_key`
`last_scrubbed_at`, `last_result`), Migration
`migrations/0001_scrub_state.up.sql`. `internal/scrub.Sample` ist eine
reine Funktion: nimmt BAK-05s `existing_in_storage` (deterministisch
sortiert) entgegen, filtert Objekte innerhalb der konfigurierbaren
Cooldown-Frist heraus, priorisiert danach nach `last_scrubbed_at`
aufsteigend (nie geprüft = ältestmöglicher Wert), begrenzt auf die
konfigurierte Stichprobengröße — kein Voll-Sort über den gesamten
Bestand bei jedem Lauf (Nutzerhinweis zum Kostenfaktor bei 10⁵+
Objekten: die WHERE-artige Cooldown-Filterung reduziert die Kandidatenmenge
VOR der Sortierung, nur die Kandidaten selbst werden sortiert, nicht der
komplette Bestand).
## Nachtrag: zwei Rückfragen vor Implementierungsbeginn geklärt
1. **OPS-05-Anbindung ist Pull, nicht Push.** OPS-05 (`internal/alerting`,
Core) ist real implementiert, aber Core OPS-03 scrapt `/metrics`-URLs
registrierter Module (`metrics_sources`-Tabelle in der Core-Registry-
DB, `SourceStore.RegisterSource`) — kein Push-API. Für BAK-08 daher
ein eigener, DAUERHAFT laufender Endpunkt (`cmd/scrub-metrics`,
getrennt vom Oneshot-`scrub-cli`, dessen Prozess nach jedem Lauf endet
und zum Scrape-Zeitpunkt nicht erreichbar wäre). Metrik als Counter
(`nexarch_archive_storage_integrity_failures_total`), monoton
steigend — kein Gauge, kein Rücksetzen bei behobenem Befund. Kein
Befund = Wert bleibt unverändert (kein Dauer-Alarm durch andauernden
"Fehler"-Zustand). Scope-Trennung gewahrt: `scrub-cli`/`scrub-metrics`
erzeugen selbst KEIN Alert-Objekt — Schwellwert/Drosselung bleiben
OPS-05-eigene Konfiguration (Alert-Regel wird separat über
`alerting.RuleStore.CreateRule` angelegt, nicht Teil dieses Tickets).
**CFG-04 war eine Verwechslung** (das ist die
Benachrichtigungs-Einstellungen-Oberfläche, ein anderes Ticket) — die
tatsächlich nötige "Config"-Aktion ist ein `INSERT` in
`metrics_sources` (Core-Registry-DB), kein UI/Ticket-Abhängigkeit.
Real ausgeführt (siehe „Echte Verdrahtung" unten).
2. **Sampling-Kriterium.** Siehe Grundsatzentscheidung oben —
`scrub_state.last_scrubbed_at` statt `file_revisions.created_at`,
Cooldown-Filterung vor Sortierung, feste Stichprobengröße (Top-N,
deterministisch, keine Zufallsstichprobe — Nutzerpräferenz für
Reproduzierbarkeit im Protokoll).
## Umsetzung
- `migrations/0001_scrub_state.up.sql`/`.down.sql``scrub_state`,
`scrub_counters` (Einzelzeile, monotoner Zähler).
- `internal/scrub.Sample` — reine Funktion, Cooldown-Filter + Alt-
Priorisierung + Stichprobenbegrenzung.
- `internal/scrub.LoadLastScrubbed`/`MarkScrubbed`/`RecordFinding`/
`FindingsTotal` — DB-Zugriff auf `scrub_state`/`scrub_counters`,
`MarkScrubbed` idempotent (`ON CONFLICT`) für unterbrechbare Läufe.
- `internal/scrub.ExpectedChecksums` — eigene, minimale Abfrage gegen
`file_revisions` (keine Erweiterung von `reconcile.DBEntry` — BAK-05
bleibt existenz-only).
- `internal/scrub.ActualChecksum` — echtes Lesen der Datei + SHA-256,
kein Header-/Größenvergleich.
- `cmd/scrub-cli` — Oneshot: BAK-05-Reconcile → `Sample` → pro Kandidat
Checksum-Vergleich → `MarkScrubbed` + bei Abweichung `RecordFinding`
JSON-Bericht auf stdout, Exit-Code 1 bei Befunden (gemeldet, nicht
automatisch repariert).
- `cmd/scrub-metrics` — dauerhafter `/metrics`-Endpunkt, liest
`scrub_counters.findings_total`.
## Prüfungen
| # | Prüfung | Ergebnis |
|---|---|---|
| 1 | Absichtlich veränderter Objektinhalt wird als Abweichung erkannt | **bestanden** — real: Testobjekt mit absichtlich falscher `checksum_sha256` in `dms_tenant_test` angelegt, echte Datei ins Storage-Verzeichnis gelegt, `scrub-cli` real über systemd ausgelöst: Befund im JSON-Bericht, Exit-Code 1, `scrub_counters.findings_total` real von 0 auf 1 erhöht (siehe Journal-Auszug unten) |
| 2 | Sampling priorisiert alte/nie geprüfte Objekte, nicht neue | **bestanden**`TestSample_PrioritizesNeverScrubbedAndOldest`: nie geprüftes Objekt kommt vor einem vor 30 Tagen geprüften, dieses vor einem vor 1 Tag geprüften |
| 3 | Wiederholter Lauf ohne neue Objekte meldet nichts erneut (kein Spam) / idempotent bei Unterbrechung | **bestanden** — real: zweiter `scrub-cli`-Lauf direkt nach dem ersten liefert `sampled: 0` (Cooldown greift), `TestMarkScrubbed_IsIdempotent` beweist wiederholtes Markieren ohne Duplikat |
Zusätzlich: `TestSample_RespectsCooldown`,
`TestSample_LimitsToSampleSize`, `TestSample_DeterministicForIdenticalInput`,
`TestRecordFinding_IsMonotonicallyIncreasing`,
`TestActualChecksum_MatchesRealFileContent` (echter Dateiinhalt, echtes
SHA-256), `TestExpectedChecksums_ReadsRealFileRevisions` (echtes
Postgres, kein Mock).
## Echte Verdrahtung auf 192.168.1.131
- `scrub-cli`, `scrub-metrics` gebaut nach `/opt/nexarch-archive/bin/`
- `/etc/nexarch/archive-scrub.env`, `/etc/nexarch/archive-scrub-metrics.env`
(0600)
- Migration real gegen `dms_tenant_test` angewendet
(`psql -f migrations/0001_scrub_state.up.sql`)
- `nexarch-archive-scrub.timer` installiert/aktiviert (täglich 06:00
UTC), `nexarch-archive-scrub-metrics.service` installiert/aktiviert
(dauerhaft, `Restart=on-failure`) — beide `systemctl status`: aktiv
- **Reales `INSERT` in `metrics_sources`** (Core-Registry-DB
`nexarch_registry`): `('archive', 'http://127.0.0.1:8090/metrics')`
bestätigt über `SELECT * FROM metrics_sources`
- **End-to-End über OPS-03 bestätigt**: `curl http://127.0.0.1:8085/metrics`
(Core-Aggregator) zeigt `nexarch_module_archive_nexarch_archive_storage_integrity_failures_total`
— reale Umbenennung gemäß OPS-03-Namenskonvention, kein synthetischer
Wert
- Realer Befund-Durchlauf: Testobjekt mit absichtlich falscher Prüfsumme
angelegt → `scrub-cli` real via `systemctl start` ausgelöst → Befund im
Journal, `scrub_counters.findings_total` real 0→1, sichtbar sowohl auf
`scrub-metrics` als auch über den Core-Aggregator → Testdaten
anschließend bereinigt (`file_revisions`/`documents`/`users`-Zeilen
gelöscht, `scrub_state`/`scrub_counters` zurückgesetzt, Testdatei
entfernt)
## Build/Test-Ergebnis (192.168.1.131, `make check`)
```
go build ./... -> clean
go vet ./... -> clean
golangci-lint run ./... -> 0 issues
go test ./... -p 1 -count=1 -> 4/4 Pakete mit Tests ok (internal/backup, internal/objectbackup, internal/reconcile, internal/scrub), 0 Fehlschläge
```
`internal/scrub`-Tests separat mit gesetzter `TEST_TENANT_DSN` gegen
`dms_tenant_test` verifiziert: 8/8 Tests bestanden.
## Bekannte Grenze (aus Ticket übernommen, nicht Teil der Abnahme)
Der Job erkennt Abweichungen nur bei Objekten, die gelesen und erneut
geprüft werden können. Ersetzt keine storage-seitige WORM-/
Versionierungsstrategie und keine Zugriffs-/Audit-Logs des
Storage-Providers (`STORAGE-KONZEPT.md` Abschnitt 6.1) — bei extern
eingebundenem, nicht-kompatiblem Kunden-Storage (Betriebsmodus 3, ohne
Versioning/Object Lock/Audit-Logs) bleibt eine Lücke, die BAK-08
technisch nicht schließen kann.
## Gesamtergebnis
**Bestanden.** Alle sechs Akzeptanzkriterien und alle drei Pflicht-
prüfungen real erfüllt — inklusive echtem Ende-zu-Ende-Nachweis über
Core OPS-03/OPS-05 (kein Stub, reale `/metrics`-Registrierung und
-Aggregation). Beide vor Implementierungsbeginn gestellten Rückfragen
(OPS-05-Anbindungsmechanismus, Sampling-Kriterium) im Protokoll
dokumentiert und in der Umsetzung berücksichtigt.