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>
- <aside> → <nav> als Root-Element
- Section-Label-Div NEBEN dem <ul> (vorher verschachtelt)
- <NavLink>-Callback → <Link> + location.pathname-Vergleich (Pattern
identisch zu netcell-webpanel/management-ui/src/components/Layout/Sidebar.tsx)
- nmg-prefix CSS-Variable `--nmg-sidebar-text` entfernt, Textfarbe
direkt #94A3B8 (enconf-Standard)
Färbung war schon enconf-konform (Gradient #0B1426→#101D33→#0D1829,
Akzent-Stab #1677ff mit color-mix tint), nur Struktur+Markup zogen
nach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L7TOUT-Bug: server-Stmt setzt `alpn h2,http/1.1` → Server handelt h2
aus → `option httpchk` sendet HTTP/1.x → Server antwortet nicht →
HAProxy markiert Backend DOWN → 503 für alle Requests. Fix: explizit
`check-alpn http/1.1` an die Server-Direktive wenn Scheme=https UND
Healthcheck aktiv. HTTP-only-Backends bleiben unverändert.
Bonus 1: Inter-Font lokal in public/fonts/ (DSGVO, Performance, Offline-
Dev) — Pattern 1:1 aus netcell-webpanel. Kein Google-CDN-Roundtrip mehr.
Test: TestRender_HTTPSHealthcheckPinsAlpnHTTP1 stellt sicher dass der
Pin gesetzt wird und HTTP-Backends KEIN check-alpn bekommen.
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>
Migration 0017 fügt backends.websocket BOOL. Wenn aktiv emittiert der
HAProxy-Renderer `timeout tunnel 1h` IM Backend-Block; defaults-Section
hat den Global-Timeout dafür verloren. Backends ohne WS-Workload bleiben
bei strikten HTTP-Timeouts (Connection-Hygiene). Migrations-Heuristik
schaltet vm-pool/proxmox/console/vnc-Namen auto auf true damit Proxmox-
Konsole nach Deploy weiterhin durchläuft.
UI: Switch im Backend-Modal + WS-Tag in der Übersichtstabelle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Proxmox-Console-Spinner-Bug — nach HTTP-Upgrade greift timeout tunnel
statt client/server. Default war fehlend → fiel auf timeout server 60s
zurück und kappte WS-Verbindungen. 1h hält Console-Sessions, SSH-WS-
Tunnel und ähnliche langlebige Streams stabil.
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: Update-Banner war eine Inline-Alert mit "Apply now"-Button —
ein Click → Hintergrund-Apt-Run, kein Feedback ob's durch ist.
Jetzt:
* Banner zeigt nur noch Hint + "Details anzeigen"-Button.
* Modal listet alle Pakete mit Upgrade (installed → available),
prominenter Warnhinweis dass edgeguard-api+scheduler restartet
werden (~2-5s Unterbrechung) — HAProxy/nft/WG/Squid/Unbound/Chrony
bleiben durch.
* "Jetzt anwenden" startet den apt-Run, Modal schaltet auf Apply-
Mode (kein Cancel mehr, Progress-Alert mit aktueller→Ziel-Version).
* Polling von /system/health auf 2s erhöht während Apply. Sobald
health.version == target → success-toast + auto-reload der UI
(1.5s delay damit das neue UI-Bundle gecached werden kann).
Pattern entlehnt von enconf/mail-gateway. Plus i18n-Erweiterung
für update.modalTitle / modalIntro / modalWarn / applyingHint /
applyingDetail / success.
Version 1.0.45.
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>
Vorher: Renderer hat record.Name 1:1 ins local-data übernommen.
Bei Apex-Records (Operator gibt '@' oder leer ein um die Zone selbst
zu adressieren) kam '@.' raus statt der Zone-FQDN — unbound parsed
das als FQDN '@', was funktional tot ist.
Fix: resolveFQDN(recName, zoneName):
'@' / leer → zone + '.'
endet mit . → as-is
endet mit zone-suffix → name + '.'
sonst → name + '.' + zone + '.'
Renderer baut recordView{DNSRecord, FQDN} pro record.
Test: zone proxy.resdom.loc + record name='@' value='10.10.20.1'
$ dig @10.10.20.1 +short proxy.resdom.loc
10.10.20.1
Auch wenn der Operator 'proxy.resdom.loc' als Name eingibt
(absoluter FQDN), 'mailcow' (relativ), oder 'mailcow.proxy.resdom.loc.'
(absolut mit Punkt) — alle drei expandieren korrekt.
Version 1.0.42.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: Wenn der Operator eine NTP/DNS/Squid/WG-Mutation gemacht hat,
wurde nur der Service neu konfiguriert + reloadet — die Auto-FW-
Rules (udp/123, udp/tcp 53, tcp 3128, udp/<wg-port>) blieben aber
auf dem Stand des letzten firewall-renders. Operator musste manuell
'edgeguard-ctl render-config --only=nftables' fahren.
Fix: withFW-Wrapper in main.go der nach jedem Service-Reloader auch
den firewall-Renderer aufruft. Service-Reload-Errors propagieren
weiterhin (Aktion gilt als gescheitert), FW-Render-Errors werden
nur geloggt (DB-Row ist commited, FW kann nachgezogen werden).
Wirkt für: WG, Squid, DNS, NTP. (HAProxy nicht — Domains/Backends
generieren keine Auto-FW-Rules.)
Version 1.0.41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renderer berechnet inbound-accept-Rules aus dem laufenden
Service-State — Operator legt keine FW-Rule mehr für DNS/Squid/WG-
Listen-Sockets manuell an.
internal/firewall:
* View.AutoRules + AutoFWRule struct (proto, port, optional dst-IP,
comment).
* loadAutoRules quert cross-service:
- DNS: dns_settings.listen_addresses ohne 127.x/::1 → udp+tcp 53
pro IP (mit ip daddr X-match).
- Squid: count(active forward_proxy_acls) > 0 → tcp 3128 (any IP,
squid bindet 0.0.0.0).
- WireGuard: server-mode + listen_port → udp <port> pro Iface.
* nft-Template emittiert eigene "Service-Auto-Rules"-Section vor
Operator-Rules. Comment im nft-Output zeigt source-service.
* LoadAutoRules exportiert für Handler-Endpoint.
Handler:
* GET /api/v1/firewall/auto-rules — gibt die berechnete Liste
zurück damit die UI sie anzeigen kann.
* FirewallHandler.Pool field + ctor-arg dazugekommen.
UI:
* SystemRulesCard fetcht /firewall/auto-rules + merged sie unter
die statischen Anti-Lockout-Rows. 30s-Polling. Operator sieht
jetzt im /firewall/Rules-Tab oben warum z.B. udp/53 offen ist
(auto: DNS auf 10.10.20.1).
Cleanup: alte manuelle DNS+WG-Rules per SQL gelöscht — Auto-Rules
übernehmen.
Version 1.0.38.
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>
Vorher: Free-Text-Input ('127.0.0.1, ::1, 10.10.20.3') — Operator
musste Werte tippen + auf Format aufpassen.
Jetzt: Multi-Select (mode='tags') das die IPs aus /system/interfaces
+ vier Spezial-Werte (0.0.0.0, ::, 127.0.0.1, ::1) anbietet. Optionen
zeigen IP + Iface-Name + Family ('10.0.20.26 — ens19 (IPv4)'). Tag-
Mode lässt zusätzlich freie Eingabe zu, falls eine geplante VIP noch
nicht im Kernel ist.
Convertierung Form↔Wire: UI Array ↔ DB Comma-CSV.
Version 1.0.35.
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>
KVS / management-center brauchen länger als 30s für TOTP-Validierung
(externe Session-Storage). Erhöht beide auf 60s — sicher noch unter
keepalive-grenzen, deckt aber legacy-Auth-flows ab.
Plus: VERSION bump 1.0.20 (asg{1,2}-cleanup direkt am DB-State,
keine code-Änderung dafür).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: backends.scheme war im Datenmodell + UI vorhanden, aber der
HAProxy-Renderer hat das Feld komplett ignoriert. Jeder Backend
wurde als plain HTTP angesprochen — wenn das Upstream (nginx etc.)
HTTPS erwartet, kam '400 The plain HTTP request was sent to HTTPS
port' zurück, was im Browser als 404/Fehler erschien.
Fix im Template: server-Zeile bekommt 'ssl verify none alpn h2,http/1.1'
wenn .Scheme == "https". 'verify none' weil interne Backends meist
self-signed; ALPN deckt H1 + H2 via Aushandlung ab (also funktioniert
sowohl proxy_protocol=https als auch =h2 aus dem alten EdgeGuard).
Version 1.0.19.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zwei show-stopper beim Cutover .101 → .6 entdeckt + behoben:
1. nft-Template-Bug: {{- if ...}}-Whitespace-Trimmer nach der
'# rule N' Kommentarzeile schluckte den Newline → die ganze
Operator-Rule landete als Teil des # Kommentars. nft akzeptierte
die Datei (legaler Comment) und der Operator sah keine Wirkung.
Fix: Body auf eigener Zeile via {{""}}-Padding, Trimmer raus.
2. wg-Renderer schrieb /etc/edgeguard/wireguard/<iface>.conf, aber
wg-quick@<iface>.service liest /etc/wireguard/<iface>.conf
(Distro-Default). Die zwei Files driftet auseinander — beim
Restart sah wg-quick die alte AllowedIPs. Fix: Renderer legt
einen Symlink /etc/wireguard/<iface>.conf → /etc/edgeguard/...
beim Render an (idempotent, ersetzt vorhandene Real-Files).
Beide Fixes waren voraussetzung für den .101 → .6 Cutover, der
jetzt sauber läuft: VIP .100 lebt auf .6, Unify Home dial't durch
zu wg7 (handshake), 10.0.10.x via wg7-Tunnel reachable.
Version 1.0.18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: edgeguard-scheduler war 60s-sleep-Stub. LE-Certs liefen nach
90 Tagen ab und mussten manuell re-issued werden.
Jetzt:
* internal/services/certrenewer — Pipeline (find expiring → ACME-Issue
→ certstore.WriteCombined → Repo.Upsert → haproxy reload). Kapselt
was der /tls-certs/issue-Handler macht, nur DB-driven für N Certs.
* edgeguard-scheduler nutzt acme.Service + tlscerts.Repo + certrenewer.
Tick alle 6h, Threshold 30 Tage Restlaufzeit. Sofort-Run bei
Startup damit eine frisch eingespielte Box auch ohne 6h-Wartezeit
prüft.
* Issuer == "letsencrypt" als Filter — manuell hochgeladene PEMs
bleiben unangetastet (Operator owns lifecycle).
* Errors landen in tls_certs.last_error, retry beim nächsten Tick
(transiente ACME-Failures self-heal).
* systemd-Unit edgeguard-scheduler.service: ReadWritePaths um
/etc/edgeguard erweitert (für Cert-PEM-Writes), NoNewPrivileges
auf false (sudo systemctl reload haproxy braucht setuid). Spiegelt
edgeguard-api-Unit.
Version 1.0.16.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: SSL-Issue-Form bot nur die operator-managed /domains an.
Wenn der Operator ein Cert für die Management-FQDN (utm-1.netcell-it.de
aus setup.json) wollte, war diese nicht in der Auswahl — er hätte
sie erst als Domain-Row anlegen müssen.
Jetzt: AutoComplete (statt Select) mit drei Quellen kombiniert:
* Management-FQDN aus /setup/status — als erste Option mit Hint
* Alle Cluster-Node-FQDNs aus /cluster/nodes
* Operator-/domains
Plus: jede beliebige FQDN ist eintippbar (DNS muss zeigen).
(combobox-mode in AntD ist deprecated — AutoComplete ist die
empfohlene Variante für free-text-with-suggestions.)
Version 1.0.15.
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>
* HAProxy neues Frontend mgmt_https :3443 → api_backend (Mgmt-UI).
Selbe TLS-Cert-Strecke wie :443 (gleicher /etc/edgeguard/tls/-Pool).
* :443 verliert default_backend → unbekannte Hosts kriegen 503,
nicht mehr versehentlich die Admin-UI. Plus default-Route auf
primary_backend pro Domain (catch-all-Routing dort, wo gewollt).
* Anti-Lockout in nft-Template um tcp dport 3443 erweitert
(zusätzlich zu 22 + 443).
* SystemRulesCard zeigt 3443 als 3. Anti-Lockout-Eintrag.
Erreichbarkeit:
* Public Backends: https://<domain>:443 (mit eigenem Cert oder LE)
* Admin-UI: https://<host>:3443 (jeder Hostname, default_backend)
* SSH: :22 (rate-limited 10/min)
Version 1.0.13.
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>
Symmetrisch zur Firewall: Domains-, Backends- und RoutingRules-Handler
bekommen einen Reloader-Hook injiziert, der nach jeder Mutation
haproxy.cfg neu rendert + sudo systemctl reload haproxy fährt. Errors
werden nur geloggt, nicht failed (Row ist committed; manuelle
Re-Render via edgeguard-ctl render-config bleibt möglich).
Vorher: nur Firewall-Regeln waren auto-applied — Domain/Backend-
Änderungen sind in der DB gelandet, aber das laufende haproxy hat
sie nicht gesehen bis zum nächsten render-config oder API-Restart.
Version 1.0.8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetrisch zur Backend-Auswahl im Domain-Modal: das Backend-Modal
hat jetzt einen Multi-Select für Domains. Auswahl wird beim Speichern
gegen den aktuellen Stand diff't und in N parallele PUTs an
/domains/:id übersetzt — Add setzt primary_backend_id auf die ID,
Remove auf null.
Domain bleibt die Quelle der Wahrheit (kein Schema-Change). Die
Backend-Seite ist nur eine alternative Edit-Affordance.
Version 1.0.7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Migration 0012: firewall_zones (id, name UNIQUE, description, builtin),
Seed wan/lan/dmz/mgmt/cluster als builtin. CHECK-Constraints auf
network_interfaces.role + firewall_rules.{src,dst}_zone +
firewall_nat_rules.{in,out}_zone gedroppt — Validation lebt jetzt
app-side (Handler prüft Existenz in firewall_zones).
* Backend: firewall.ZonesRepo (CRUD + Exists + References-Lookup),
/api/v1/firewall/zones, builtin geschützt (Name nicht änderbar,
Delete blockiert), Rename eines Custom-Zone aktuell ohne Cascade
(Handler-Sorge bei Rules/NAT/Networks).
* Handler-Validation in CreateRule/UpdateRule/CreateNAT/UpdateNAT +
NetworksHandler: Zone-Existence-Check pro Mutation, 400 bei Tippfehler.
* Frontend: Firewall-Tab "Zonen" (CRUD mit builtin-Schutz). Networks-
Form lädt Rollen aus /firewall/zones (statt hardcoded Liste); Rules-
und NAT-Forms ziehen die Zone-Auswahl ebenfalls aus der API.
* Domain-Form bekommt Primary-Backend-Picker (Field war im Modell,
fehlte im UI). Backends-Tabelle zeigt umgekehrt welche Domains
darauf zeigen — bidirektionale Sicht ohne Schemaänderung.
* HAProxy-Renderer: safeID-FuncMap escaped Server-Namen mit Whitespace
("Control Master 1" → "Control_Master_1"). Vorher ist haproxy beim
Reload an Spaces im Backend-Namen kaputt gegangen.
* Version 1.0.3 → 1.0.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Migration 0011: members JSONB für network_interfaces. Bridge/bond
brauchen ≥1 Member (NOT VALID-Constraint, schont bestehende Rows).
vlan/wireguard/ethernet ignorieren das Feld.
* Backend-Validation pro Typ: vlan→parent+vlan_id, bridge/bond→members,
ethernet/wireguard→keins. Repo serialisiert via JSONB.
* Form Networks: Members-Multi-Select für bridge/bond, Composition-
Spalte zeigt vlan-tag bzw. Member-Liste.
* Firewall-Rules-Tab zeigt jetzt SystemRulesCard ganz oben — Anti-
Lockout (SSH/443), stateful baseline, default-deny-Erklärung.
* Theme-Tokens 1:1 mail-gateway: fontSize 13, controlHeight 34
(vorher zu dichtes 12/28). Density kommt vom DataTable size="small".
* Makefile publish-amd64 lädt jetzt auch edgeguard-ui_*_all.deb und
edgeguard_*_all.deb hoch (vorher nur api).
* Version 1.0.0 → 1.0.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DataTable (components/DataTable.tsx) gibt jeder CRUD-Tabelle drei
Baseline-Features auf einmal:
* Search-Input (Volltext über alle string-Felder, case-insensitive)
* Pagination 25/Seite mit showSizeChanger
* Auto-sorter pro Spalte mit dataIndex (string→localeCompare,
number→subtract, boolean→bool→Number) — Spalten mit eigenem
sorter behalten den.
Sweep aller 13 CRUD-Pages auf <DataTable>: Domains, Backends,
Routing-Rules, Networks, IP-Addresses, SSL, Cluster, sechs Firewall-
Tabs. Kleine Sub-Tabellen (System-Discovered IP-Card) bleiben
auf <Table> — read-only ohne CRUD braucht keine Pagination.
i18n: common.search, common.totalRows.
Version-Bump auf 1.0.0 (User-Direktive: ohne -dev): VERSION-Datei,
Go-Literale in cmd/edgeguard-{api,ctl,scheduler}/main.go,
package.json, Sidebar-Konstante. Live deployed auf 89.163.205.6 als
edgeguard 1.0.0 (api + ui + meta).
Memory: project_versioning.md hält die Patch-Bump-Konvention fest
(Gitea Package Registry 409't bei Doppel-Upload — bei jedem Release
zuerst die VERSION inkrementieren).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
management-ui/src/pages/Firewall/:
* index.tsx — AntD Tabs default=Rules
* AddressObjects.tsx — Table + Modal (kind-Switch ändert Placeholder)
* AddressGroups.tsx — Members als Multi-Select aus Address-Objects
* Services.tsx — Builtin-Rows sind Edit/Delete-disabled mit Tooltip,
Form blendet Port-Felder bei proto != tcp/udp aus
* ServiceGroups.tsx — analog AddressGroups
* Rules.tsx — Renderer mit object/group/cidr/any-Switch pro Seite
+ Service-Picker; Action+Zone als Tags in der Tabelle
* NATRules.tsx — kind-spezifische Form (DNAT braucht in_zone+dport,
SNAT/MASQ braucht out_zone, MASQ verbietet target_addr)
Sidebar bekommt eigene Sektion "Sicherheit" mit FireOutlined-Icon
für /firewall. i18n de/en für alle 6 Tabs + Form-Labels.
Backend war schon im vorigen Commit fertig — diese Pages konsumieren
direkt /api/v1/firewall/{address-objects,address-groups,services,
service-groups,rules,nat-rules}. Renderer (nft aus den Joins) +
auto-apply folgen in den nächsten Commits — bis dahin sind die Rules
in der DB sichtbar aber noch nicht aktiv im Kernel.
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>
Minimal-Slice für Phase-3-Cluster:
* internal/cluster/node_id.go — stable UUID 'n-<16hex>' in
/var/lib/edgeguard/node-id, idempotent über reboots.
* internal/cluster/store.go — ha_nodes-Repo (List/Get/UpsertSelf)
via pgxpool. EnsureSelfRegistered upsertet die lokale Row beim
Boot mit FQDN aus setup.json.
* internal/handlers/cluster.go — GET /api/v1/cluster/nodes liefert
alle ha_nodes plus local_id (für UI-Highlighting).
* main.go: nach DB-Pool-Open wird EnsureSelfRegistered (nur wenn
setup.completed) ausgeführt, ClusterHandler registriert.
* management-ui/src/pages/Cluster/index.tsx — Tabelle mit Node-ID,
FQDN, Rolle, Beitrittszeit; eigene Node mit "diese Node"-Tag
markiert. Sidebar-Eintrag + i18n de/en.
Bewusst NICHT in dieser Runde: cluster-init/cluster-join CLIs, KeyDB
Active-Active config-gen, PG streaming replication, mTLS zwischen
Peers, License-Leader-Election. Diese kommen mit dem ersten echten
Multi-Node-Test (Phase 3.1) — sonst Code ohne Smoke-Möglichkeit.
End-to-end-Smoke: setup → restart → ha_nodes hat 1 Row mit
fqdn=eg.example.com, /cluster/nodes liefert sie korrekt mit
local_id-Markierung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CRUD-Pages analog Domains:
* Backends: AntD Table + Modal-Form mit name, scheme, address, port,
health_check_path, active. TanStack-Query gegen /api/v1/backends.
* RoutingRules: Table mit Domain-Name- und Backend-Label-Resolution,
Modal mit Select-Pickern für Domain + Backend, Path-Prefix,
Priority, Active. Drei parallele Queries (rules, domains, backends)
liefern die Listen.
* Settings: read-only Descriptions-Cards mit /system/health und
/setup/status. Editable Werte folgen später.
Sidebar erweitert um Backends, Routing-Rules, Settings (mit AntD-
Icons). i18n de/en für alle drei neuen Seiten.
bun run build + npx tsc -b strict (0 errors). Live-Smoke gegen API:
SPA-Routes /backends, /routing-rules, /settings antworten 200 mit
index.html (NoRoute fallback wirkt).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scaffold und Core-Infrastruktur 1:1 nach enconf-Pattern (netcell-
webpanel/management-ui), reduziert auf EdgeGuard-Scope (kein reseller/
customer-Roles, keine codemirror/extensions). Stack: React 19 + AntD 6
+ TS strict + Vite + TanStack-Query + zustand + react-i18next.
Layout: AppLayout (Sider+Header+Content), Sidebar (Dashboard/Domains),
Header (User-Dropdown + Logout). i18n mit de/en common.json.
Pages: Login (POST /auth/login), Setup-Wizard (POST /setup/complete),
Dashboard (Health-Polling + Statistics), Domains (volles CRUD via
TanStack-Query gegen /domains-API). UpdateBanner-Komponente
(/system/package-versions, alle 5 min poll, /system/upgrade trigger)
ist von Tag 1 wie vom User gefordert eingebaut.
API-Wiring: cmd/edgeguard-api/main.go mountUI() — gin StaticFS für
/usr/share/edgeguard/ui/ (overridebar via EDGEGUARD_UI_DIR), echte
Files werden direkt geserved, alle nicht-API-Pfade fallen via
NoRoute auf index.html für React-Router-SPA. Wenn dist/ fehlt:
HTML-Placeholder mit Build-Hinweis.
Verifiziert: bun install + npx tsc -b strict (0 errors) + bun run
build (12 chunks). End-to-end gegen /tmp/eg-api: / serviert echte
React-index.html, /domains SPA-Fallback, /api/v1/* JSON, /assets/*
direkt, /api/v1/nonexistent korrekt 404.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>