Files
OPNexus/backend/app/schemas/host.py
T
Julian Boldt 20fd575420
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 v1.12.0
Netzwerk-Verwaltung und Übernahme von Hand angelegter Regeln. Alles gegen die
OPNsense-Core-Quellen geprüft und live auf drei Test-Firewalls (26.7.1 und
26.7.6) getestet, siehe Handbuch „Netzwerk, DNS und DHCP“:

- **Neuer Bereich „Netzwerk“:** Inventar je Firewall (Schnittstellen mit
  Adressen und CARP-Zustand, VLANs, virtuelle IPs, Gateways, Routen, DNS-Einträge,
  DHCP) und Änderungen daran: anlegen, bearbeiten, löschen für VLANs, virtuelle
  IPs, Gateways, statische Routen, Schnittstellen-Zuweisung samt IP-Konfiguration,
  Unbound-DNS-Einträge, dnsmasq-Hosts sowie Kea-Subnetze und -Reservierungen.
- **Abgesichert:** Jede Änderung läuft über eine Vorschau mit Diff und einem Token,
  der den Firewall-Stand erfasst. Vor dem Schreiben wird ein Konfig-Backup
  gespeichert (ohne Backup keine Änderung), danach angewendet und zurückgelesen.
  OPNexus erkennt, über welche Schnittstelle es die Firewall erreicht; Änderungen,
  die diese Verbindung kappen können (Verwaltungs-Schnittstelle, Standard-Gateway,
  Standardroute, Route über die Firewall-Adresse), brauchen eine ausdrückliche
  Bestätigung mit Eingabe des Firewall-Namens. Es gibt bewusst **keinen**
  automatischen Rollback: OPNexus kann eine Änderung nicht zurücknehmen, wenn sie
  seine eigene Verbindung trennt. Das Backup vor der Änderung ist der Rückweg.
- **Schnittstellen-Löschen:** OPNsense entfernt dabei selbst alle Regeln, die die
  Schnittstelle nutzen. Die Vorschau listet sie, die Bestätigung ist Pflicht.
  Bereits auf der Firewall vorgemerkte Änderungen blockieren eine neue Änderung.
- **IP-Konfiguration per API gibt es erst ab OPNsense 26.7.6.** Auf 26.7.1 ist die
  Zuweisungsliste nur lesbar; OPNexus erkennt das an den Daten und sperrt die
  Änderung mit Begründung.
- **Von der Firewall übernehmen:** Regeln, die direkt in OPNsense angelegt wurden,
  lassen sich in OPNexus übernehmen, bearbeiten und ausrollen. Die Übernahme schreibt
  nichts auf die Firewall. Regeln, die das Modell verfälschen würde (invertierte
  Quelle/Ziel, mehrere Schnittstellen, keine Quick-Regel, …), werden mit Grund
  abgelehnt. Neu: Regeln haben ein Protokollieren-Flag (`log`, Migration 0003),
  damit es beim Bearbeiten nicht still verloren geht; nicht modellierte Felder
  (Gateway, Zustandsoptionen, …) bleiben auf der Firewall unverändert.
- **Geheimnisse:** Das CARP-Passwort (liefert OPNsense im Klartext) und der
  Kea-DDNS-Schlüssel werden nie zurückgegeben, gespeichert oder auditiert.
- **Host-Adresse änderbar:** Hosts › Bearbeiten hat jetzt das Feld „Adresse“. Nach
  einer Änderung der Verwaltungs-IP muss der Host nicht mehr gelöscht und neu
  angelegt werden; Verlauf und Zuordnungen bleiben erhalten. Meldet eine Änderung
  „angewendet, aber nicht prüfbar“, weist die Oberfläche darauf hin.
- **Fehler aus den Live-Tests, die Mocks nicht gezeigt haben:** Jede Änderung einer
  virtuellen IP muss die Adresse (`network`) wiederholen, sonst leert OPNsense sie
  bzw. lehnt ab; die Prüfung nach dem Anwenden vergleicht jetzt den vollständigen
  Soll-Zustand statt nur der gesendeten Felder. Dazu: Gateway-Priorität als Zahl,
  Routen mit `enabled` statt `disabled`, Beschreibungsmuster der Schnittstellen,
  gesperrte Schnittstellen, Pflicht-Backup vor jeder Änderung.
