Files
OPNexus/backend/app/models/backup_settings.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

49 lines
2.7 KiB
Python

"""Scheduled config backups and their offsite copy (singleton row + upload ledger).
The S3 secret key and the encryption passphrase live on disk (notification_secret_store,
names ``backup_s3_secret`` / ``backup_passphrase``), never here.
"""
import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class BackupSettings(Base):
__tablename__ = "backup_settings"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
scheduled_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
interval_hours: Mapped[int] = mapped_column(Integer, nullable=False, default=24)
keep_count: Mapped[int] = mapped_column(Integer, nullable=False, default=30)
offsite_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
s3_endpoint: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
s3_region: Mapped[str] = mapped_column(String(64), nullable=False, default="us-east-1")
s3_bucket: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
s3_prefix: Mapped[str] = mapped_column(String(255), nullable=False, default="opnexus")
s3_access_key: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
s3_path_style: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
s3_secret_set: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
passphrase_set: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
last_offsite_ok_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
last_offsite_error: Mapped[Optional[str]] = mapped_column(String(300), nullable=True)
class BackupUpload(Base):
"""Which local snapshot already exists offsite (so nothing is uploaded twice)."""
__tablename__ = "backup_uploads"
__table_args__ = (UniqueConstraint("host_id", "snapshot_id", name="uq_backup_upload"),)
id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
host_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), ForeignKey("opnsense_hosts.id", ondelete="CASCADE"), index=True)
snapshot_id: Mapped[str] = mapped_column(String(64), nullable=False)
remote_key: Mapped[str] = mapped_column(String(400), nullable=False)
uploaded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)