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
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.
46 lines
1.8 KiB
Python
46 lines
1.8 KiB
Python
"""Client-side encryption of config backups before they leave the server.
|
|
|
|
File format (``.opxbak``): ``OPXBAK1`` | salt (16) | nonce (12) | AES-256-GCM ciphertext+tag.
|
|
The key is scrypt(passphrase, salt). The header is authenticated as associated data, so a
|
|
file cannot be re-labelled. The passphrase never leaves OPNexus' secret store; without it
|
|
an offsite copy is unreadable, which is the point — and also why it must be kept somewhere
|
|
else too (see the handbook). Decrypt with ``python -m app.backup_decrypt``.
|
|
"""
|
|
import os
|
|
|
|
from cryptography.exceptions import InvalidTag
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|
|
|
MAGIC = b"OPXBAK1"
|
|
_SALT, _NONCE = 16, 12
|
|
# scrypt cost: ~64 MiB, well above interactive defaults; a backup is encrypted rarely.
|
|
_N, _R, _P = 2**16, 8, 1
|
|
|
|
|
|
class DecryptError(Exception):
|
|
pass
|
|
|
|
|
|
def _key(passphrase: str, salt: bytes) -> bytes:
|
|
return Scrypt(salt=salt, length=32, n=_N, r=_R, p=_P).derive(passphrase.encode())
|
|
|
|
|
|
def encrypt(plaintext: bytes, passphrase: str) -> bytes:
|
|
salt, nonce = os.urandom(_SALT), os.urandom(_NONCE)
|
|
header = MAGIC + salt + nonce
|
|
return header + AESGCM(_key(passphrase, salt)).encrypt(nonce, plaintext, header)
|
|
|
|
|
|
def decrypt(blob: bytes, passphrase: str) -> bytes:
|
|
header_len = len(MAGIC) + _SALT + _NONCE
|
|
if len(blob) < header_len + 16 or not blob.startswith(MAGIC):
|
|
raise DecryptError("Keine OPNexus-Backup-Datei (Kopfzeile fehlt).")
|
|
header, body = blob[:header_len], blob[header_len:]
|
|
salt = header[len(MAGIC): len(MAGIC) + _SALT]
|
|
nonce = header[len(MAGIC) + _SALT:]
|
|
try:
|
|
return AESGCM(_key(passphrase, salt)).decrypt(nonce, body, header)
|
|
except InvalidTag:
|
|
raise DecryptError("Falsche Passphrase oder beschädigte Datei.") from None
|