syncova-backup/migrations/000006_verification.up.sql
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

175 lines
8.7 KiB
SQL

-- Pruefung und Recovery Assurance (Phase 10).
--
-- Das Exit-Kriterium der Phase lautet: Ein Backup laesst sich objektiv
-- einstufen. Dafuer braucht es zwei Dinge in der Datenbank:
--
-- 1. Eine **Historie der Pruefungen**. Ohne sie waere jede Aussage ueber die
-- Wiederherstellbarkeit eine Momentaufnahme ohne Herkunft.
-- 2. Die **Einstufung am Backup selbst**, damit eine Uebersicht sie zeigen
-- kann, ohne fuer jede Zeile die Pruefhistorie zu durchsuchen.
--
-- Der wichtigste Grundsatz steckt in den Spaltentypen: Alles Ungemessene ist
-- NULL, niemals ein Nullwert. „Nie geprueft" und „vor einer Sekunde geprueft"
-- duerfen sich nicht gleich anfuehlen.
-- ---------------------------------------------------------------------------
-- Pruefauftraege
-- ---------------------------------------------------------------------------
CREATE TABLE verification_jobs (
-- id ist der oeffentliche Bezeichner.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- backup_id ist das gepruefte Backup.
backup_id UUID NOT NULL REFERENCES backups(id) ON DELETE CASCADE,
-- verification_type benennt die Art der Pruefung.
--
-- Die Arten unterscheiden sich in Aussagekraft und Kosten erheblich:
-- manifest ist in Millisekunden erledigt und sagt fast nichts,
-- restore_test dauert und ist der einzige echte Nachweis.
verification_type TEXT NOT NULL,
-- status ist der Zustand des Auftrags.
status TEXT NOT NULL DEFAULT 'queued',
-- result ist das Ergebnis: clean, corrupted, incomplete oder failed.
result TEXT,
-- report haelt den vollstaendigen Pruefbericht als JSON.
--
-- Der Bericht bleibt erhalten, nicht nur sein Ergebnis: Wer spaeter fragt,
-- warum ein Backup als beschaedigt gilt, braucht die betroffenen Objekte.
report JSONB,
-- chunks_checked ist die Zahl gepruefter Bloecke.
chunks_checked BIGINT NOT NULL DEFAULT 0,
-- chunks_missing ist die Zahl fehlender Bloecke.
chunks_missing BIGINT NOT NULL DEFAULT 0,
-- chunks_corrupted ist die Zahl beschaedigter Bloecke.
chunks_corrupted BIGINT NOT NULL DEFAULT 0,
-- bytes_read ist die gelesene Datenmenge.
bytes_read BIGINT NOT NULL DEFAULT 0,
-- started_at ist der Beginn in UTC.
started_at TIMESTAMPTZ,
-- completed_at ist das Ende in UTC.
completed_at TIMESTAMPTZ,
-- error_message ist die verstaendliche Fehlermeldung.
error_message TEXT,
-- correlation_id verbindet den Auftrag mit seinen Protokollzeilen.
correlation_id UUID NOT NULL,
-- scheduler_instance benennt den ausfuehrenden Control-Server.
scheduler_instance TEXT,
-- heartbeat_at ist die letzte Lebendmeldung in UTC.
heartbeat_at TIMESTAMPTZ,
-- created_by benennt den Anfordernden; NULL bei automatischer Pruefung.
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT verification_jobs_type_valid
CHECK (verification_type IN ('manifest', 'chunk_presence', 'chunk_integrity', 'chain', 'restore_test')),
CONSTRAINT verification_jobs_status_valid
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
CONSTRAINT verification_jobs_result_valid
CHECK (result IS NULL OR result IN ('clean', 'corrupted', 'incomplete', 'failed')),
-- Ein abgeschlossener Auftrag ohne Endzeitpunkt waere in jeder Auswertung
-- eine Luecke.
CONSTRAINT verification_jobs_completion_consistent CHECK (
status IN ('queued', 'running') OR completed_at IS NOT NULL
),
-- Ein abgeschlossener Auftrag ohne Ergebnis waere eine Pruefung, die nichts
-- feststellte — dann haette sie nicht stattgefunden.
CONSTRAINT verification_jobs_completed_has_result CHECK (
status <> 'completed' OR result IS NOT NULL
),
-- Beschaedigte oder fehlende Bloecke sind niemals ein sauberes Ergebnis.
CONSTRAINT verification_jobs_clean_means_clean CHECK (
result <> 'clean' OR (chunks_missing = 0 AND chunks_corrupted = 0)
)
);
CREATE INDEX verification_jobs_backup_idx ON verification_jobs (backup_id, completed_at DESC);
CREATE INDEX verification_jobs_status_idx ON verification_jobs (status) WHERE status IN ('queued', 'running');
CREATE INDEX verification_jobs_created_idx ON verification_jobs (created_at DESC);
-- Zwei gleichzeitige Pruefungen desselben Backups lesen dieselben Daten
-- doppelt, ohne mehr festzustellen.
CREATE UNIQUE INDEX verification_jobs_single_active_idx
ON verification_jobs (backup_id)
WHERE status IN ('queued', 'running');
-- ---------------------------------------------------------------------------
-- Einstufung am Backup
-- ---------------------------------------------------------------------------
-- Die Einstufung steht am Backup, damit eine Uebersicht sie zeigen kann, ohne
-- fuer jede Zeile die Pruefhistorie zu durchsuchen. Sie ist ein Ergebnis der
-- Pruefungen, keine eigene Wahrheit — und wird ausschliesslich von ihnen
-- fortgeschrieben.
ALTER TABLE backups
-- classification ist die objektive Einstufung (Exit-Kriterium §12).
ADD COLUMN classification TEXT,
-- last_verified_at ist die letzte erfolgreiche Integritaetspruefung in UTC.
--
-- NULL bedeutet: nie geprueft. Der Unterschied zu einem Nullwert ist der
-- Kern der ehrlichen Bewertung.
ADD COLUMN last_verified_at TIMESTAMPTZ,
-- last_restore_test_at ist der letzte bestandene Wiederherstellungstest in UTC.
ADD COLUMN last_restore_test_at TIMESTAMPTZ,
-- last_restore_test_duration_seconds ist die gemessene Dauer eines Tests.
--
-- Sie ist die einzige belastbare Grundlage fuer die Aussage, ob eine
-- Wiederherstellungsdauer eingehalten wird. Ohne sie bleibt die Vorgabe
-- eine Absichtserklaerung.
ADD COLUMN last_restore_test_duration_seconds DOUBLE PRECISION,
-- assurance_score ist die zuletzt berechnete Bewertung in Prozent.
ADD COLUMN assurance_score INTEGER,
-- assurance_unknown_inputs ist die Zahl ungemessener Eingangsgroessen.
--
-- Sie steht neben der Bewertung, damit niemand eine Prozentzahl fuer
-- belastbar haelt, die auf lauter Unbekanntem beruht.
ADD COLUMN assurance_unknown_inputs INTEGER,
-- assurance_calculated_at ist der Zeitpunkt der Berechnung in UTC.
ADD COLUMN assurance_calculated_at TIMESTAMPTZ,
ADD CONSTRAINT backups_classification_valid
CHECK (classification IS NULL OR classification IN
('failed', 'corrupted', 'successful', 'verified', 'recoverable')),
ADD CONSTRAINT backups_assurance_score_range
CHECK (assurance_score IS NULL OR (assurance_score >= 0 AND assurance_score <= 100)),
-- Ein als wiederherstellbar eingestuftes Backup muss einen bestandenen
-- Wiederherstellungstest vorweisen. Die Einstufung ist die staerkste
-- Aussage der Anlage; sie ohne Nachweis zu vergeben waere genau die
-- Beschoenigung, die PROMPT.md §138 verbietet.
ADD CONSTRAINT backups_recoverable_needs_test CHECK (
classification <> 'recoverable' OR last_restore_test_at IS NOT NULL
),
-- Ebenso: „geprueft" verlangt eine Pruefung.
ADD CONSTRAINT backups_verified_needs_verification CHECK (
classification <> 'verified' OR last_verified_at IS NOT NULL
);
-- Fuer die Uebersicht: Welche Backups wurden nie geprueft?
CREATE INDEX backups_never_verified_idx ON backups (repository_id)
WHERE last_verified_at IS NULL AND status = 'complete';
-- Fuer die Uebersicht: Welche Backups sind auffaellig?
CREATE INDEX backups_classification_idx ON backups (classification)
WHERE classification IN ('corrupted', 'failed');
-- ---------------------------------------------------------------------------
-- Berechtigungen
-- ---------------------------------------------------------------------------
-- verification.read und verification.write stammen bereits aus 000002_identity.
-- Neu ist der Wiederherstellungstest: Er schreibt zwar nur an einen Wegwerfort,
-- liest aber das gesamte Backup und belastet damit Datentraeger und Leitung.
-- Wer ihn ausloest, sollte wissen was er tut.
INSERT INTO permissions (name, description) VALUES
('verification.restore_test', 'Wiederherstellungstests ausloesen');
-- Vergeben wird das Recht nur an die Rollen, die ueberhaupt eine Pruefung
-- ausloesen koennen (verification.write): super_administrator und
-- backup_operator. Ein Recht an eine Rolle zu haengen, die den Endpunkt gar
-- nicht erreicht, sieht in der Rechteuebersicht nach einer Faehigkeit aus, die
-- es nicht gibt.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name IN ('super_administrator', 'backup_operator')
AND p.name = 'verification.restore_test';