Files
OPNexus/backend/migrations/versions/0011_scheduled_backups.py
T
Julian Boldt b2c69f51a0
Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
Release v1.19.0
Backups, Governance, Regelnutzung. Auf einem Test-Stack mit drei Test-Firewalls geprüft; die Vollsicherung Ende-zu-Ende
(sichern, Schaden anrichten, zurückspielen), siehe Handbuch „Konfigurationssicherungen“, „Sichern und Wiederherstellen“.

- **Rolle „Operator“**: rollt aus und betreibt (Regeln, NAT, VPN, Wartung, Sicherungen), verwaltet aber keine
  Benutzer, Einstellungen oder Hosts und lädt keine Roh-Backups herunter. Abgelehnte Versuche stehen im Audit.
  Tokens eines Operators gehen bis zum Bereich „Ausrollen“.
- **Audit-Export** (Einstellungen): Syslog (RFC 5424, UDP/TCP/TLS) und/oder signierter Webhook, mindestens
  einmal zugestellt, ohne Objektinhalte. Neue Tabelle `audit_export_settings` (Migration `0010`).
- **Geplante Backups** mit Aufbewahrung, **verschlüsseltem Offsite-Ziel** (S3-kompatibel, AES-256-GCM,
  Passphrase bleibt auf dem Server), Benachrichtigungen bei Problemen und einer **Änderungsansicht** zwischen
  zwei Sicherungen (Geheimnisse nur als Hash). Neue Tabellen `backup_settings`, `backup_uploads`, Spalte
  `notification_settings.backup_failed_enabled` (Migration `0011`). Die S3-Signatur ist gegen die offiziellen
  AWS-Beispiele geprüft; ein Lauf gegen einen echten Anbieter steht noch aus.
- **Vollsicherung** `opnexus backup --full` und `opnexus restore`: Datenbank und alle Geheimnis-Volumes in einer
  verschlüsselten Datei; Ende-zu-Ende auf dem Test-Stack geprüft (Sichern, Schaden anrichten, Zurückspielen).
- **Regelnutzung**: wann hat jede Regel zuletzt etwas getroffen (aus den pf-Zählern, überlebt Neuladen und
  Neustart); Befund „Regeln in Benutzung“ in der Sicherheitslage nach 30 Tagen ohne Treffer. Neue Tabelle
  `rule_hits` (Migration `0012`).
- Änderungen an Einstellungen (`/api/settings/…`) und das Auslösen von Sicherungen stehen jetzt im Audit.
2026-10-20 00:26:57 +02:00

60 lines
2.9 KiB
Python

"""Geplante Backups mit Aufbewahrung und verschlüsseltem Offsite-Ziel; Benachrichtigung bei Backup-Problemen.
Revision ID: 0011
Revises: 0010
Create Date: 2026-10-20
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision = "0011"
down_revision = "0010"
branch_labels = None
depends_on = None
def upgrade() -> None:
inspector = sa.inspect(op.get_bind())
op.execute("ALTER TABLE notification_settings ADD COLUMN IF NOT EXISTS backup_failed_enabled boolean NOT NULL DEFAULT true")
if not inspector.has_table("backup_settings"):
op.create_table(
"backup_settings",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("scheduled_enabled", sa.Boolean(), nullable=False),
sa.Column("interval_hours", sa.Integer(), nullable=False),
sa.Column("keep_count", sa.Integer(), nullable=False),
sa.Column("offsite_enabled", sa.Boolean(), nullable=False),
sa.Column("s3_endpoint", sa.String(length=255), nullable=True),
sa.Column("s3_region", sa.String(length=64), nullable=False),
sa.Column("s3_bucket", sa.String(length=255), nullable=True),
sa.Column("s3_prefix", sa.String(length=255), nullable=False),
sa.Column("s3_access_key", sa.String(length=255), nullable=True),
sa.Column("s3_path_style", sa.Boolean(), nullable=False),
sa.Column("s3_secret_set", sa.Boolean(), nullable=False),
sa.Column("passphrase_set", sa.Boolean(), nullable=False),
sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_offsite_ok_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_offsite_error", sa.String(length=300), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if not inspector.has_table("backup_uploads"):
op.create_table(
"backup_uploads",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("host_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("snapshot_id", sa.String(length=64), nullable=False),
sa.Column("remote_key", sa.String(length=400), nullable=False),
sa.Column("uploaded_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["host_id"], ["opnsense_hosts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("host_id", "snapshot_id", name="uq_backup_upload"),
)
op.create_index("ix_backup_uploads_host_id", "backup_uploads", ["host_id"])
def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS backup_uploads")
op.execute("DROP TABLE IF EXISTS backup_settings")
op.execute("ALTER TABLE notification_settings DROP COLUMN IF EXISTS backup_failed_enabled")