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>
223 lines
11 KiB
SQL
223 lines
11 KiB
SQL
-- Immutability und Aufbewahrung (Phase 11).
|
|
--
|
|
-- Der Grundsatz dieser Migration steht in einer einzigen Spalte:
|
|
-- repositories.enforcement_level. Sie haelt fest, was das Dateisystem des
|
|
-- Repositorys **nachweislich** verhindert — gemessen, nicht behauptet. Ein
|
|
-- Backup-Produkt, das „unveraenderlich" in eine Datenbankspalte schreibt und
|
|
-- dabei belaesst, verkauft eine Ransomware-Strategie, die keine ist.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Repositories: gemessene Durchsetzung und Fristen
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
ALTER TABLE repositories
|
|
-- enforcement_level ist die zuletzt gemessene Durchsetzungsstufe.
|
|
--
|
|
-- NULL bedeutet: nie gemessen. Der Unterschied zu 'advisory' ist wichtig —
|
|
-- „wir wissen es nicht" ist keine Aussage ueber den Schutz.
|
|
ADD COLUMN enforcement_level TEXT,
|
|
-- enforcement_measured_at ist der Zeitpunkt der Messung in UTC.
|
|
ADD COLUMN enforcement_measured_at TIMESTAMPTZ,
|
|
-- enforcement_report haelt die einzelnen Versuche der Messung.
|
|
--
|
|
-- Der Bericht bleibt erhalten, nicht nur sein Ergebnis: Wer wissen will,
|
|
-- worauf sich die Stufe stuetzt, soll es nachlesen koennen.
|
|
ADD COLUMN enforcement_report JSONB,
|
|
-- retention_seconds ist die Aufbewahrungsfrist neuer Backups.
|
|
--
|
|
-- Massgeblich bleibt der Descriptor im Repository; diese Spalte ist sein
|
|
-- Abbild fuer Uebersichten. Bei Widerspruch gilt das Repository — es muss
|
|
-- auch ohne Control Server deutbar bleiben.
|
|
ADD COLUMN retention_seconds BIGINT,
|
|
-- minimum_retention_seconds ist die kuerzeste je zulaessige Frist.
|
|
ADD COLUMN minimum_retention_seconds BIGINT,
|
|
|
|
ADD CONSTRAINT repositories_enforcement_level_valid
|
|
CHECK (enforcement_level IS NULL OR enforcement_level IN
|
|
('none', 'advisory', 'filesystem', 'storage')),
|
|
ADD CONSTRAINT repositories_retention_not_negative
|
|
CHECK (retention_seconds IS NULL OR retention_seconds >= 0),
|
|
ADD CONSTRAINT repositories_minimum_retention_not_negative
|
|
CHECK (minimum_retention_seconds IS NULL OR minimum_retention_seconds >= 0);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Backups: Legal Hold neben der Frist
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
ALTER TABLE backups
|
|
-- legal_hold meldet einen unbefristeten Schutz fuer Beweiszwecke.
|
|
--
|
|
-- Getrennt von immutable_until, weil die Abhilfe eine andere ist: Eine Frist
|
|
-- laeuft ab, ein Legal Hold muss ausdruecklich aufgehoben werden.
|
|
ADD COLUMN legal_hold BOOLEAN NOT NULL DEFAULT false,
|
|
-- legal_hold_reason begruendet den unbefristeten Schutz.
|
|
ADD COLUMN legal_hold_reason TEXT,
|
|
-- legal_hold_placed_at ist der Zeitpunkt der Anordnung in UTC.
|
|
ADD COLUMN legal_hold_placed_at TIMESTAMPTZ,
|
|
-- legal_hold_placed_by benennt den Anordnenden.
|
|
ADD COLUMN legal_hold_placed_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- deleted_at ist der Zeitpunkt der Loeschung in UTC.
|
|
--
|
|
-- Ein geloeschtes Backup wird nicht aus der Tabelle entfernt: Die Frage
|
|
-- „warum ist das Backup von vorletzter Woche weg?" muss beantwortbar
|
|
-- bleiben, gerade wenn niemand mehr weiss, wer geloescht hat.
|
|
ADD COLUMN deleted_at TIMESTAMPTZ,
|
|
-- deleted_by benennt den Loeschenden; NULL bei automatischer Aufbewahrung.
|
|
ADD COLUMN deleted_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- deletion_reason begruendet die Loeschung.
|
|
ADD COLUMN deletion_reason TEXT,
|
|
|
|
-- Ein Legal Hold ohne Begruendung waere nach einem Jahr nicht mehr
|
|
-- aufloesbar: Niemand traut sich, einen Schutz aufzuheben, dessen Anlass
|
|
-- unbekannt ist.
|
|
ADD CONSTRAINT backups_legal_hold_needs_reason CHECK (
|
|
legal_hold = false OR (legal_hold_reason IS NOT NULL AND legal_hold_reason <> '')
|
|
),
|
|
-- Ein Backup unter Legal Hold darf nicht als geloescht vermerkt sein. Die
|
|
-- Regel steht in der Datenbank und nicht nur im Code: Im Code muesste jede
|
|
-- Stelle sie einhalten, und eine vergisst es.
|
|
ADD CONSTRAINT backups_legal_hold_not_deleted CHECK (
|
|
legal_hold = false OR deleted_at IS NULL
|
|
);
|
|
|
|
-- Fuer die Uebersicht: Was steht unter Schutz?
|
|
CREATE INDEX backups_legal_hold_idx ON backups (repository_id)
|
|
WHERE legal_hold = true;
|
|
|
|
-- Fuer die Aufbewahrung: Welche Backups sind noch da?
|
|
CREATE INDEX backups_not_deleted_idx ON backups (repository_id, completed_at DESC)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Aufbewahrungsregeln
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- retention_policies existiert seit 000002 mit einer freien rules-Spalte.
|
|
-- Die einzelnen Haltevorgaben bekommen eigene Spalten: Eine CHECK-Bedingung
|
|
-- gegen ein JSON-Feld liesse sich nicht formulieren, und genau hier wird eine
|
|
-- gebraucht — eine Regel ohne jede Haltevorgabe wuerde alle Backups loeschen.
|
|
ALTER TABLE retention_policies
|
|
-- keep_within_seconds haelt jedes Backup, das juenger ist als diese Spanne.
|
|
ADD COLUMN keep_within_seconds BIGINT,
|
|
-- keep_last haelt die juengsten n Backups, unabhaengig vom Alter.
|
|
ADD COLUMN keep_last INTEGER NOT NULL DEFAULT 1,
|
|
-- keep_daily haelt n Tagesstaende.
|
|
ADD COLUMN keep_daily INTEGER,
|
|
-- keep_weekly haelt n Wochenstaende.
|
|
ADD COLUMN keep_weekly INTEGER,
|
|
-- keep_monthly haelt n Monatsstaende.
|
|
ADD COLUMN keep_monthly INTEGER,
|
|
-- keep_yearly haelt n Jahresstaende.
|
|
ADD COLUMN keep_yearly INTEGER,
|
|
-- time_zone ist die Zeitzone der Tages- und Monatsgrenzen.
|
|
--
|
|
-- Ohne sie liefe die Einteilung in UTC, und wer taeglich um 00:30 deutscher
|
|
-- Zeit sichert, verlore systematisch die falschen Backups.
|
|
ADD COLUMN time_zone TEXT,
|
|
-- created_by benennt den Anlegenden.
|
|
ADD COLUMN created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
-- Der Standardwert 1 fuer keep_last ist die wichtigste Zeile dieser
|
|
-- Migration: Er verhindert die haeufigste Katastrophe einer
|
|
-- Aufbewahrungsregel — ein System sichert monatelang nicht, alle Backups
|
|
-- fallen aus der Frist, und die Regel loescht das letzte vorhandene.
|
|
ADD CONSTRAINT retention_policies_keeps_something CHECK (
|
|
COALESCE(keep_within_seconds, 0) > 0 OR keep_last > 0 OR
|
|
COALESCE(keep_daily, 0) > 0 OR COALESCE(keep_weekly, 0) > 0 OR
|
|
COALESCE(keep_monthly, 0) > 0 OR COALESCE(keep_yearly, 0) > 0
|
|
),
|
|
ADD CONSTRAINT retention_policies_counts_not_negative CHECK (
|
|
keep_last >= 0 AND
|
|
COALESCE(keep_daily, 0) >= 0 AND COALESCE(keep_weekly, 0) >= 0 AND
|
|
COALESCE(keep_monthly, 0) >= 0 AND COALESCE(keep_yearly, 0) >= 0 AND
|
|
COALESCE(keep_within_seconds, 0) >= 0
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Laeufe der Aufbewahrung
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Jede Anwendung einer Aufbewahrungsregel wird festgehalten. Ohne diese Tabelle
|
|
-- liesse sich im Nachhinein nicht beantworten, warum ein Backup fehlt — und das
|
|
-- ist die erste Frage, die im Ernstfall gestellt wird.
|
|
CREATE TABLE retention_runs (
|
|
-- id ist der oeffentliche Bezeichner.
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- repository_id ist das betroffene Repository.
|
|
repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
|
|
-- retention_policy_id ist die angewandte Regel.
|
|
retention_policy_id UUID REFERENCES retention_policies(id) ON DELETE SET NULL,
|
|
-- policy_name haelt den Namen fest, auch wenn die Regel spaeter verschwindet.
|
|
policy_name TEXT NOT NULL,
|
|
-- dry_run meldet eine reine Vorschau ohne Loeschung.
|
|
dry_run BOOLEAN NOT NULL DEFAULT true,
|
|
-- evaluated_count ist die Zahl betrachteter Backups.
|
|
evaluated_count INTEGER NOT NULL DEFAULT 0,
|
|
-- kept_count ist die Zahl behaltener Backups.
|
|
kept_count INTEGER NOT NULL DEFAULT 0,
|
|
-- deleted_count ist die Zahl geloeschter Backups.
|
|
deleted_count INTEGER NOT NULL DEFAULT 0,
|
|
-- protected_count ist die Zahl faelliger, aber geschuetzter Backups.
|
|
protected_count INTEGER NOT NULL DEFAULT 0,
|
|
-- failed_count ist die Zahl gescheiterter Loeschungen.
|
|
failed_count INTEGER NOT NULL DEFAULT 0,
|
|
-- chunks_removed ist die Zahl bereinigter Datenbloecke.
|
|
chunks_removed BIGINT NOT NULL DEFAULT 0,
|
|
-- bytes_freed ist der freigegebene Speicher.
|
|
bytes_freed BIGINT NOT NULL DEFAULT 0,
|
|
-- plan haelt die Einzelentscheidungen samt Begruendung.
|
|
plan JSONB,
|
|
-- error_message ist die verstaendliche Fehlermeldung.
|
|
error_message TEXT,
|
|
-- correlation_id verbindet den Lauf mit seinen Protokollzeilen.
|
|
correlation_id UUID NOT NULL,
|
|
-- triggered_by benennt den Ausloesenden; NULL bei automatischem Lauf.
|
|
triggered_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
-- started_at ist der Beginn in UTC.
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
-- completed_at ist das Ende in UTC.
|
|
completed_at TIMESTAMPTZ,
|
|
|
|
-- Eine Vorschau darf niemals etwas geloescht haben. Die Regel gehoert in die
|
|
-- Datenbank: Sie ist die Zusicherung, auf die sich der Betreiber verlaesst,
|
|
-- wenn er eine unbekannte Regel ausprobiert.
|
|
CONSTRAINT retention_runs_dry_run_deletes_nothing CHECK (
|
|
dry_run = false OR (deleted_count = 0 AND chunks_removed = 0 AND bytes_freed = 0)
|
|
),
|
|
CONSTRAINT retention_runs_counts_not_negative CHECK (
|
|
evaluated_count >= 0 AND kept_count >= 0 AND deleted_count >= 0 AND
|
|
protected_count >= 0 AND failed_count >= 0 AND
|
|
chunks_removed >= 0 AND bytes_freed >= 0
|
|
)
|
|
);
|
|
|
|
CREATE INDEX retention_runs_repository_idx ON retention_runs (repository_id, started_at DESC);
|
|
CREATE INDEX retention_runs_correlation_idx ON retention_runs (correlation_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Berechtigungen
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Drei neue Rechte, und ihre Trennung ist der Kern der Zugriffssteuerung dieser
|
|
-- Phase: Wer Backups loeschen darf, darf deshalb noch lange keinen
|
|
-- Aufbewahrungsschutz aufheben.
|
|
INSERT INTO permissions (name, description) VALUES
|
|
('backups.delete', 'Backups löschen'),
|
|
('retention.write', 'Aufbewahrungsregeln anlegen, ändern und anwenden'),
|
|
('immutability.manage', 'Aufbewahrungsschutz verlängern und Legal Holds setzen oder aufheben');
|
|
|
|
-- backups.delete: Wer sichert, raeumt auch auf.
|
|
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 IN ('backups.delete', 'retention.write');
|
|
|
|
-- immutability.manage bleibt der Sicherheitsverwaltung und dem Super
|
|
-- Administrator vorbehalten. Ein Legal Hold ist eine rechtliche Anordnung, und
|
|
-- das Aufheben eines Aufbewahrungsschutzes ist der Schritt, der einem Angreifer
|
|
-- den Weg oeffnet — er gehoert nicht in dieselbe Hand wie das Loeschen.
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id FROM roles r, permissions p
|
|
WHERE r.name IN ('super_administrator', 'security_administrator')
|
|
AND p.name = 'immutability.manage';
|