syncova-backup/migrations/000002_identity.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

397 lines
20 KiB
PL/PgSQL

-- Identität, RBAC, Sessions und Audit (Phase 1).
--
-- Grundsätze:
-- - Passwörter werden ausschließlich als Argon2id-Hash abgelegt (PROMPT.md §41).
-- - MFA-Secrets liegen verschlüsselt vor, niemals im Klartext (PROMPT.md §12).
-- - Sessiontokens werden ausschließlich als Hash gespeichert: ein Datenbankleck
-- erlaubt damit keine Übernahme laufender Sitzungen.
-- - Audit-Daten sind append-only (SYNCOVA_DATABASE.md §13).
-- ---------------------------------------------------------------------------
-- Benutzer
-- ---------------------------------------------------------------------------
CREATE TABLE users (
-- id ist der öffentliche Bezeichner des Benutzers.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- username ist der Anmeldename; er ist eindeutig.
username TEXT NOT NULL UNIQUE,
-- email ist optional, aber falls gesetzt eindeutig.
email TEXT UNIQUE,
-- password_hash enthält den Argon2id-Hash im PHC-Format inklusive Parametern.
-- NULL bedeutet: der Benutzer besitzt (noch) kein Passwort.
password_hash TEXT,
-- status steuert, ob eine Anmeldung möglich ist.
status TEXT NOT NULL DEFAULT 'active',
-- mfa_enabled meldet, ob mindestens ein zweiter Faktor aktiv ist.
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
-- failed_login_attempts zählt aufeinanderfolgende Fehlversuche (Brute-Force-Schutz).
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
-- locked_until sperrt die Anmeldung bis zu diesem Zeitpunkt.
locked_until TIMESTAMPTZ,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- last_login_at ist der Zeitpunkt der letzten erfolgreichen Anmeldung.
last_login_at TIMESTAMPTZ,
-- deleted_at kennzeichnet eine weiche Löschung; die Historie bleibt auditierbar.
deleted_at TIMESTAMPTZ,
-- Nur definierte Zustände sind zulässig; ein Tippfehler darf keinen
-- unbeabsichtigt zugriffsberechtigten Benutzer erzeugen.
CONSTRAINT users_status_valid CHECK (status IN ('active', 'disabled', 'locked'))
);
COMMENT ON TABLE users IS 'Benutzerkonten der Control Plane. Passwörter ausschließlich als Argon2id-Hash.';
COMMENT ON COLUMN users.password_hash IS 'Argon2id-Hash im PHC-Format. Niemals ein Klartextpasswort.';
-- Aktive Benutzer werden bei jeder Anmeldung gesucht.
CREATE INDEX users_status_idx ON users (status) WHERE deleted_at IS NULL;
-- ---------------------------------------------------------------------------
-- Zweiter Faktor
-- ---------------------------------------------------------------------------
CREATE TABLE user_mfa_methods (
-- id ist der Bezeichner der MFA-Methode.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- user_id verweist auf den Besitzer.
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
-- type benennt das Verfahren. V1 unterstützt totp; webauthn folgt später.
type TEXT NOT NULL,
-- secret_ciphertext ist das mit dem Datenschlüssel verschlüsselte Secret.
secret_ciphertext BYTEA NOT NULL,
-- key_version benennt den verwendeten Schlüssel und ermöglicht Rotation (PROMPT.md §143).
key_version TEXT NOT NULL,
-- enabled meldet, ob die Methode die Bestätigung durchlaufen hat.
-- Eine unbestätigte Methode darf keine Anmeldung ermöglichen.
enabled BOOLEAN NOT NULL DEFAULT FALSE,
-- last_used_time_step ist der zuletzt akzeptierte TOTP-Zeitschritt.
-- Er verhindert die Wiederverwendung eines abgefangenen Codes.
last_used_time_step BIGINT,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- confirmed_at ist der Zeitpunkt der erfolgreichen Bestätigung.
confirmed_at TIMESTAMPTZ,
CONSTRAINT user_mfa_methods_type_valid CHECK (type IN ('totp')),
-- Je Verfahren genügt eine Methode pro Benutzer.
CONSTRAINT user_mfa_methods_unique_per_type UNIQUE (user_id, type)
);
COMMENT ON TABLE user_mfa_methods IS 'Zweite Faktoren je Benutzer. Secrets ausschließlich verschlüsselt.';
COMMENT ON COLUMN user_mfa_methods.last_used_time_step IS 'Verhindert die Wiederverwendung eines bereits benutzten TOTP-Codes.';
-- ---------------------------------------------------------------------------
-- Wiederherstellungscodes
-- ---------------------------------------------------------------------------
CREATE TABLE user_recovery_codes (
-- id ist der Bezeichner des Codes.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- user_id verweist auf den Besitzer.
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
-- code_hash ist der Argon2id-Hash des Codes. Der Klartext existiert nur einmal
-- bei der Ausgabe an den Benutzer (PROMPT.md §43).
code_hash TEXT NOT NULL,
-- used_at ist der Zeitpunkt der Verwendung; ein Code gilt genau einmal.
used_at TIMESTAMPTZ,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE user_recovery_codes IS 'Einmalige Wiederherstellungscodes für den Verlust des zweiten Faktors.';
CREATE INDEX user_recovery_codes_user_idx ON user_recovery_codes (user_id) WHERE used_at IS NULL;
-- ---------------------------------------------------------------------------
-- Rollen und Berechtigungen
-- ---------------------------------------------------------------------------
CREATE TABLE roles (
-- id ist der Bezeichner der Rolle.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- name ist der eindeutige technische Name, z. B. backup_operator.
name TEXT NOT NULL UNIQUE,
-- description erklärt den Zweck der Rolle in der Oberfläche.
description TEXT,
-- is_system kennzeichnet mitgelieferte Rollen. Sie dürfen nicht gelöscht werden,
-- da sonst bestehende Zuweisungen ihre Bedeutung verlören.
is_system BOOLEAN NOT NULL DEFAULT FALSE,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- updated_at ist der Zeitpunkt der letzten Änderung in UTC.
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE roles IS 'Rollen der rollenbasierten Zugriffssteuerung.';
CREATE TABLE permissions (
-- id ist der Bezeichner der Berechtigung.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- name ist der eindeutige technische Name im Format bereich.aktion.
name TEXT NOT NULL UNIQUE,
-- description erklärt, was die Berechtigung erlaubt.
description TEXT NOT NULL
);
COMMENT ON TABLE permissions IS 'Einzelberechtigungen im Format bereich.aktion.';
CREATE TABLE role_permissions (
-- role_id verweist auf die Rolle.
role_id UUID NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
-- permission_id verweist auf die Berechtigung.
permission_id UUID NOT NULL REFERENCES permissions (id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
-- user_id verweist auf den Benutzer.
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
-- role_id verweist auf die zugewiesene Rolle.
role_id UUID NOT NULL REFERENCES roles (id) ON DELETE RESTRICT,
-- assigned_at ist der Zeitpunkt der Zuweisung in UTC.
assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, role_id)
);
COMMENT ON TABLE user_roles IS 'Zuordnung von Benutzern zu Rollen. ON DELETE RESTRICT schützt vor dem Löschen benutzter Rollen.';
-- ---------------------------------------------------------------------------
-- Sitzungen
-- ---------------------------------------------------------------------------
CREATE TABLE sessions (
-- id ist der Bezeichner der Sitzung.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- user_id verweist auf den angemeldeten Benutzer.
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
-- access_token_hash ist der SHA-256-Hash des Zugriffstokens.
-- Der Klartext verlässt den Server nur einmal in der Anmeldeantwort.
access_token_hash TEXT NOT NULL UNIQUE,
-- refresh_token_hash ist der SHA-256-Hash des Erneuerungstokens.
refresh_token_hash TEXT NOT NULL UNIQUE,
-- access_expires_at ist die Ablaufzeit des Zugriffstokens in UTC.
access_expires_at TIMESTAMPTZ NOT NULL,
-- refresh_expires_at ist die Ablaufzeit des Erneuerungstokens in UTC.
refresh_expires_at TIMESTAMPTZ NOT NULL,
-- revoked_at kennzeichnet eine widerrufene Sitzung (Abmeldung, Sperre, Rotation).
revoked_at TIMESTAMPTZ,
-- ip_address ist die Herkunft der Anmeldung.
ip_address INET,
-- user_agent ist die Kennung des verwendeten Programms.
user_agent TEXT,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- last_used_at ist der Zeitpunkt der letzten Verwendung in UTC.
last_used_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE sessions IS 'Aktive Sitzungen. Tokens ausschließlich als Hash gespeichert, damit ein Datenbankleck keine Sitzungsübernahme erlaubt.';
-- Jeder authentifizierte Request schlägt über den Token-Hash nach.
CREATE INDEX sessions_user_idx ON sessions (user_id) WHERE revoked_at IS NULL;
CREATE INDEX sessions_access_expiry_idx ON sessions (access_expires_at) WHERE revoked_at IS NULL;
-- ---------------------------------------------------------------------------
-- MFA-Anmeldeversuche
-- ---------------------------------------------------------------------------
CREATE TABLE mfa_challenges (
-- id ist der Bezeichner der Herausforderung und wird dem Client mitgeteilt.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- user_id verweist auf den Benutzer, dessen Passwort bereits stimmte.
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
-- expires_at begrenzt die Gültigkeit; eine offene Herausforderung ist ein Risiko.
expires_at TIMESTAMPTZ NOT NULL,
-- attempts zählt die Fehlversuche dieser Herausforderung.
attempts INTEGER NOT NULL DEFAULT 0,
-- consumed_at kennzeichnet die bereits eingelöste Herausforderung.
consumed_at TIMESTAMPTZ,
-- ip_address ist die Herkunft des Anmeldeversuchs.
ip_address INET,
-- created_at ist der Anlagezeitpunkt in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE mfa_challenges IS 'Offene MFA-Anmeldeversuche zwischen Passwortprüfung und zweitem Faktor.';
CREATE INDEX mfa_challenges_expiry_idx ON mfa_challenges (expires_at) WHERE consumed_at IS NULL;
-- ---------------------------------------------------------------------------
-- Audit
-- ---------------------------------------------------------------------------
CREATE TABLE audit_events (
-- id ist der Bezeichner des Ereignisses.
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- user_id verweist auf den Handelnden. NULL bei fehlgeschlagener Anmeldung
-- eines unbekannten Kontos oder bei Systemvorgängen.
user_id UUID REFERENCES users (id) ON DELETE SET NULL,
-- actor_username hält den Anmeldenamen fest. Er bleibt erhalten, auch wenn
-- der Benutzer später gelöscht wird - sonst verlöre das Protokoll seine Aussage.
actor_username TEXT,
-- action benennt die Handlung, z. B. USER_LOGIN oder DELETE_JOB.
action TEXT NOT NULL,
-- entity_type benennt die Art des betroffenen Objekts.
entity_type TEXT,
-- entity_id benennt das betroffene Objekt.
entity_id UUID,
-- result ist das Ergebnis der Handlung.
result TEXT NOT NULL,
-- ip_address ist die Herkunft der Anfrage.
ip_address INET,
-- user_agent ist die Kennung des verwendeten Programms.
user_agent TEXT,
-- details trägt unbedenklichen Zusatzkontext. Niemals Secrets.
details JSONB,
-- correlation_id verknüpft das Ereignis mit den Logzeilen derselben Operation.
correlation_id UUID,
-- created_at ist der Zeitpunkt des Ereignisses in UTC.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT audit_events_result_valid CHECK (result IN ('success', 'failure', 'denied'))
);
COMMENT ON TABLE audit_events IS 'Append-only Protokoll sicherheitsrelevanter Handlungen. Änderungen und Löschungen sind per Trigger unterbunden.';
COMMENT ON COLUMN audit_events.details IS 'Unbedenklicher Zusatzkontext. Enthält niemals Secrets.';
-- Auditrecherchen laufen fast immer über Zeitraum, Benutzer oder Aktion.
CREATE INDEX audit_events_created_at_idx ON audit_events (created_at DESC);
CREATE INDEX audit_events_user_idx ON audit_events (user_id, created_at DESC);
CREATE INDEX audit_events_action_idx ON audit_events (action, created_at DESC);
-- Das Auditprotokoll ist append-only.
--
-- Die Regel wird in der Datenbank durchgesetzt und nicht allein in der Anwendung:
-- ein Angreifer mit Datenbankzugriff soll seine Spuren nicht durch ein einfaches
-- UPDATE oder DELETE verwischen können (PROMPT.md §40).
CREATE OR REPLACE FUNCTION reject_audit_modification() RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Auditereignisse duerfen nicht veraendert oder geloescht werden (append-only).';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_events_no_update
BEFORE UPDATE ON audit_events
FOR EACH ROW EXECUTE FUNCTION reject_audit_modification();
CREATE TRIGGER audit_events_no_delete
BEFORE DELETE ON audit_events
FOR EACH ROW EXECUTE FUNCTION reject_audit_modification();
-- ---------------------------------------------------------------------------
-- Berechtigungen (Stammdaten)
-- ---------------------------------------------------------------------------
INSERT INTO permissions (name, description) VALUES
('users.read', 'Benutzer und deren Rollen einsehen'),
('users.write', 'Benutzer anlegen, ändern und löschen'),
('roles.read', 'Rollen und Berechtigungen einsehen'),
('roles.write', 'Rollen anlegen, ändern und löschen'),
('jobs.read', 'Backup-Jobs und deren Läufe einsehen'),
('jobs.write', 'Backup-Jobs anlegen, ändern und löschen'),
('jobs.run', 'Backup-Jobs sofort starten, pausieren und fortsetzen'),
('backups.read', 'Backups und Wiederherstellungspunkte einsehen'),
('restores.read', 'Wiederherstellungsvorgänge einsehen'),
('restores.execute', 'Wiederherstellungen durchführen'),
('repositories.read', 'Repositories und deren Zustand einsehen'),
('repositories.write', 'Repositories anlegen, ändern und prüfen'),
('agents.read', 'Agents und deren Zustand einsehen'),
('agents.write', 'Agents registrieren, sperren und Zugangsdaten wechseln'),
('providers.read', 'Virtualisierungsumgebungen einsehen'),
('providers.write', 'Virtualisierungsumgebungen verwalten'),
('verification.read', 'Verifikationsläufe und Ergebnisse einsehen'),
('verification.write', 'Verifikationen auslösen und abbrechen'),
('alerts.read', 'Meldungen und Ereignisse einsehen'),
('alerts.write', 'Meldungen bestätigen, auflösen und Regeln verwalten'),
('security.read', 'Sicherheitszustand und Befunde einsehen'),
('security.write', 'Sicherheitseinstellungen und Richtlinien ändern'),
('audit.read', 'Auditprotokoll einsehen'),
('reports.read', 'Berichte einsehen und erzeugen'),
('settings.read', 'Systemeinstellungen einsehen'),
('settings.write', 'Systemeinstellungen ändern'),
('monitoring.read', 'Zustand, Metriken und Statistiken einsehen');
-- ---------------------------------------------------------------------------
-- Rollen (Stammdaten laut PROMPT.md §42)
-- ---------------------------------------------------------------------------
INSERT INTO roles (name, description, is_system) VALUES
('viewer', 'Nur lesender Zugriff auf den Systemzustand', TRUE),
('backup_operator', 'Verwaltet Backup-Jobs und deren Ausführung', TRUE),
('restore_operator', 'Führt Wiederherstellungen durch', TRUE),
('security_administrator', 'Verwaltet Sicherheitseinstellungen, Benutzer und Rollen', TRUE),
('infrastructure_administrator', 'Verwaltet Repositories, Agents und Virtualisierungsumgebungen', TRUE),
('auditor', 'Liest Auditprotokoll und Berichte', TRUE),
('super_administrator', 'Vollzugriff auf alle Funktionen', TRUE);
-- Viewer: ausschließlich lesend.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'viewer'
AND p.name IN ('jobs.read', 'backups.read', 'restores.read', 'repositories.read',
'agents.read', 'providers.read', 'verification.read', 'alerts.read',
'monitoring.read', 'reports.read');
-- Backup Operator: Jobs verwalten und starten.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'backup_operator'
AND p.name IN ('jobs.read', 'jobs.write', 'jobs.run', 'backups.read',
'repositories.read', 'agents.read', 'providers.read',
'verification.read', 'verification.write', 'alerts.read',
'monitoring.read', 'reports.read');
-- Restore Operator: Wiederherstellungen durchführen, aber keine Jobs ändern.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'restore_operator'
AND p.name IN ('backups.read', 'restores.read', 'restores.execute', 'jobs.read',
'repositories.read', 'agents.read', 'providers.read',
'verification.read', 'alerts.read', 'monitoring.read');
-- Security Administrator: Sicherheit, Benutzer und Rollen.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'security_administrator'
AND p.name IN ('users.read', 'users.write', 'roles.read', 'roles.write',
'security.read', 'security.write', 'audit.read', 'alerts.read',
'alerts.write', 'settings.read', 'monitoring.read');
-- Infrastructure Administrator: Repositories, Agents, Virtualisierung.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'infrastructure_administrator'
AND p.name IN ('repositories.read', 'repositories.write', 'agents.read', 'agents.write',
'providers.read', 'providers.write', 'jobs.read', 'backups.read',
'alerts.read', 'alerts.write', 'monitoring.read', 'settings.read',
'settings.write', 'verification.read');
-- Auditor: Protokoll und Berichte, ausdrücklich ohne Schreibrechte.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'auditor'
AND p.name IN ('audit.read', 'reports.read', 'security.read', 'users.read',
'roles.read', 'jobs.read', 'backups.read', 'restores.read',
'repositories.read', 'monitoring.read', 'alerts.read');
-- Super Administrator: alle Berechtigungen.
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'super_administrator';
-- ---------------------------------------------------------------------------
-- Nachtrag zu Migration 000001
-- ---------------------------------------------------------------------------
-- system_settings konnte den Verweis auf den ändernden Benutzer erst erhalten,
-- nachdem die Benutzertabelle existiert (SYNCOVA_DATABASE.md §16).
ALTER TABLE system_settings
ADD COLUMN updated_by UUID REFERENCES users (id) ON DELETE SET NULL;
COMMENT ON COLUMN system_settings.updated_by IS 'Benutzer, der die Einstellung zuletzt geändert hat.';