Preempt-Rückkehr (preempt_delay 120) wieder aktiv: der bevorzugte Node
(PG-Primary, Prio 200) holt die VIP nach Erholung zurück. Der Incident
2026-08-03 (halb-kaputter Node riss die VIP an sich) wird verhindert, weil
keepalived-check.sh jetzt zusätzlich fordert:
- haproxy-Prozess aktiv
- :443 gebunden (bedient wirklich Traffic)
Ein nicht-bedienender Node geht damit in FAULT und kann NICHT (mehr) preempten.
Außerdem: CrowdSec-Management-Whitelist (Backend api_backend) fest ins postinst
gebacken (Admin-SPA-Traffic wird nie mehr als http-crawl gebannt, IP-unabhängig,
Incident-Root-Fix) + Altlast netcell-mgmt-whitelist.yaml wird aufgeräumt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WordPress-Plugin nutzt den Branch `master` (nicht `main`), und `curl -sL` ohne
-f wertete den 404 als Erfolg → WordPress-Plugin wurde still übersprungen. Fix:
beide Branches probieren, curl -f (harter HTTP-Fehler), Tarball-Extraktion als
Erfolgskriterium.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Statt manueller SecRuleRemoveById-IDs kann man pro Domain offizielle OWASP-CRS-
Exclusion-Plugins aktivieren — pfad-genaue, upstream-gepflegte App-Ausnahmen.
- Migration 0046: waf_configs.crs_plugins text[].
- Engine (engine.go): je gewähltem Plugin werden config/before VOR den CRS-Rules
und after DANACH inkludiert (exakt nach OWASP-CRS-Plugin-Spec); nur die für
DIESE Domain gewählten, nur wenn die Datei existiert. Whitelist KnownCRSPlugins.
- Handler: crs_plugins im Upsert-Body + Whitelist-Validierung (Include-Pfad-
Injection-Schutz).
- Packaging (postinst): lädt die Plugins (coreruleset/<name>-plugin) nach
<crs>/plugins/ — self-healing auf jedem configure, nur fehlende.
- UI: Multi-Select „App-Profile (CRS-Plugins)" im WAF-Config-Drawer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
keepalived-master.sh loggte beim VIP-Übernehmen "PG-Rolle ist noch 'standby'",
weil es /var/lib/edgeguard/pg_role las — die schreibt nur `promote`, ein via
cluster-init-replication eingerichteter Primary hat sie nie → falsches Label.
Jetzt zuverlässig über pg_publication (nur der Primary trägt edgeguard_shared,
Konvention wie cluster_repair.go): Primary → "bereits PG-Primary (kein promote
nötig)", sonst → "standby, edgeguard-ctl promote". Rein kosmetisch/Logging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- fix(scheduler): backend.down-Check läuft nur noch wenn dieser Node den VIP
hält (nodeHoldsVIP). Ein keepalived-BACKUP-Node hat KEINE VLAN-IP → erreicht
die Backend-Subnetze nicht → sah bisher ALLE Backends L4-down und feuerte
Dauer-Fehlalarme (Hauptquelle des alert_events-Spams). Master sieht die
echten States.
- feat(ctl): `cluster-reconcile-replication` — bringt Publication/Grants/
Subscription idempotent in den Soll-Zustand (Publisher: fehlende Shared-
Tables ADD, node-lokale DROP, GRANT SELECT für Replikator; Subscriber:
neue Tabellen leeren + REFRESH). Läuft im postinst nach migrate.
- fix(packaging): postinst re-added network_interfaces/ip_addresses bei JEDEM
Upgrade in die Publication (alter fester Block) → ersetzt durch den Reconcile.
DAS war die Wiederkehr-Ursache.
- fix(replication): waf_alerts → localOnlyTables (Event-Daten, node-lokal wie
alert_events).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nach v1.2.102 (VIP nicht mehr statisch gebunden) wandert die Public-VIP .100
erstmals wirklich per keepalived zwischen den Node-MACs. Der Hosting-Upstream
lernte die neue MAC aber nicht zuverlässig → .100 nach Schwenk von außen
unerreichbar (vorher maskiert, weil .100 statisch dauerhaft auf utm-1 lag).
Zwei Mechanismen ergänzt:
1) keepalived.conf.tpl global_defs: vrrp_garp_master_repeat 5 +
vrrp_garp_master_refresh 60 (+ vrrp_garp_interval/gna 0, aus Legacy-Template
verloren gegangen) → forciertes GARP beim Wechsel + periodische Auffrischung,
damit die VIP-MAC am Switch nicht altert.
2) keepalived-master.sh: neuer Master pingt den Default-Gateway aus jeder
Public-IP/VIP an (ping -I <vip>) → Upstream sieht Traffic VON der VIP und
lernt die MAC sofort. Manche Hoster relearnen nur so, nicht via GARP.
Test: keepalived_test.go prüft GARP-Direktiven im Render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ursache der „WireGuard reißt immer wieder ab"-Abrisse war NICHT die UniFi,
sondern keepalived-Flapping im HA-Cluster: die VIP 89.163.205.100 (an der die
UniFi-Site-to-Site hängt) wanderte bei ~17 VRRP-Wahlen/Tag zwischen utm-1/utm-2
→ Tunnel-Abriss bei jeder Wahl.
Drei Bugs:
1) Firewall ließ VRRP (IP-Proto 112) zwischen den Cluster-Peers NICHT zu
(policy drop). Adverts überlebten nur via conntrack-Reverse-Matching → bei
conntrack-Ablauf gedroppt → Peer promotet sich → Split-Brain.
Fix: ruleset.nft.tpl erlaubt `ip/ip6 ... vrrp saddr @peer_ipv4/6`;
firewall.go nimmt zusätzlich hb_src_ip/hb_peer_ip aus cluster_settings ins
Peer-Set (deckt den Heartbeat-Pfad 169.254.0.x ab).
2) Kein nopreempt → erholter Node riss die VIP sofort zurück (Flap-Back);
aggressiver gw-Check (fall 2 → 10s-Blip = Failover).
Fix: keepalived.conf.tpl mit `nopreempt` in VI_1+VI_HB, chk_gateway fall 2→5;
keepalived.go setzt State immer BACKUP (nopreempt wirkt nur in BACKUP),
Priorität 200/100 aus pg_role bleibt → deckt sich mit „manuelles Promote".
3) keepalived-Boot-Race: Unit startete vor vlan500 (nur After=network-online)
→ „interface vlan500 doesn't exist" → permanenter CONFIG-Crash ohne Recovery
(keepalived nach Reboot tot). Fix: postinst legt Drop-in mit
After=/Wants=edgeguard-interfaces.service + Restart=on-failure an.
Neuer Test internal/keepalived/keepalived_test.go (nopreempt/BACKUP/fall).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zwei Bugs, die WireGuard-Verbindungen verhinderten/abrissen:
1) Client-Config-Endpoint war hartkodierter Platzhalter REPLACE_WITH_PUBLIC_HOST → neue Clients bauten nie einen Tunnel auf (Host löst nicht auf). Jetzt: WireguardHandler.PublicHost (aus setup.json FQDN, main.go) → Endpoint = <fqdn>:<port>. Platzhalter nur noch als Fallback wenn FQDN unbekannt.
2) Renderer machte bei JEDER Config-Änderung 'systemctl restart wg-quick@<iface>' → voller Link-Flap, alle Peers droppen (verstößt gegen 'wireguard darf nie abbrechen'). Jetzt: laufendes Interface → 'wg-quick strip | wg syncconf' (Peers/Listen-Port live, KEIN Abbruch); nur erstmaliges Hochfahren via systemctl start; restart nur noch als Fallback mit WARN. interfaceExists() via 'ip link show'. Neue sudoers: wg syncconf *, wg-quick strip *.
Ein edgeguard-api-Restart (Deploy) fasst wg-quick@<iface> nicht an → Tunnel bleibt während Deploy bestehen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cluster/Replication:
- Drift-Banner: Button 'Resync erzwingen' baut die PG-Logical-Replication-
Subscription neu auf (via edgeguard-ctl cluster-setup-standby).
- Primary-Dispatch: Button auf dem Primary delegiert per mTLS an den
Standby (POST /agent/cluster/repair-replication); auf dem Standby lokal.
- Status-Proxy Primary->Standby via Aggregator.FanOut; Erfolg = Job-success
ODER drift_found wird false (--collect-Unit verschwindet nach Erfolg).
- Job als transiente systemd-Unit edgeguard-repair-replication.service
(sudoers exact-match + festes Script wie upgrade.sh).
- Banner-Text korrigiert (keine 'Outbox').
Frontend-Stabilität:
- Stale-Chunk-Auto-Reload: Lazy-Import-Fehler nach Deploy ('Failed to fetch
dynamically imported module') lösen einen einmaligen Reload aus (Loop-
Schutz via sessionStorage) statt einer Fehlerseite. Globaler
vite:preloadError-Listener + ErrorBoundary-Integration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- SPOE-Config: option continue-on-error — HAProxy blockt nie wegen
SPOE-Timeout (z.B. CRS-Engine-Load beim ersten Request). Ohne dieses
Flag waren alle Requests geblockt wenn der WAF-Agent kurz nicht
antwortete, auch für Domains ohne WAF-Konfiguration.
- SPOE-Config: timeout processing 50ms → 1s — CRS-Load braucht >50ms
- postinst: *.conf → * beim CRS-Copy — .data-Dateien wurden nicht
mitkopiert, SecRule @pmFromFile scanners-user-agents.data fehlte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
applyAsync()-Goroutinen in networks.go und ipaddresses.go nutzten
c.Request.Context() — der wird gecancelt wenn der Handler zurückkehrt,
was zu "query: context canceled" in jedem Apply-Lauf führte.
Fix: context.Background() direkt in der Goroutine.
StartLimitIntervalSec=0 in beiden apply-Services verhindert dass
systemd bei mehrfachen schnellen Triggers (Burst > 5) drosselt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fügt zwei neue systemd-oneshot-Services hinzu:
- edgeguard-interfaces.service: erstellt VLAN/bridge/bond-Interfaces
(ip link add/del) wenn der Operator sie über die GUI anlegt/entfernt.
Ethernet + WireGuard bleiben OS-managed.
- edgeguard-ipaddresses.service: bindet/entfernt IP-Adressen (ip addr
add/del) nach jeder GUI-Mutation; läuft jetzt After=edgeguard-interfaces
damit Interfaces immer vor den Adressen existieren.
Beide Services triggern per applyAsync() in den zugehörigen Handlern
(networks.go → Interfaces, ipaddresses.go → Adressen). Diff-Ansatz
über *-applied.conf verhindert dass manuell gebundene Adressen/
Interfaces angefasst werden.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PG Logical Replication: edgeguard_shared PUBLICATION auf Primary,
edgeguard_sub SUBSCRIPTION auf Secondary. Nur geteilte Config-Tabellen
werden repliziert; node-eigene Daten (network_interfaces, ip_addresses,
static_routes, cluster_settings, dns_settings, ntp_settings) bleiben
lokal — OPNsense-Muster.
- cluster-init-replication: Erstellt PUBLICATION, Rolle + pg_hba-Einträge
(logical + replication), WAL-Level auf logical.
- cluster-setup-standby: Erstellt SUBSCRIPTION (copy_data=true), pollt
pg_subscription_rel bis alle Tabellen sync = 'r', rendert dann Configs.
- promote: manueller Failover via pg_promote() + touch recovery.signal.
- VIP/Keepalived: cluster_settings-Tabelle (vip_address, vip_interface,
vrrp_router_id), /cluster/vip-settings API, Keepalived-Config-Generator
mit VRRP + check_script + notify-Skripten in /usr/lib/edgeguard/scripts/.
- config_hash sync: Secondary pusht alle 5 Min seinen Hash via mTLS an
Primary (PushSelfToPrimary). Heartbeat schreibt nur LOCAL, daher ohne
aktiven Push wäre Primary-Sicht des Secondary-Hash stale gewesen.
- runSecondaryConfigRender: Goroutine auf Secondary rendert HAProxy+nftables
neu wenn config_hash sich ändert (Logical-Replication-Nachzügler).
- confighash: node-spezifische Tabellen aus hashSpec entfernt.
- postinst: Keepalived-Skripte installieren, sudoers für keepalived.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WireGuard-Peer-Änderungen landeten nicht im laufenden Interface:
/etc/wireguard/ ist root:root 700, os.Readlink schlug fehl →
ensureWGQuickSymlink fiel immer in den Error-Pfad. Fix: Symlink
via sudo /bin/ln -sf (sudoers-Entry in postinst ergänzt).
- Site-to-Site-Masquerade: Roadwarrior-Clients (z. B. 192.168.99.3)
konnten LANs hinter anderen Peers nicht erreichen, weil das remote
Gateway die VPN-Client-IP nicht als Tunnel-Route kannte. Fix: auto
masquerade in nftables postrouting_nat pro WireGuard-Server-Interface
(oifname "wg7" ip saddr 192.168.99.0/24 masquerade).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code-Vorbereitung für Multi-Node, ohne dass eine zweite Box nötig ist.
Single-Node-Mode bleibt der Default; alles existiert und wird sichtbar,
sobald ein 2. Knoten joined (Phase 3.2 später).
Migration 0020:
ha_nodes += version (edgeguard-api-Version)
config_hash (drift-Detection-Hash)
mgmt_ip (Management-IP, niemals VIP)
status (online|offline|joining|leaving|unknown)
internal/cluster/local_config.go:
/etc/edgeguard/node.conf — INI-style, node-lokale Identität:
NODE_ID, HOSTNAME, MGMT_IP, ROLE, PEER_HOSTS. NIEMALS zwischen
Cluster-Peers replizieren. LoadLocalConfig / SaveLocalConfig /
EnsureLocalConfig (auto-Generierung beim ersten Boot).
MgmtIP-Default = firstNonLoopbackIPv4(); Operator kann
überschreiben (mehrere Interfaces).
internal/cluster/store.go:
- HANode-Model um die 4 neuen Felder erweitert
- UpsertSelf nimmt jetzt mgmt_ip/version/config_hash/status, COALESCE
erhält werte wenn der Caller sie nicht setzt
- EnsureSelfRegistered-Signatur: + role + version-Argument
internal/handlers/cluster.go:
GET /api/v1/cluster/status — strukturierter Endpoint:
{local_id, local_node, peers[], mode, health, drift_found, updated_at}
GET /api/v1/cluster/nodes bleibt für Tools.
UI (pages/Cluster):
- Header zeigt Mode-Tag (Single-Node / Cluster) + Health-Tag (OK /
degraded / split-brain)
- Self-Card: Descriptions mit FQDN, Node-ID, Status, Role, Version,
MGMT-IP, API-URL, Config-Hash
- Peers-Tabelle nur wenn vorhanden, mit "drift"-Marker pro Row
- Drift-Alert-Banner wenn ein Peer einen anderen config_hash hat
- Single-Node-Mode Hinweis-Alert ("cluster-join kommt in 3.2")
postinst: leeres /etc/edgeguard/node.conf wird angelegt (chown
edgeguard); API auto-befüllt beim ersten boot.
main.go ruft EnsureLocalConfig + EnsureSelfRegistered mit version.
Verifiziert auf der Box (1.0.70):
- /etc/edgeguard/node.conf hat NODE_ID, HOSTNAME, MGMT_IP=89.163.205.6,
ROLE=primary
- ha_nodes-Row: status=online, version=1.0.70, mgmt_ip=89.163.205.6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI-Restruktur nach User-Feedback:
- Sidebar-Eintrag „Firewall-Live" entfernt — gehört thematisch unter
Firewall, kein Top-Level-Item. Standalone-Page /firewall-live raus.
- Neuer Firewall-Tab „Live-Log" zwischen NAT und Zonen.
- Default = AUS: zeigt Empty-State mit Start-Button. WebSocket
verbindet erst nach Klick. Stop-Button schließt explizit.
- Filter-Inputs (src/dst/rule_id) jetzt 300ms debounced — vorher
triggerte jeder Tastendruck einen WS-Reconnect.
Server-Pipeline „wirklich live" gepinnt:
- ulogd.conf NFLOG-Plugin bekommt qthreshold=1 + qtimeout=1. Default
des Kernels batched Pakete bis 1024 oder 1s; mit 1/1 fließt jedes
Paket sofort. Critical für die Wahrnehmung „live" statt „bursty".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation für Live-Log + Firewall-History (Logsystem Phase 1):
- nft-Renderer: `log prefix "edgeguard:<rule-id>" group 0` für Rules
mit log=true. Ohne `group` schrieb nft in kernel-log (dmesg), nie
in netlink → ulogd2 sah nichts.
- ulogd2 + ulogd2-json als Depends, postinst legt /etc/ulogd.conf
(NFLOG group 0 → /var/log/edgeguard/firewall.jsonl) + logrotate-
Profil (14d, daily, copytruncate) + enable/restart ulogd2.service.
- /var/log/edgeguard/ ist root:edgeguard 0640 — ulogd2 schreibt
(root), edgeguard-api liest (UI-Endpoints kommen in Phase 2).
End-to-End smoke-test bestätigt: ICMP echo → JSON-Line mit allen
Feldern (src_ip, dest_ip, oob.prefix, oob.in, icmp.*) in ~30ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edgeguard-api.service hat PrivateTmp=true → schreibt in privates /tmp.
Die per `sudo systemd-run` gestartete Transient-Unit sah das nicht und
brach mit "bash: /tmp/edgeguard-upgrade.sh: No such file or directory"
ab — Modal hing endlos. Pfad jetzt /var/lib/edgeguard/upgrade.sh
(edgeguard-owned, persistent, in beiden Namespaces sichtbar). Sudoers
entsprechend angepasst.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handler: edgeguard-User darf systemd-run nicht direkt aufrufen ("Inter-
active authentication required"). sudo -n + sudoers-Whitelist auf
exakt die Unit-Form für edgeguard-upgrade.service.
UI: UpdateBanner-Komponente neu — Pattern wie mail-gateway/enconf:
Banner mit Force-Check-Button + Popconfirm. Beim Apply zeigt full-
screen-Overlay mit animiertem Orbit (zwei Ringe + Dots), Versions-
sprung, vier Step-Indicators (Download/Install/Restart/Verify) und
Live-Timer. Poll auf /system/health detektiert Version-Flip ODER
"sah down dann up" und window.reload nach 1.5s. Sicherheits-Timeout
2 min schickt sonst auch reload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
API läuft als edgeguard-User; ohne /etc/sudoers.d/edgeguard-Whitelist
schreibt apt-get update nicht in /var/lib/apt/lists/ und der candidate
bleibt auf dem Stand des letzten postinst-Runs. Erfordert einmalig
manuelles `sudo apt-get update && apt install edgeguard-api edgeguard-ui
edgeguard` um auf 1.0.48 zu kommen — ab dann läuft der Check automatisch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: Service-Health-Grid hat 'nftables.service' per systemctl
abgefragt. Distro-Unit ist disabled (wir laden via 'nft -f' aus
dem Renderer) → Dashboard zeigte FW als 'inactive', obwohl Pakete
sehr wohl gefiltert werden.
Fix: Special-case in /system/services für unit='nftables'. Status
= existiert 'table inet edgeguard' im Kernel-Ruleset (sudo nft list
tables). 'kernel-loaded' wenn ja, 'no-table' wenn nein.
Plus: sudoers im postinst erweitert um 'nft list tables' + 'nft list
table inet edgeguard'.
Version 1.0.44.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: Unbound bindet Listen-Sockets nur beim startup. Bei einer
Mutation von dns_settings.listen_addresses (z.B. neue LAN-IP für
Resolver-Zugriff) hat 'systemctl reload' die Config zwar gelesen,
aber nicht neu gebound — neue IPs blieben tot.
Fix: Renderer ruft RestartService statt ReloadService. ~200ms
Resolver-Downtime beim Save, dafür konsistentes Verhalten für jede
Settings/Zone/Record-Mutation.
Plus configgen.RestartService Helper neu (analog ReloadService),
sudoers im postinst um systemctl restart unbound.service erweitert.
NOTE für DNS-LAN-Zugang: zwei Operator-FW-Rules nötig (DNS-UDP +
DNS-TCP from any to any) wenn der Resolver auf LAN-IPs lauscht.
Aktuell manuell anzulegen — ein Auto-Rule-Generator (analog
NAT-auto-forward) wäre die nächste Iteration.
Version 1.0.36.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mehrere zusammenhängende Fehler beim Import der NAT-Rules von der
alten EdgeGuard-Box gefunden + behoben:
1. nft-Template: NAT-Rules landeten als Comment (gleicher
Whitespace-Trimmer-Bug wie bei den Operator-Rules vor zwei
commits). Fix: Body auf eigener Zeile via {{""}}-Padding.
2. nft-Syntax-Reihenfolge: emittierte 'tcp ip daddr X dport Y' →
parser-Fehler. Korrekt ist L3-match (ip saddr/daddr) zuerst,
dann L4 (tcp/udp dport). Reihenfolge in der dnat-Zeile
getauscht.
3. eth0 als Iface-Row hinzugefügt (Type ethernet, role wan) damit
der zone→iface-Lookup für 'wan' tatsächlich auf das Linux-Iface
trifft. Vorher war nur 'WAN'-bridge in der DB, das im Kernel
nicht existiert → iifname-match griff nicht.
4. forward-chain: ct status dnat accept (DNAT-Pakete dürfen
forwarden) + Auto-Forward pro SNAT/masquerade-Rule für die
Origin-Pakete (return geht via established,related).
5. postrouting_nat: ct status dnat masquerade als Hairpin-Catch-All
— sonst antwortet das DNAT-Ziel via seinem default-GW (oft
nicht zur EdgeGuard-Box) → SYN_SENT + UNREPLIED. Trade-off:
Backend sieht Box-IP statt client-IP.
6. Sysctl-Profil /etc/sysctl.d/99-edgeguard.conf bei jedem Install:
- Forwarding (ip_forward + ipv6 forwarding) — Voraussetzung für
ALLES NAT/DNAT/Masquerade.
- Conntrack-Buckets + max=524288 (Edge-Box trackt viele
parallele Sessions).
- HAProxy-Tuning (somaxconn 64k, rmem/wmem 16M, keepalive,
tcp_tw_reuse, ip_local_port_range).
- BBR + fq als modernes Congestion-Control + Queueing.
- Anti-DoS: tcp_syncookies, log_martians, kptr_restrict.
Verified end-to-end:
$ nc -v 89.163.205.100 2030
SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.16
Version 1.0.25.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: render-config --no-reload schrieb nur die Files; haproxy
wurde explizit per systemctl restart unten neu gefahren, aber
nft-Set blieb beim Kernel-Stand vom letzten Boot. Bug sichtbar bei
1.0.13: Anti-Lockout-Eintrag für 3443 war im Template, aber der
Kernel hatte die Regel nicht — Port von außen blockiert.
Fix: zwei render-Calls — haproxy mit --no-reload (wie bisher),
nftables ohne, damit `sudo nft -f` direkt nach dem Schreiben
ausgeführt wird.
Version 1.0.14.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pages auf PageHeader/StatusDot/ActionButtons-Pattern migriert:
* Dashboard — Komplett-Rewrite. KPI-Tiles (Domains, Backends, Iface,
FW-Rules, NAT, WG), Detail-Cards (WireGuard live status, Firewall
zone overview, SSL expiring soon, Cluster nodes, Routing summary,
System info). Polled queries pro Card.
* Domains, Backends, RoutingRules, Networks, IPAddresses, SSL,
Cluster, Settings, Firewall (index) — alle inline Action-Buttons
→ ActionButtons; alle Yes/No-Renders → StatusDot; Add-Button in
DataTable.extraActions; PageHeader oben.
WireGuard
---------
* Neuer /wireguard/status-Endpoint parsed `wg show all dump`,
liefert {iface, peer_pubkey, endpoint, last_handshake_unix, rx, tx}.
Sudoers im postinst um `wg show` erweitert.
* Server-Drawer Peer-Liste zeigt jetzt Live-Status (Online/Offline-
Dot, "vor Xs", Traffic-Counter) per 10s-Polling. Importierte
"Unify Home" peer kann jetzt im UI verifiziert werden.
* Importer-Bug fixed: nextName ("# Unify Home" comment) wurde beim
Sektionswechsel zu früh geresettet — jetzt nur nach echtem
flushPeer.
Routing-Rules
-------------
* Aus Sidebar entfernt. URL bleibt funktional, aber für 90% der
Setups reicht domains.primary_backend_id (das HAProxy ohnehin
als default_backend rendert). Path-basiertes Routing ist ein
Advanced-Feature und kommt später als Domain-Modal-Tab zurück.
* nav.routing-Sidebar-Eintrag + BranchesOutlined-Import entfernt.
Misc
----
* "Firewall (v2)" → "Firewall" im Nav (DE).
* Dashboard-i18n Block in DE+EN.
* Version 1.0.11 → 1.0.12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
internal/firewall/firewall.go komplett neu: joint zone-iface-mapping
(network_interfaces.role), address objects + groups (members
expandiert), services + groups, rules, nat-rules. Output: einheitliche
View mit Legs (rule × service cross-product) damit das Template kein
sub-template/dict braucht.
Template:
* Anti-Lockout-Block am input-chain-Top (SSH+443 immer erlaubt,
KANN nicht von Custom-Rules overruled werden — User-Wunsch).
* Rules: pro Leg eine nft-Zeile mit iif/oif sets, ip saddr/daddr,
proto+dport, optional log-prefix.
* prerouting_nat: iteriert dnat-Rules.
* postrouting_nat: snat + masquerade.
Auto-apply: FirewallHandler bekommt einen Reloader-Hook der nach
jedem POST/PUT/DELETE aufgerufen wird. main.go injected
firewall.New(pool).Render — schreibt + sudo nft -f.
Sudoers (/etc/sudoers.d/edgeguard): NOPASSWD für 'nft -f
/etc/edgeguard/nftables.d/ruleset.nft'. configgen.ReloadService
nutzt jetzt sudo (haproxy reload klappte vorher nicht aus dem
edgeguard-User).
Frontend (Sweep): style={{ marginBottom: 16 }} → className="mb-16"
in allen 7 Firewall-Tabs — User-Feedback "globales CSS statt inline".
Live auf 89.163.205.6: nft list table inet edgeguard zeigt
Anti-Lockout + Baseline + Cluster-Peer-Set + (jetzt noch leere)
Custom-Rules-Sektion. render-config postinst-mäßig sauber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
* internal/services/tlscerts/ — Repo (List/Get/Upsert/Delete/
GetByDomain/ListExpiringSoon/MarkError) gegen tls_certs-Tabelle.
* internal/services/certstore/ — WriteCombined verifiziert cert/key
match via tls.X509KeyPair, schreibt /etc/edgeguard/tls/<domain>.pem
(HAProxy-format: cert + chain + key konkatenert). Parse extrahiert
NotBefore/After/Issuer/SANs aus dem PEM. Domain-Charset-Whitelist
gegen Path-Traversal beim Filename. 4 Tests (happy path, mismatched
key, hostile filename, parse).
* internal/services/acme/ — go-acme/lego v4 mit HTTP-01 über die
bestehende /var/lib/edgeguard/acme-Webroot (HAProxy proxied dort
schon hin). Account-Key persistent in /var/lib/edgeguard/acme-
account/account.key, Registrierung lazy beim ersten Issue().
* internal/handlers/tlscerts.go — REST CRUD + /upload (custom PEM)
+ /issue (LE HTTP-01) auf /api/v1/tls-certs. Reload HAProxy via
sudo nach jeder Mutation. Audit-Log pro Aktion.
Frontend:
* management-ui/src/pages/SSL/ — Tabs (Let's Encrypt / Eigenes
Zertifikat) plus Tabelle aller installierten Zerts mit
expires-in-Anzeige (orange ab <30 Tage, rot wenn abgelaufen) und
Status-Tags. Sidebar-Eintrag, i18n de/en.
* Networks-Form: Parent-Interface ist jetzt ein Select aus den
System-Discovered-Interfaces statt freier Input — User-Wunsch.
Packaging:
* postinst legt /var/lib/edgeguard/acme-account/ 0700 an.
* postinst installt /etc/sudoers.d/edgeguard mit NOPASSWD-Rule für
systemctl reload haproxy.service — damit der edgeguard-User
reloaden kann ohne root.
Live deployed auf 89.163.205.6. /api/v1/tls-certs antwortet jetzt
401 ohne Cookie (Route registriert), POST /tls-certs/upload + /issue
sind bereit. ACME-Issue gegen externe FQDN (utm-1.netcell-it.de)
braucht nur noch die Domain, die im wizard schon angelegt ist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
migrate up|down|check|dump (1:1 nmg-ctl-Pattern, ruft internal/database
Migrate/MigrateDown/ValidateMigrations/CopyEmbeddedMigrationsTo).
initdb prüft pg_roles/pg_database und legt Role + DB idempotent via
sudo -u postgres psql an, mit Identifier-Whitelist gegen Injection.
postinst wirt die drei Schritte vor systemd-enable: migrate check
(Pre-Flight ohne DB), initdb, migrate up (als edgeguard-User via
Socket-Peer-Auth). cluster-join/promote/dump-config bleiben explizit
Phase-3-Stubs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverse-Proxy von Angie (eigenes APT-Repo) auf nginx (Distro) umgestellt
— vereinfacht Bootstrap (kein angie.software-Repo mehr), reduziert
Offene-Punkte (arm64-Verfügbarkeit entfällt). Neuer Service Unbound
übernimmt zwei Rollen: Caching-Forwarder mit DNSSEC und Cluster-internes
Split-Horizon (Local-Zone eg.cluster, Peer-Adressen aus PG ha_nodes,
Reload via unbound-control). Architektur-Spec §7.5 dokumentiert beide
Rollen + Config-Schichtung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>