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>
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>
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>
* 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>
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>
internal/handlers/firewall.go: ein FirewallHandler-Struct hält alle
6 Repos + Audit-Ref. Register(authed) mountet 30 Endpoints unter
/api/v1/firewall/{address-objects,address-groups,services,
service-groups,rules,nat-rules}.
Validation:
* Address-Objects: kind=host → ParseIP, network → ParseCIDR,
range → "IP-IP", fqdn → looksLikeFQDN.
* Rules: src/dst max one of (object_id|group_id|cidr); 0 = "any".
service max one of (object|group). CIDR-Werte werden geparsed.
* NAT: kind-spezifische Pflichtfelder. dnat braucht target_addr
+ match_dport_start. snat braucht target_addr. masquerade
verbietet target_addr (Iface-IP gewinnt).
* Services: builtin-Rows können nicht editiert/gelöscht werden
(Repo-Layer enforced).
Audit-Log pro Mutation. NoContent für DELETE.
Wiring in cmd/edgeguard-api/main.go: 6 Repos + ein
NewFirewallHandler(...).Register(authed).
Renderer (nft aus allen Joins) + Frontend folgen in den nächsten
Commits.
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>
internal/handlers/acme.go: GET /.well-known/acme-challenge/:token
serviert Token-Files aus /var/lib/edgeguard/acme/.well-known/
acme-challenge/ (default; override via EDGEGUARD_ACME_WEBROOT).
Validiert Token-Charset gegen RFC 8555 §8.3 (base64url, 1..128
chars) und prüft mit filepath.Abs+HasPrefix gegen path-traversal.
Mounted auf der bare gin Engine vor SetupGate/RequireAuth — ACME
muss unmittelbar nach HAProxy-Start funktionieren, lange bevor
ein Admin Setup abgeschlossen hat.
4 Unit-Tests (valid/missing/dir/invalid-charset). Live-Smoke gegen
/tmp/eg-acme bestanden.
Test gegen 89.163.205.6 mit echtem certbot wird Teil von (d) —
unnötig Let's-Encrypt-Rate-Limits zu verbrennen ohne stehendes
HAProxy-Frontend auf dem Server.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
REST-API mit Response-Envelope (1:1 mail-gateway), HS256-JWT-Signer
(Secret persistent unter /var/lib/edgeguard/.jwt_fingerprint),
Setup-Wizard (Bcrypt-Admin-Passwort in setup.json), Auth-Middleware
(Cookie + Bearer), Setup-Gate. Update-Banner-Endpoints
/system/package-versions + /system/upgrade ab Tag 1 wired (Pattern
aus enconf-management-agent: systemd-run detached, HTTP-Response
geht VOR dem Self-Replace raus).
CRUD-Repos für domains/backends/routing_rules mit pgxpool +
handgeschriebenem SQL (mail-gateway-Pattern, kein GORM zur Laufzeit).
Audit-Log-Schreiber auf jede Mutation, NodeID aus /etc/machine-id.
DB-Pool öffnet best-effort — ohne erreichbare PG bleiben CRUD-Routen
unregistriert, Auth/Setup/System antworten weiter (Dev ohne PG).
End-to-end live-getestet gegen lokale postgres-16: Setup → Login →
POST/PUT/DELETE Backends + Domains + Routing-Rules → audit_log
schreibt 5 Zeilen mit korrektem actor/action/subject. Graceful
degrade ohne DB ebenfalls verifiziert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>