feat: HA-Cluster v1.2.x — Split-Brain, TOTP, Enterprise-FW, Drift-Fix, VIP-Recovery

- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung
- keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload)
- confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix)
- TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login
- Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator
- fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen
- VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034)
- Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032)
- unbound-control: edgeguard in unbound-Gruppe via postinst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-31 18:18:31 +02:00
parent 49899e984c
commit 1d06b28064
55 changed files with 3132 additions and 672 deletions

View File

@@ -152,7 +152,10 @@
"emptyTitle": "Noch keine eigenen Firewall-Regeln.",
"emptyDesc": "Die System-Regeln oben halten SSH (rate-limited), HTTPS :443 und Mgmt-UI :3443 immer offen (Anti-Lockout). Eigene Regeln für app-spezifische Inbound-Ports oder zonenübergreifende Forwards anlegen.",
"logEnabled": "Logging aktiv — gematchte Pakete werden ins Firewall-Log geschrieben",
"ruleDisabled": "Regel deaktiviert"
"ruleDisabled": "Regel deaktiviert",
"enabled": "Aktiv",
"unnamed": "(kein Name)",
"zeroHitHint": "Keine Treffer seit dem letzten Neustart — möglicherweise ungenutzte oder überlagerte Regel"
},
"kpi": {
"policyRules": "Policy-Regeln",
@@ -281,7 +284,12 @@
"loggedInAs": "Angemeldet als",
"forgotPassword": "Passwort vergessen?",
"viewerBadge": "Nur lesen",
"viewerHint": "Dieser Account hat die Rolle Betrachter — Änderungen sind gesperrt. Ein Admin kann die Rolle anpassen."
"viewerHint": "Dieser Account hat die Rolle Betrachter — Änderungen sind gesperrt. Ein Admin kann die Rolle anpassen.",
"totp": {
"prompt": "Bitte gib den 6-stelligen Code aus deiner Authenticator-App ein.",
"verify": "Code bestätigen",
"invalidCode": "Ungültiger Code"
}
},
"reset": {
"title": "Admin-Passwort zurücksetzen",
@@ -392,6 +400,17 @@
"backends": "Backends",
"attached": "{{count}}/{{total}} Domains haben einen Primary-Backend"
},
"vipCard": {
"title": "VIP / VRRP",
"noVips": "Keine VIPs konfiguriert",
"keepalivedInactive": "keepalived läuft nicht",
"state": {
"MASTER": "MASTER",
"BACKUP": "BACKUP",
"FAULT": "FAULT",
"UNKNOWN": "Unbekannt"
}
},
"systemCard": {
"title": "System",
"version": "Version",
@@ -399,9 +418,16 @@
"ifaces": "Interfaces",
"wg": "WireGuard"
},
"networkServicesCard": {
"title": "Netzwerk-Dienste",
"configure": "Konfigurieren"
},
"alertsCard": {
"title": "Aktuelle Alerts",
"viewAll": "Alle anzeigen"
"viewAll": "Alle anzeigen",
"summary": "{{critical}} kritisch · {{warning}} Warnung",
"summaryWarning": "{{warning}} Warnung",
"summaryCritical": "{{critical}} kritisch"
},
"downBackendsAlert": "{{count}} Backend(s) komplett ausgefallen — kein Server UP",
"maintenanceAlert": "{{count}} Domain(s) im Wartungs-Modus",
@@ -664,7 +690,18 @@
"hintPrimary": "Auf dem Primary: edgeguard-ctl cluster-init-replication",
"hintStandby": "Auf dem Secondary: edgeguard-ctl cluster-setup-standby <primary-ip>",
"hintKeepalived": "Keepalived auf beiden Nodes: sudo systemctl enable --now keepalived",
"hintFailover": "Bei Failover: edgeguard-ctl promote (auf dem Secondary)"
"hintFailover": "Bei Failover: edgeguard-ctl promote (auf dem Secondary)",
"splitBrainSection": "Split-Brain-Schutz (Dual-Path VRRP + Gateway-Tracking)",
"hbInterface": "Heartbeat-Interface",
"hbInterfaceHelp": "Zweites Interface für die VI_HB-Instanz — VRRP-Advertisements laufen hier unabhängig von VI_1. Leer lassen um zu deaktivieren.",
"hbSrcIp": "Heartbeat-Quell-IP",
"hbSrcIpHelp": "Eigene IP auf dem Heartbeat-Interface (unicast_src_ip für VI_HB).",
"hbPeerIp": "Heartbeat-Peer-IP",
"hbPeerIpHelp": "Peer-IP auf dem Heartbeat-Interface (unicast_peer für VI_HB).",
"hbRouterId": "Heartbeat Router-ID",
"hbRouterIdHelp": "VRRP virtual_router_id für VI_HB — muss sich von der Haupt-Router-ID unterscheiden. Standard: 52.",
"gwCheckIp": "Gateway-Check-IP",
"gwCheckIpHelp": "Upstream-Gateway-IP die alle 5 s angepingt wird. Nicht erreichbar → Priorität sinkt um 110 → Failover wird ausgelöst. Leer lassen um zu deaktivieren."
},
"loadTitle": "Per-Node Resources (mTLS-Aggregator)",
"loadEmpty": "Keine Node-Resources verfügbar — Agent-Listener nicht erreichbar?",
@@ -699,7 +736,27 @@
"step3SetupDesc": "Setup-Wizard auf dem neuen Knoten öffnen (https://<node-fqdn>:3443/setup), \"Vorhandenem Cluster beitreten\" wählen, Primary-FQDN ({{primaryFqdn}}) eingeben und den Token oben einfügen.",
"generateNewToken": "Neuen Token generieren",
"setupWizardHint": "Setup-Wizard auf dem neuen Knoten öffnen",
"newNodeFqdnLabel": "FQDN des neuen Knotens"
"newNodeFqdnLabel": "FQDN des neuen Knotens",
"vipTest": {
"cardTitle": "VIP-Schwenk Test",
"cardDesc": "Verschiebt einen VIP temporär auf den Secondary um zu prüfen ob die Dienste korrekt antworten. Keepalived ist nicht beteiligt — reiner ip addr add/del Test.",
"colAddress": "VIP-Adresse",
"colInterface": "Interface",
"colActiveOn": "Aktiv auf",
"swingBtn": "→ Secondary",
"restoreBtn": "← Primary",
"swingOk": "VIP erfolgreich auf Secondary geschwenkt",
"restoreOk": "VIP zurück auf Primary",
"swingFailed": "VIP-Schwenk fehlgeschlagen",
"restoreFailed": "VIP-Rückschwenk fehlgeschlagen",
"noVips": "Keine VIPs konfiguriert (ip_addresses mit is_vip=true)",
"steps": "Schritte",
"stepOk": "OK",
"stepFail": "Fehler",
"confirmSwing": "{{addr}} auf Secondary verschieben?",
"confirmRestore": "{{addr}} zurück auf Primary?",
"unknown": "unbekannt"
}
},
"ssl": {
"title": "SSL-Zertifikate",
@@ -1126,7 +1183,16 @@
"flushCacheFailed": "Cache-Flush fehlgeschlagen",
"upstreamForwardsInvalid": "Jeder Forwarder muss eine gültige IP sein (z.B. 1.1.1.1 oder 9.9.9.9)",
"accessACLInvalid": "Jeder Eintrag muss eine gültige IP oder CIDR sein (z.B. 10.0.0.0/8)",
"cacheTTLError": "Cache-Max-TTL muss ≥ Cache-Min-TTL sein"
"cacheTTLError": "Cache-Max-TTL muss ≥ Cache-Min-TTL sein",
"cacheSection": "Cache",
"prefetch": "Häufige Records vorausladen",
"prefetchExtra": "Häufig abgefragte Records werden vor Ablauf der TTL neu aufgelöst — reduziert Latenz für bekannte Namen.",
"serveExpired": "Abgelaufene Records ausliefern",
"serveExpiredExtra": "Stale Cache-Einträge zurückgeben wenn Upstream-Resolver nicht erreichbar ist. Reduziert SERVFAIL bei Ausfällen.",
"msgCacheSizeMB": "Message-Cache (MB)",
"msgCacheSizeMBExtra": "RAM für DNS-Antwort-Cache (msg-cache-size). Standard 64 MB.",
"rrsetCacheSizeMB": "RRset-Cache (MB)",
"rrsetCacheSizeMBExtra": "RAM für Resource-Record-Cache (rrset-cache-size). Sollte ~2x Message-Cache sein. Standard 128 MB."
}
},
"fwd": {
@@ -1180,6 +1246,28 @@
"dstdom_regex": "dstdom_regex — Domain-Regex",
"srcdom_regex": "srcdom_regex — Quell-Domain-Regex",
"browser": "browser — User-Agent-Regex"
},
"settings": {
"title": "Proxy-Einstellungen",
"listenAddresses": "Listen-Adressen",
"listenAddressesExtra": "Komma-getrennte IPs auf denen Squid lauscht (z.B. 10.0.5.1, 10.0.20.1). Leer = alle Interfaces.",
"listenPort": "Port",
"listenPortExtra": "Standard: 3128.",
"saveFailed": "Einstellungen konnten nicht gespeichert werden.",
"cacheSection": "Cache",
"cacheMemMB": "RAM-Cache (MB)",
"cacheMemMBExtra": "RAM den Squid für Caching nutzt (cache_mem). Standard 64 MB.",
"cacheDirMB": "Disk-Cache (MB)",
"cacheDirMBExtra": "Speicherplatz für den UFS-Cache. Standard 100 MB.",
"maxObjSizeMB": "Max. Objektgröße (MB)",
"maxObjSizeMBExtra": "Größtes Objekt das Squid cached. Größere Objekte werden direkt durchgeleitet. Standard 4 MB.",
"timeoutSection": "Timeouts",
"connectTimeout": "Verbindungs-Timeout (s)",
"connectTimeoutExtra": "Sekunden, die Squid beim Verbindungsaufbau zum Upstream wartet.",
"readTimeout": "Lese-Timeout (s)",
"readTimeoutExtra": "Sekunden zwischen aufeinanderfolgenden Lesevorgängen vom Upstream.",
"requestTimeout": "Anfrage-Timeout (s)",
"requestTimeoutExtra": "Maximale Zeit für einen vollständigen Request/Response-Zyklus."
}
},
"common": {
@@ -1205,6 +1293,8 @@
"retry": "Erneut versuchen",
"close": "Schließen",
"refresh": "Aktualisieren",
"back": "Zurück",
"next": "Weiter",
"up": "UP",
"down": "DOWN",
"relTime": {
@@ -1542,7 +1632,24 @@
"errorEmailTaken": "Diese E-Mail-Adresse wird bereits verwendet.",
"cannotDeleteSelf": "Das eigene Konto kann nicht gelöscht werden.",
"you": "Ich",
"never": "Nie"
"never": "Nie",
"totp": {
"on": "2FA",
"off": "",
"setup": "2FA einrichten",
"manage": "2FA verwalten",
"disable": "2FA deaktivieren",
"disableFor": "2FA für {{email}} deaktivieren",
"enabled": "2FA wurde aktiviert",
"disabled": "2FA wurde deaktiviert",
"setupTitle": "Zwei-Faktor-Authentifizierung einrichten",
"manageTitle": "Zwei-Faktor-Authentifizierung",
"scanHint": "Scanne den QR-Code mit Google Authenticator, Authy oder einer kompatiblen App.",
"enterCode": "Gib den 6-stelligen Code aus deiner Authenticator-App ein:",
"confirm": "Bestätigen & aktivieren",
"alreadyEnabled": "2FA ist für diesen Account aktiv.",
"disableHint": "Klicke auf 'Deaktivieren' um 2FA für diesen Account zu entfernen."
}
},
"audit": {
"title": "Audit-Log",