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>
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>
Phase-1 firewall_rules (chain/match_expr raw nft) → Fortigate-Niveau:
* firewall_address_objects (host/network/range/fqdn)
* firewall_address_groups + members junction
* firewall_services (proto+port range, builtin-Flag)
* firewall_service_groups + members junction
* firewall_rules komplett umgebaut: src_zone+addr/group/cidr,
dst_zone+addr/group/cidr, service_object_id ODER service_group_id,
action accept|drop|reject, log-Flag, priority+enabled
* firewall_nat_rules (kind=dnat|snat|masquerade) als separate Tabelle
Zonen kommen aus network_interfaces.role (wan|lan|dmz|mgmt|cluster
+ pseudo-Zone 'any').
Builtin-Inserts: 18 Standard-Services (HTTP/HTTPS/SSH/DNS/SMTP-Familie/
DBs/RDP/WireGuard/Ping) plus 5 Service-Groups (Web, Mail-Submit,
Mail-Receive, DNS, Ping).
Renderer (internal/firewall/firewall.go) lässt firewall_rules-Query
für jetzt aus — Template fällt auf baseline + cluster-peer-set
zurück. Volle Render-Logik mit den neuen Joins kommt mit Task #44.
Models + Repos + Handlers + 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>
scripts/install.sh: full curl-Onliner für Debian 13 trixie analog
mail-gateway/scripts/install.sh — OS+Arch-Detection, Pre-flight-
Tools, GPG-Key (nmg.asc, geteilt mit mail-gateway), APT-Source-Line
trixie main, apt install edgeguard, Service-Smoke + healthz-Probe.
Bestimmungsort: get.netcell-edgeguard.de (Hosting separat).
UI: IP-Adressen-Page bekommt eine "Adressen am Kernel"-Card oben,
analog zur Networks-Page. Listet jede vom Kernel sichtbare IP
(lo + eth0 + …) mit Family-Tag (IPv4/IPv6) — read-only. Verwaltete
Adressen darunter wie zuvor. User-Feedback: "die bestehenden
IP-Adressen werden nicht angezeigt" — adressiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Makefile: ui target prefers bun (with npm fallback) so make deb
end-to-end reproduces a fresh dist/. deb-amd64/deb-arm64 now
depend on ui, otherwise stale (or missing) management-ui/dist
goes into edgeguard-ui.deb.
* deploy/systemd/edgeguard-api.service: stale "angie" reference
in hardening comment removed.
Verified locally: make deb-amd64 produces three packages —
edgeguard-api_<v>_amd64.deb (6.9 MB), edgeguard-ui_<v>_all.deb
(320 KB), edgeguard_<v>_all.deb (4 KB meta). dpkg-deb -c
confirms layout matches packaging spec (binaries in /usr/bin,
units in /etc/systemd/system, UI under /usr/share/edgeguard/ui).
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>
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>
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>
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>
Initialer Schema-Set (8 Migrationen, 13 Tabellen) für EdgeGuard v1:
users + audit_log + system_settings, ha_nodes, backends/domains/
routing_rules/tls_certs, forward_proxy_acls, wireguard_peers,
firewall_rules, dns_zones/dns_records, licenses. Migrations liegen
in internal/database/migrations/ (analog mail-gateway) und werden
per //go:embed ins Binary gepackt — keine separate SQL-Dateien im
.deb. ValidateMigrations + Test schützen vor Duplicate-Versionen
(mail-gateway 2026-05-08-Vorfall). GORM-Models für alle Tabellen,
sensible Felder (password_hash, private_key_enc) sind json:"-".
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>