- **Hinweis zu den Versionen:** Die IP-Konfiguration von Schnittstellen per API gibt
  es erst ab OPNsense 26.7.6. Mit 26.7.5 (die Prod-Firewalls) sind VLANs, virtuelle
  IPs, Gateways, Routen, DNS und DHCP verwaltbar, Schnittstellen nur lesbar.
2026-10-09 21:23:04 +02:00

155 lines
4.2 KiB
Python

import uuid
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.services.firewall_url import validate_base_url
class HostCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
base_url: str = Field(min_length=1, max_length=255)
api_key: str = Field(min_length=1, max_length=255)
api_secret: str = Field(min_length=1)
verify_tls: bool = True
# Pinned certificate of the firewall (PEM), obtained via POST /hosts/tls-probe.
tls_ca_pem: str | None = Field(default=None, max_length=32768)
enabled: bool = True
cluster_name: str | None = Field(default=None, max_length=120)
policy_hierarchy_group_id: uuid.UUID | None = None
@field_validator("base_url")
@classmethod
def _base_url_is_safe(cls, value: str) -> str:
return validate_base_url(value)
class TlsProbeRequest(BaseModel):
base_url: str = Field(min_length=1, max_length=255)
class TlsProbeResult(BaseModel):
pem: str
sha256: str
subject: str
issuer: str
self_signed: bool
not_before: datetime
not_after: datetime
expired: bool
san: list[str]
class HostUpdate(BaseModel):
"""Partial update — everything optional, only provided fields change.
Credential rotation (api_key/api_secret) isn't included yet; delete +
recreate is still the way to rotate a host's API key for now."""
name: str | None = Field(default=None, min_length=1, max_length=120)
# New address of the firewall, e.g. after its management IP was changed.
base_url: str | None = Field(default=None, min_length=1, max_length=255)
verify_tls: bool | None = None
tls_ca_pem: str | None = Field(default=None, max_length=32768)
clear_tls_pin: bool = False
enabled: bool | None = None
cluster_name: str | None = Field(default=None, max_length=120)
clear_cluster_name: bool = False # explicit flag, since cluster_name=None is ambiguous with "don't touch it"
policy_hierarchy_group_id: uuid.UUID | None = None
clear_policy_hierarchy_group_id: bool = False # same ambiguity as clear_cluster_name above
@field_validator("base_url")
@classmethod
def _base_url_is_safe(cls, value: str | None) -> str | None:
return validate_base_url(value) if value is not None else None
class HostRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
base_url: str
verify_tls: bool
tls_pinned: bool = False
tls_fingerprint: str | None = None
enabled: bool
cluster_name: str | None
policy_hierarchy_group_id: uuid.UUID | None = None
created_at: datetime
status: Literal["online", "offline", "disabled"]
carp_status: str | None = None
status_detail: str | None = None
class LiveRule(BaseModel):
uuid: str
description: str
action: str
action_label: str
direction: str
direction_label: str
interface: str | None
interface_label: str | None
protocol: str | None
protocol_label: str | None
source_net: str
source_port: str | None
destination_net: str
destination_port: str | None
enabled: bool
is_automatic: bool
class FirmwareInfo(BaseModel):
product_id: str
product_version: str
class InterfaceOption(BaseModel):
name: str
label: str
class AliasOption(BaseModel):
name: str
type: str
description: str
content: str
enabled: bool
class HostBackup(BaseModel):
id: str
created_at: datetime
size: int
CertStatus = Literal["valid", "expiring_soon", "expired", "unknown"]
class HostCertificate(BaseModel):
uuid: str
# OPNsense's own cross-references (OpenVPN cert/ca, Trust-Store caref) use
# this id, not the row UUID.
refid: str = ""
descr: str
common_name: str | None
issuer: str
valid_from: datetime | None
valid_to: datetime | None
days_until_expiry: int | None
status: CertStatus
in_use: bool
class HostCa(BaseModel):
uuid: str
refid: str = ""
descr: str
common_name: str | None
valid_from: datetime | None
valid_to: datetime | None
days_until_expiry: int | None
status: CertStatus