Commit Graph

54 Commits

Author SHA1 Message Date
Debian
81a8217493 feat(alerts): Health-Alarme via Webhook + Email-SMTP
Sidebar → System → Alarme.

Migration 0021: alert_channels (kind=webhook|email, target, settings,
active) + alert_events (kind, severity=info/warning/error/critical,
subject, message, sent_to JSONB).

internal/services/alerts/:
  - Fire(kind, severity, subject, message) — broadcastet an alle
    aktiven Channels + persistiert Event mit per-Channel-Result
    (ok/error) in sent_to.
  - Webhook-Sender: POST JSON {kind, severity, subject, message,
    content, text, fired_at}. Slack/Discord/Teams akzeptieren das
    out-of-the-box ohne Adapter (content + text-Felder gleichzeitig).
  - Email-Sender: net/smtp + STARTTLS optional. Settings (smtp_host,
    smtp_port, username/password, from, use_tls) liegen in
    channel.settings JSONB.

internal/handlers/alerts.go: CRUD + POST /alerts/test + GET
/alerts/events (history).

Scheduler-Trigger:
  - cert.expiring  — TLS-Cert <14 Tage Restzeit (12h-dedupe pro cert)
                     severity warning, <3 Tage → error
  - cert.renew_failed       — Renewer-Cycle hat fails
  - cert.renewer.run_failed — Renewer-Cycle abgebrochen
  - backup.failed  — Scheduled Backup error
  - license.invalid — License-Server liefert valid=false

In-process Dedupe (12h TTL, map[key]time.Time) verhindert dass
identische Alerts in Schleifen feuern.

UI (pages/Alerts): Tabs Channels (CRUD-Tabelle, Add-Modal mit
conditional-Email-Fields) + History (200 letzte Events mit
severity-Tag + per-Channel-Delivery-Status). Header-Button
„Test-Alert" feuert einen Test-Event in alle aktiven Channels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:57:05 +02:00
Debian
4a34629023 feat(audit): Live-Stream im Dashboard via WebSocket
Recent-Activity-Karte zeigt neue audit_log-Events jetzt sofort statt
in 15s-Polls.

internal/services/audit/audit.go:
  - Repo bekommt Subscribe()-Methode mit fan-out-channel (Buffer 32,
    non-blocking-send — langsame Clients droppen Events statt die
    Pipeline zu blockieren).
  - Log() macht jetzt INSERT … RETURNING id, created_at und broadcastet
    den fertigen Entry an alle Subscribers. Broadcast nur nach
    erfolgreichem INSERT — failed inserts erscheinen nicht.

internal/handlers/audit.go:
  - Neuer GET /api/v1/audit/live (WebSocket): sendet beim Connect die
    letzten 50 Einträge (oldest→newest), danach Live-Stream aus
    Subscribe-Channel. 30s-Ping gegen HAProxy-Tunnel-Timeout.
  - Recent (Poll-Endpoint) bleibt für Fallbacks erhalten.

UI Dashboard:
  - useAuditLive(keep=15)-Hook ersetzt das 15s-useQuery-Poll.
  - WebSocket auf wss://<host>/api/v1/audit/live; Auto-Reconnect alle
    2s nach Drop.
  - dedupe per id (Snapshot + erste live-Events können sich kurz
    überschneiden während des Subscribe-Race).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:39:04 +02:00
Debian
5bdea1bced feat(diagnostics): UI-Tools — ping/traceroute/dig/curl/tcp
Operator-Diagnose ohne SSH. Sidebar → System → Diagnose.

internal/services/diagnostics/:
  - Ping        — ping -c4 -W2 (12s timeout)
  - Traceroute  — traceroute -n -w2 -q1 -m20 (60s timeout)
  - Dig         — dig +timeout=3 +tries=2 <TYPE> <NAME> (Types A/AAAA/
                  CNAME/MX/TXT/NS/SOA/PTR/SRV/CAA whitelisted)
  - Curl        — curl -IsSv --max-time 10 (nur http(s)://, kein
                  file:// / smb:// / data://)
  - TCPProbe    — nc -zv -w5 (8s timeout)

Sicherheit: validTarget() prüft jeden Input gegen Allow-List
[a-zA-Z0-9.:/_-]; verhindert Shell-Metachar-Injection. exec.Command
mit nackten Argument-Slices (kein /bin/sh, kein Glob-Expansion).

internal/handlers/diagnostics.go: POST /api/v1/diagnostics/<tool>
hinter requireAuth.

UI (pages/Diagnostics): 5 Tool-Cards, jede mit eigenem Input + Run-
Button + monospace-Output-Pane (dunkel, scrollbar, max-height 320px).
Pro Tool ein Status-Tag (OK / exit N) + Dauer-ms. info-Alert oben
erklärt dass Tools auf der Box laufen, nicht im Browser; security-
Alert unten erklärt die Restrictions.

control: iputils-ping, traceroute, dnsutils, curl, netcat-openbsd
als Depends. Auf Test-Box bereits da (waren Distro-defaults).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:30:07 +02:00
Debian
e07b484a48 feat(cluster): Config-Hash-Compute für Drift-Detection
Setzt die Foundation aus 1.0.70 fort — bisher war ha_nodes.config_hash
noch NULL und das UI konnte keinen Drift erkennen.

internal/cluster/confighash.go:
  - ComputeConfigHash() berechnet SHA-256 (truncated auf 16 hex chars)
    über alle replizierbaren Tabellen. Pattern 1:1 aus mail-gateway/
    internal/handlers/cluster_status.go (driftHashSpec).
  - Pro Tabelle: md5((to_jsonb(t) - id - updated_at - created_at -
    excludes)::text) per row, dann string_agg ORDER BY rh.
  - Singleton-Tabellen (dns_settings, ntp_settings, mail_config-Stil)
    hashen direkt ohne agg.
  - 23 Tabellen: domains, backends, backend_servers, routing_rules,
    network_interfaces, ip_addresses, tls_certs (mit ExtraExclude
    last_renewed_at + last_error damit cert-renewal keinen drift
    erzeugt), firewall_zones+address_objects+address_groups+services+
    service_groups+rules+nat_rules, wireguard_interfaces+peers,
    forward_proxy_acls, dns_zones+records+settings, ntp_pools+settings,
    static_routes.
  - RefreshLocalHash() schreibt den Hash in die eigene ha_nodes-Row.

Scheduler:
  - 5-min-Tick ruft RefreshLocalHash. Pro-Mutation-Refresh wäre zu
    teuer (jede UI-Action triggert sonst 23 jsonb-Queries).
  - Initial-Refresh beim Scheduler-Boot damit /cluster/status nicht
    5 min auf den ersten Wert wartet.

handlers/cluster.go:
  - Status() ruft RefreshLocalHash mit 2s-Timeout on-demand. Damit
    sieht das UI auch zwischen den Scheduler-Ticks immer frische
    Werte; bei Timeout fallback auf den DB-Wert (eventuell stale).

Verifiziert auf 1.0.71: ha_nodes-Row hat config_hash=728834dce5ca4e48,
scheduler-log "config-hash refresh enabled tick=5m0s".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:33:42 +02:00
Debian
ea7c356455 feat(cluster): Phase 3 Foundation — node.conf + ha_nodes-Drift + UI
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>
2026-05-13 08:27:00 +02:00
Debian
df77b814ff feat(firewall): default-drop logging in input + forward chain
User-Feedback: das Live-Log zeigte nur die Smoke-Test-Snapshots von
gestern weil keine einzige Firewall-Rule den log-Flag hatte. „Das ist
kein Live-Log."

Fix: das nft-Template emittiert jetzt am Ende der input und forward
chain einen `limit rate 10/second log prefix "edgeguard:drop-*" group 0`
direkt vor dem default `policy drop`. Damit fließen ALLE Pakete die
keine Custom-Rule erlaubt hat ins Log — ohne dass der Operator pro
Rule den Log-Switch setzen muss.

limit rate 10/second burst 5: schützt vor Log-Floods durch Port-
Scanner, ohne die normale Visibility zu verlieren. Bei einer typischen
Edge-Box mit 99% Drop auf WAN-Inbound liegt das Volumen so bei ~300
Events/min = 5MB/h gzipped — logrotate keeps 14 days.

Reader: drop-input/drop-forward-Prefix wird NICHT als RuleID gemappt
(es gibt keine zugehörige Rule), Action explizit auf "drop". UI rendert
die mit eigenem Tag "default-input" / "default-fwd" (volcano-Farbe) in
der Rule-Spalte.

Verifiziert auf der Box: 26 echte Drop-Pakete in 5s nach Re-render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:15:23 +02:00
Debian
b031725dfe feat(routes): Static-Routes-Management + Live-View (Networks-Tab)
Migration 0019: static_routes (id, destination, gateway, dev, metric,
table_name, active, comment).

internal/services/staticroutes/:
  - CRUD-Repo
  - Generator schreibt /etc/edgeguard/routes.conf (pipe-format) und
    triggert `sudo systemctl restart edgeguard-routes.service`
  - LiveAll() ruft `ip -j route show table all` und parsed JSON

internal/handlers/routes.go:
  GET /api/v1/routes           — managed (DB)
  POST/PUT/DELETE              — CRUD (re-render + apply on mutate)
  GET /api/v1/routes/live      — kernel-state via ip(8)

postinst:
  - /usr/sbin/edgeguard-apply-routes (root-owned shell-script). Liest
    routes.conf, flusht `proto 250` (= edgeguard), setzt neue Routen
    mit proto 250. Andere Quellen (kernel/dhcp/manuell) bleiben
    unangetastet.
  - /etc/systemd/system/edgeguard-routes.service (Type=oneshot,
    After=network-online.target). Beim Boot automatisch via
    multi-user.target.
  - /etc/iproute2/rt_protos.d/edgeguard.conf — Symbol "edgeguard" =
    250 damit `ip route show proto edgeguard` funktioniert.
    (Debian 13 hat kein /etc/iproute2 default → .d-Pattern statt
    rt_protos-Anhängen.)
  - sudoers: edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl
    restart edgeguard-routes.service

UI: Networks-Page jetzt mit Tabs (Interfaces + Routen). Routes-Tab
hat zwei Cards:
  - Live-Routen (read-only, 30s refresh, `proto edgeguard` farblich
    hervorgehoben)
  - Verwaltete Routen (CRUD-Tabelle, Add/Edit-Modal mit destination/
    gateway/dev/metric/table/active/comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:50:26 +02:00
Debian
dbc14a24a4 feat(backup): Restore-Pfad — POST /backups/:id/restore + UI
backup.Service.Restore(id) schreibt /var/lib/edgeguard/restore.sh
und dispatcht via `sudo systemd-run --unit=edgeguard-restore.service`.
Skript-Ablauf:
  1. tar -xzf der Backup-Datei → /var/lib/edgeguard/restore-tmp
  2. state-files (setup.json/license/jwt/node.conf/acme-account) per
     cp -a zurück, chown edgeguard
  3. systemctl stop edgeguard-api + scheduler (DB-Connections freigeben)
  4. sudo -u postgres psql -f dump.sql (--clean droppt + recreated)
  5. edgeguard-ctl render-config (haproxy/nft/squid/unbound/chrony)
  6. systemctl start edgeguard-api + scheduler
  7. rm -rf restore-tmp + restore.sh

UI: pro Backup-Row neuer Restore-Button mit Popconfirm. Beim Trigger
zeigt sich das vertraute Fullscreen-Overlay (Klassen .update-modal*
re-used) mit 4 Steps (Extract / DB-Restore / Render / Restart) + Live-
Timer. Health-Poll alle 3s detektiert API-Restart + reload. Safety-
Timeout 3 min für große DB-Dumps.

postinst: sudoers für `systemd-run --unit=edgeguard-restore.service
--description=... --collect bash /var/lib/edgeguard/restore.sh` +
zugehöriges `systemctl reset-failed`. Pfad fix damit kein Wildcard
nötig wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:22:55 +02:00
Debian
571f51ba9a feat(backup): pg_dump + state-tarball + daily auto + UI
Production-Box braucht Backups — bisher keine. Jetzt komplette
Pipeline:

Backend (internal/services/backup/):
  - Output: /var/backups/edgeguard/eg-YYYYMMDD-HHMMSS.tar.gz
  - Inhalt: dump.sql (pg_dump --clean --if-exists --no-owner --no-acl),
    files/setup.json, files/license_key, files/license.cache,
    files/.jwt_fingerprint, files/node.conf, files/acme-account/* +
    manifest.json (Version, kind, hostname, sizes)
  - sha256 während-write via TeeWriter, Size + sha in backups-DB-Row
  - Failure-Path: row mit status=failed + error, kein orphan-tarball
  - Prune(keepN=14) löscht erfolgreiche Backups älter als die letzten N

Migration 0018: backups(id, file, size, sha256, db/files bytes, kind,
status, error, host, started/finished).

Scheduler (cmd/edgeguard-scheduler):
  - 24h-Tick → backup.Run(KindScheduled) + Prune. Beim Boot wird ein
    initialer Backup NICHT sofort gezogen (kein nervöses Spam),
    sondern erst beim nächsten 24h-Slot.

REST (internal/handlers/backup.go):
  GET    /api/v1/backups              — list (newest first)
  POST   /api/v1/backups              — trigger manual (sync, audit'ed)
  GET    /api/v1/backups/:id          — single
  GET    /api/v1/backups/:id/download — sendfile tar.gz
  DELETE /api/v1/backups/:id          — entferne file + row

UI (management-ui/src/pages/Backups):
  - Liste mit Time, File+sha (first 16), Kind-Tag, Status, Size (mit
    DB + Files Aufschlüsselung), Dauer
  - „Backup jetzt erstellen" Button, Refresh, Download, Delete
  - Auto-Refresh 30s
  - Sidebar-Eintrag „Backups" unter System

postinst:
  - /var/backups/edgeguard 0750 edgeguard:edgeguard (enthält sensitive
    pg_dump + license_key → NICHT world-readable)
  - sudoers-Whitelist `sudo -u postgres /usr/bin/pg_dump --clean
    --if-exists --no-owner --no-acl edgeguard` — exakte Form

Verifiziert auf der Box: backups-Tabelle existiert, scheduler logged
„backup enabled tick=24h dir=/var/backups/edgeguard keep_n=14",
pg_dump-via-sudoers liefert 2808 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:08:18 +02:00
Debian
827c364335 feat(logs): Phase 4 — zentrales Logsystem /api/v1/logs + /system/logs
Aggregierter Reader für alle EdgeGuard-Service-Journale + audit_log.

internal/services/syslogs/
  - 9 Quellen: edgeguard-api, edgeguard-scheduler, haproxy, squid,
    unbound, chrony, wg-quick@*, ulogd2, audit
  - journalctl --output=json + parser für __REALTIME_TIMESTAMP,
    PRIORITY (0-7 → debug/info/warn/error), MESSAGE, _HOSTNAME
  - audit-Reader nutzt bestehende audit.Repo.ListRecent
  - Concurrent fan-out über alle gewählten Quellen, dann merge-sort
    by Timestamp DESC + cap auf Limit (max 1000)
  - Client-Filter: Level, Grep (case-insensitive über message +
    actor + action + subject)

internal/handlers/logs.go:
  GET /api/v1/logs            — Filter via Query-Params
  GET /api/v1/logs/sources    — statische Quellen-Liste fürs UI

postinst: edgeguard → systemd-journal + adm Gruppen, damit
journalctl ohne sudo lesen kann. Verifiziert auf der Box: id zeigt
`groups=adm,systemd-journal,haproxy,edgeguard`.

UI: management-ui/src/pages/Logs — Multi-Source-Select, Level-Color-
Tags, Time-Range-Picker, Volltext-Suche, Auto-Refresh 5s (Toggle),
CSV-Export. Sidebar-Eintrag "Logs" unter System (FileSearchOutlined).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:29:38 +02:00
Debian
66187e5b77 feat(firewall-log): Phase 3 — UI /firewall-live mit WS-Stream + Filter + CSV
Neue Page management-ui/src/pages/FirewallLive — Live-Tail der NFLOG-
Events aus /api/v1/firewall/log/live (WebSocket). Features:

- Status-Indicator (Live/getrennt), Auto-Reconnect alle 2s nach Drop
- Filter-Bar (action/proto/src/dst/rule_id) — bei Änderung wird der
  WS neu verbunden, Server schickt frischen Snapshot
- Pause-Toggle: während Pause werden Events gebuffert (max 1000),
  beim Resume in die Tabelle gemerged
- CSV-Export der aktuellen Tabelle (timestamp/rule/action/proto/src/
  dst/iface/size)
- Color-coded Action-Tags (ACCEPT=grün, DROP=rot, REJECT=orange)
- Ring-Buffer 1000 im UI damit die DOM-Last bei Hochlast bleibt
- Sidebar-Eintrag "Firewall-Log" unter Sicherheit (Eye-Icon)
- DE/EN i18n

haproxy: backend api_backend bekommt `timeout tunnel 1h` damit der
WebSocket-Stream nicht nach `timeout server 60s` ohne Events stirbt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:14:43 +02:00
Debian
a798d1b796 feat(firewall-log): Phase 2 — HTTP-Tail + WebSocket-Live-Stream
Backend für /firewall-Live-Tail und historische Recherche der
ulogd2-JSONL aus Phase 1.

internal/services/firewalllog/
  reader.go  — JSONL parser + Filter (since/until/rule_id/src/dst/
               proto/action/limit). Proto-Mapping aus IP-Protocol-Number
               (1=icmp, 6=tcp, 17=udp, 58=icmpv6). RuleID wird aus
               oob.prefix "edgeguard:<id>" extrahiert.
  tailer.go  — fsnotify-Watcher auf /var/log/edgeguard/, In-Memory
               Ring-Buffer 1000 Events, fan-out an Subscribe()-Channel.
               Robust gegen logrotate copytruncate (truncate-detection
               via stat.Size() < offset → seek(0)). Safety-Net 2s-poll
               falls fsnotify einen Write verschluckt. Non-blocking send
               an Subscriber — langsame Clients droppen Events statt
               die Pipeline zu blockieren.

internal/handlers/firewall_log.go:
  GET /api/v1/firewall/log     — typed JSON list, Filter via Query
  WS  /api/v1/firewall/log/live — Snapshot + live broadcast
                                  (gorilla/websocket, 30s-ping)

main.go: Tailer beim Startup gestartet (context.Background) — UI
landet in Phase 3.

deps: gorilla/websocket v1.5.3, fsnotify v1.10.1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:05:39 +02:00
Debian
3c817b7080 feat(firewall-log): ulogd2 + NFLOG group 0 → JSON-Lines
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>
2026-05-12 20:44:00 +02:00
Debian
1bb13e8107 fix(haproxy): check-alpn http/1.1 für HTTPS-Backends mit Healthcheck
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>
2026-05-12 13:45:47 +02:00
Debian
2fac8f40dd fix(update): Upgrade-Skript ausserhalb /tmp wegen PrivateTmp
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>
2026-05-11 22:21:59 +02:00
Debian
117d16e597 fix(update): self-upgrade via sudo systemd-run + animiertes Modal
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>
2026-05-11 22:02:54 +02:00
Debian
26f321de9d feat(backends): WebSocket-Toggle pro Backend
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>
2026-05-11 21:51:09 +02:00
Debian
da35097041 fix(haproxy): timeout tunnel 1h für WebSocket/noVNC
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>
2026-05-11 21:31:28 +02:00
Debian
8aac24b566 feat(backends): Pool-Modell — Backend = Pool, N Server pro Backend
Migration 0016: backend_servers (id, backend_id, name, address, port,
weight, backup, active) + backends.lb_algorithm. Daten-Migration kopiert
bestehende backends.address/port als ersten Server, dann DROP COLUMN.

HAProxy-Renderer: rendert pro Backend einen Block mit `balance <algo>`
+ N `server`-Zeilen (weight, backup-Flag, optional check inter 5s).
LB-Algorithmen: roundrobin / leastconn / source.

REST: /backends/:id/servers (GET/POST), /backend-servers/:id (PUT/DELETE).
Re-rendert HAProxy nach jeder Server-Mutation.

UI: address/port aus Backend-Form raus, lb_algorithm-Select rein. Server
verwaltet ein expandable Sub-Panel pro Backend-Row (Tabelle + Add/Edit/
Delete-Modal). Domain-Attachment-Multi-Select bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:55:47 +02:00
Debian
05850934fb fix(update): sudo für apt-get update — Banner blieb sonst auf altem Stand
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>
2026-05-11 14:28:37 +02:00
Debian
62505d547c feat(license): Lizenz-System mit Ed25519-Verify gegen license.netcell-it.com
Portiert mail-gateway/internal/license (Verify, Cache, Trial, Signature)
+ DB-Mirror (internal/services/license) + REST-Handler (status/verify/key/clear)
+ UI-Page /license (Activate, Status, Limits, Features, Re-verify)
+ <LicenseBanner /> neben UpdateBanner (trial-expiring, expired, verify-failed)
+ Scheduler: täglich Re-verify (24h-Tick)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:41:16 +02:00
Debian
9464322450 fix(dashboard): nftables-Status aus Kernel statt Systemd-Unit
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>
2026-05-11 07:51:55 +02:00
Debian
c7b98f196e feat(dashboard): Operations-Dashboard mit Live-Health/Resources/Audit/HAProxy
Vorher: Dashboard war Counts + statische Cards. Jetzt operativer
Überblick — was läuft, was klemmt, was wurde gerade geändert.

Backend (4 neue Endpoints):
* GET /api/v1/system/services — systemctl is-active für 8 services
  (edgeguard-api, scheduler, haproxy, nftables, unbound, chrony,
  squid, postgresql). Inklusive ActiveEnterTimestamp.
* GET /api/v1/system/resources — /proc/loadavg, meminfo, statfs(/),
  nf_conntrack count+max, uptime.
* GET /api/v1/audit/recent?limit=N — letzte audit_log entries.
  audit-Repo bekommt ListRecent + Entry struct.
* GET /api/v1/haproxy/stats — parsed haproxy 'show stat' CSV vom
  /run/haproxy/admin.sock (postinst addet edgeguard zu haproxy-
  group für socket-read; haproxy-group exists nach apt install).

Frontend Dashboard rewrite:
* PageHeader + KPI-Strip (6 tiles, wie zuvor) — bleibt.
* Resources-Strip: Load (1/5/15) + Mem-Progress + Disk-Progress +
  Conntrack-Progress + Uptime.
* Service-Health-Grid: 8 Karten mit StatusDot + state.
* Recent-Activity-Card (audit-log): action-Tag + actor + subject +
  relative time.
* HAProxy-Backends-Card: backend/server + UP/DOWN-Tag + sessions +
  bytes_in/out + last_change_age.
* WireGuard live (handshake-age, traffic) — bleibt aus früherem
  Stand.
* Cluster + Firewall + SSL + Routing Cards — bleiben.
* Polling 10s für services/resources/haproxy, 15s für audit.

Plus: postinst usermod -a -G haproxy edgeguard für admin.sock
read-permission.

Version 1.0.43.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:46:39 +02:00
Debian
cc500139fc fix(unbound): Apex-Records (@/leer) korrekt zur Zone-FQDN expandieren
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>
2026-05-11 07:18:45 +02:00
Debian
e4d83d226e feat: NTP-Server (Chrony) — vollständig
Stub raus, vollständige Implementierung analog Unbound/Squid:

* Migration 0015: ntp_settings (single-row mit listen_addresses,
  allow_acl, serve_clients, makestep, rtcsync) + ntp_pools (kind
  pool|server, address, iburst/prefer, minpoll/maxpoll). Default
  4 deutsche pool.ntp.org-Server seeded.
* Models DNSSettings/NTPPool, services/ntp Repo, handlers/ntp.go
  REST /api/v1/ntp/{settings,pools} mit Auto-Restart nach Mutation.
* internal/chrony/chrony.cfg.tpl + chrony.go: Renderer schreibt
  /etc/chrony/conf.d/edgeguard.conf direkt (analog unbound — distro
  chrony.conf included conf.d automatisch). Listen-bind nur wenn
  serve_clients=true; sonst port 0 (= Client-only).
* main.go: ntpRepo + chronyReloader injiziert.
* render.go: chrony als sechste generator.
* postinst:
  - chrony als hard Depends im control file.
  - Conf-Datei /etc/chrony/conf.d/edgeguard.conf wird als
    edgeguard:edgeguard 0644 angelegt.
  - Sudoers für systemctl reload + restart chrony.
* Auto-FW-Rule-Generator: udp/123 wenn serve_clients=true und
  listen_addresses non-loopback enthält.
* Frontend /ntp: PageHeader + Quellen-Tab + Settings-Tab. Listen-
  Addresses als Multi-Select aus Kernel-IPs (analog DNS).
* Sidebar-Eintrag unter Network.
* i18n DE/EN für ntp.* Block.

chrony.service hat kein 'reload' — Renderer ruft RestartService auf.

Verified: 4 default-pool-server connected (chronyc sources zeigt
sie nach erstem render).

Version 1.0.40.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:58:54 +02:00
Debian
2556a93b34 feat(firewall): Auto-FW-Rule-Generator + UI-Anzeige
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>
2026-05-11 06:47:38 +02:00
Debian
8357d84c7b fix(unbound): restart statt reload + DNS Auto-FW-Rules dokumentiert
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>
2026-05-11 06:32:59 +02:00
Debian
e537d70e04 feat: Unbound DNS-Resolver — vollständig (Renderer + Handler + UI)
Stub raus, vollständig implementiert:

* Migration 0014: dns_settings (single-row) + dns_zones.forward_to.
  Default-Settings sind sinnvoll für die typische LAN-Resolver-Rolle
  (1.1.1.1 + 9.9.9.9 upstream, localnet allow, DNSSEC + qname-min on).
* internal/services/dns: CRUD-Repo für zones, records, settings.
* internal/handlers/dns.go: REST /api/v1/dns/zones, /records, /settings
  mit Auto-Reload nach jeder Mutation.
* internal/unbound/unbound.cfg.tpl + unbound.go: Renderer schreibt
  /etc/unbound/unbound.conf.d/edgeguard.conf direkt (kein Symlink-
  Dance, weil AppArmor unbound nur /etc/unbound erlaubt). Local-zones
  authoritativ aus dns_records; forward-zones per stub-zone; default-
  forwarders catchen alles sonst.
* main.go: dnsRepo + unbound-Reloader injiziert.
* render.go: unbound.New() bekommt Pool.
* postinst:
  - Conf-Datei /etc/unbound/unbound.conf.d/edgeguard.conf wird als
    edgeguard:edgeguard 0644 angelegt damit Renderer schreiben kann.
  - /etc/edgeguard + Service-Subdirs auf 0755 (Squid + Unbound laufen
    NICHT als edgeguard, brauchen Read-Traversal).
  - Sudoers: systemctl reload unbound.service whitelisted.
* Template: chroot:"" (Conf liegt außerhalb /var/lib/unbound default-
  chroot), DNSSEC-Trust-Anchor NICHT setzen (Distro hat schon
  root-auto-trust-anchor-file.conf — sonst doppelter Anchor → start
  failure).
* Frontend /dns: PageHeader + zwei Tabs (Zones + Resolver-Settings).
  Zones-Tab mit Drawer für Records (CRUD pro Zone, A/AAAA/CNAME/TXT/
  MX/SRV/NS/PTR/CAA). Sidebar-Eintrag unter Network.
* i18n DE/EN für dns.* Block.

Verified end-to-end: render → unbound restart → dig @127.0.0.1
example.com → 104.20.23.154 / 172.66.147.243.

Version 1.0.34 (mehrere Iterationen wegen AppArmor + chroot + perms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:24:51 +02:00
Debian
72269f5b7c feat: Squid Forward-Proxy — vollständig (Renderer + Handler + UI)
Stub raus, vollständig implementiert:

* internal/services/forwardproxy: CRUD-Repo gegen forward_proxy_acls
  (priority desc, action allow|deny).
* internal/handlers/forwardproxy.go: REST /api/v1/forward-proxy/acls
  mit Validation (acl_type-Whitelist verhindert Squid-Reload-Crash
  bei Tippfehlern). Auto-Reload nach jeder Mutation.
* internal/squid/squid.cfg.tpl + squid.go: Renderer schreibt
  /etc/edgeguard/squid/squid.conf, atomic + Symlink von
  /etc/squid/squid.conf (Squid liest Distro-Pfad — gleicher
  Pattern-Fix wie wg-quick). cache_dir 100MB, cache_mem 64MB,
  http_port 3128. Default-Policy: nur localnet (10/8, 172.16/12,
  192.168/16) — verhindert Open-Relay, falls Operator keine ACLs
  anlegt.
* main.go: forwardproxy-Repo + squid-Reloader instanziiert + Handler
  registriert.
* render.go: squid.New() bekommt Pool (war () vorher, Stub-Signatur).
* postinst sudoers: edgeguard darf systemctl reload squid.service.
* Frontend /forward-proxy: PageHeader + DataTable + ACL-Modal mit
  acl_type-Dropdown (13 Squid-Vokabular-Typen), action-Select,
  Priority. Sidebar-Eintrag unter Security.
* i18n DE/EN für fwd.* Block + nav.forwardProxy.

Verified end-to-end: ACL-Insert via SQL, render → squid reload →
curl -x http://127.0.0.1:3128 http://example.com/ → 200.

Version 1.0.26.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:27:05 +02:00
Debian
e379162a7f fix(firewall+nat): NAT funktioniert end-to-end + Edge-Sysctl-Profil
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>
2026-05-11 00:10:42 +02:00
Debian
52da8d7c9e feat(haproxy): timeout client/server 30s → 60s
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>
2026-05-10 23:54:19 +02:00
Debian
6290cde45f fix(haproxy): backend.scheme auswerten (https → ssl verify none alpn h2,h1)
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>
2026-05-10 23:50:38 +02:00
Debian
b1eb940d09 fix(firewall+wg): Operator-Rule landete als Comment + wg-quick las falsche Conf
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>
2026-05-10 23:43:39 +02:00
Debian
e8334cd276 feat(scheduler): Auto-Renewal für Let's Encrypt Certs
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>
2026-05-10 22:50:00 +02:00
Debian
0d51b26170 feat(haproxy): Admin-UI auf eigenem Port :3443 (mailgateway-Pattern)
* 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>
2026-05-10 21:37:53 +02:00
Debian
fd294a273e feat(ui): Pages auf neues Design + Dashboard + WG-Live-Status + Routing-Rules-Verstecken
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>
2026-05-10 21:07:38 +02:00
Debian
85904d0c36 feat: WireGuard (server + client + peers + QR) + shared UI components
WireGuard
---------
* Migration 0013: wireguard_interfaces (server|client mode, key envelope-
  encrypted) + wireguard_peers (per-server roster). Drop old empty
  0005-Schema (Option-A peer_type, kein Iface-FK), neuer Aufbau mit
  zwei Tabellen + FK.
* internal/services/secrets: Box mit AES-256-GCM, Master-Key in
  /var/lib/edgeguard/.master_key (lazy-create, 0600). Sealed/Open
  für PrivateKey + PSK.
* internal/services/wireguard: KeyGen (Curve25519 mit clamping),
  PublicFromPrivate (für Import), InterfacesRepo, PeersRepo, Importer
  (parst /etc/wireguard/*.conf, server vs. client heuristisch nach
  ListenPort + Peer-Anzahl).
* internal/wireguard: Renderer schreibt /etc/edgeguard/wireguard/<iface>.conf
  (0600), restartet wg-quick@<iface> via sudo (sudoers im postinst
  erweitert). Idempotent — re-render nur wenn content geändert.
* internal/handlers/wireguard.go: REST CRUD für interfaces+peers,
  /generate-keypair, /peers/:id/config (text/plain wg-quick conf),
  /peers/:id/qr (PNG via go-qrcode). Auto-reload nach Mutation.
* edgeguard-ctl wg-import [--path /etc/wireguard]: liest existierende
  conf-Files in die DB. Idempotent (überspringt vorhandene Iface-Namen).

Shared UI components (proxy-lb-waf design pattern)
--------------------------------------------------
* PageHeader: icon + title + subtitle + extras row, einheitlich oben
  auf jeder Page.
* ActionButtons: Edit + Delete combo mit Popconfirm + Tooltip.
* StatusDot: AntD Badge pattern statt "Yes/No" — schneller scanbar
  in dichten Tabellen.
* DataTable: pageSizeOptions [20,50,100,200] + extraActions-Alias +
  optional renderMobileCard für Card-Liste auf < md Breakpoint.
* enterprise.css: .page-header* + .datatable-toolbar Klassen.

Frontend WireGuard
------------------
* /vpn/wireguard mit zwei Tabs (Server / Client) im neuen Pattern.
* Server-Tab: Modal mit Generate-Keypair-Toggle, Peer-Roster im
  Drawer per Server. Pro Peer: QR-Code-Modal + .conf-Download.
* Client-Tab: Upstream-Card im Modal, full-tunnel-Default
  (0.0.0.0/0,::/0), Keepalive 25.
* i18n DE/EN für wg.* Block + common.* Erweiterung.

Misc
----
* Sidebar: WireGuard unter Security-Sektion.
* Nav-i18n: "Firewall (v2)" → "Firewall".
* Version 1.0.8 → 1.0.11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:51:25 +02:00
Debian
3545b8422b feat(api): Auto-Reload HAProxy bei Domain/Backend/Routing-Mutation
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>
2026-05-10 18:23:18 +02:00
Debian
51ea1fc802 feat: Zonen als first-class Entity + Domain↔Backend-Verknüpfung sichtbar
* 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>
2026-05-10 18:05:27 +02:00
Debian
aa14b6b2be feat: Networks-Members für bridge/bond + System-Rules-Card + Theme-Revert
* 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>
2026-05-10 16:19:07 +02:00
Debian
1b2c0d7411 feat(fw): Renderer-Rewrite + auto-apply + Anti-Lockout
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>
2026-05-10 13:34:06 +02:00
Debian
c9dd0b4cb1 feat(fw): /api/v1/firewall/* CRUD-Handler für alle 6 Entities
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>
2026-05-10 11:38:37 +02:00
Debian
0307dc68bb feat(fw): Models + Repos für Firewall-v2 (6 Entities)
Models (internal/models/):
* FirewallAddressObject (host|network|range|fqdn)
* FirewallAddressGroup mit MemberIDs gorm:"-"-Slice
* FirewallService (proto+ports, builtin-Flag)
* FirewallServiceGroup mit MemberIDs
* FirewallRule (v2-Shape, src/dst nullable refs, exactly-one-of-Validation
  in Handler-Layer)
* FirewallNATRule (kind=dnat|snat|masquerade, alle nullable)

Repos (internal/services/firewall/, ein Paket):
* AddressObjectsRepo, AddressGroupsRepo (mit Members-Junction-Ops)
* ServicesRepo (refused Update/Delete für builtin=TRUE Rows),
  ServiceGroupsRepo
* RulesRepo, NATRulesRepo
Jeweils Standard-CRUD; Group-Repos handhaben Members atomic in einer
TX (Update ersetzt komplette Membership).

Handler + Renderer-Rewrite + Frontend folgen in den nächsten
Commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:40:08 +02:00
Debian
e517783c42 feat(fw): Migration 0010 — Firewall-v2-Schema (Fortigate-Style)
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>
2026-05-09 23:52:01 +02:00
Debian
e096531df2 feat(ssl): TLS-Cert-Verwaltung in der GUI — Let's Encrypt + eigenes PEM
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>
2026-05-09 21:49:14 +02:00
Debian
ca03e69637 feat: Network/IP-Verwaltung + Mailguard-Design-Übernahme
Backend:
* Migration 0009_networks: network_interfaces (ethernet|vlan|bond|
  bridge|wireguard, role wan|lan|dmz|mgmt|cluster, parent + vlan_id
  für VLANs) + ip_addresses (interface_id FK, address+prefix, is_vip
  + vip_priority für Cluster-Failover-VIPs).
* Repos services/networkifs + services/ipaddresses + Models +
  Handler /api/v1/network-interfaces (CRUD + /:id/ip-addresses)
  und /api/v1/ip-addresses (CRUD).
* /api/v1/system/interfaces refactored auf Go-natives net.Interfaces()
  statt `ip -j addr show` shell-out — die systemd-Sandbox blockt
  AF_NETLINK auch für Go's runtime, deswegen edgeguard-api.service
  RestrictAddressFamilies um AF_NETLINK ergänzt. Output-Shape
  bleibt identisch (ifindex, ifname, flags[], mtu, link_type,
  address, addr_info[]) — Frontend muss nicht angepasst werden.

Frontend:
* Networks-Page (/networks): "System-discovered Interfaces"
  read-only Tags-Card oben, deklarierte Interfaces unten als
  Tabelle mit Modal-CRUD; Type-Switch zeigt parent+vlan_id-Felder
  bei type=vlan; Role-Tags farbig (wan blau, lan grün, dmz orange,
  mgmt purple, cluster magenta).
* IPAddresses-Page (/ip-addresses): Tabelle pro Interface, VIP-
  Toggle blendet vip_priority-Eingabe ein. Goldenes VIP-Tag in der
  Liste.
* Sidebar erweitert um Networks + IP-Adressen + section-grouping.

Design 1:1 von mail-gateway/management-ui/ übernommen:
* enterprise.css verbatim (Inter-Font via Google CDN statt local
  woff2), Sidebar 240px dunkler Gradient #0B1426→#101D33→#0D1829,
  branding-accent #1677ff für Active-State, abgerundete Cards mit
  shadow-Token, Header weiß mit subtilem backdrop-filter.
* AntD-Theme-Tokens: colorPrimary #0EA5E9, fontSize 13, fontFamily
  'Inter', controlHeight 34, borderRadius 6.
* Layout-Komponenten neu strukturiert: AppLayout/Sidebar/Header
  matchen mailguard-Klassen-Naming (.app-layout, .main-content,
  .sidebar-section, .sidebar-menu-item.active, .header-left, …).
* Sidebar mit 4 Sektionen (Übersicht / Routing / Netzwerk / System)
  + Logo-Header + Versions-Footer.

Live-deployed auf 89.163.205.6: Networks-Endpoint listet eth0
(89.163.205.6/24, MAC bc:24:11:64:29:e8) + lo, frontend zeigt sie
als System-Tags in der Networks-Page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:08:44 +02:00
Debian
f0589e5628 feat(deploy): (d) end-to-end .deb install on 89.163.205.6 grün
Box (Debian 13 Trixie, amd64): apt install ./edgeguard-{api,ui,meta}.deb
zieht postgresql-17, haproxy 3.0, certbot, openssl, etc. nach.
postinst flow läuft sauber: migrate check → initdb → migrate up
(8 migrations) → render-config → install HAProxy drop-in → restart
haproxy → enable api+scheduler. Self-register in ha_nodes nach
Setup-Wizard funktioniert.

End-to-end smoke gegen 89.163.205.6:
* :80  → 301 Moved Permanently → https://
* :443 → TLS termination (self-signed _default.pem aus postinst)
        → JSON envelope vom api_backend (HTTP/2 + HSTS)
* /    → React index.html aus /usr/share/edgeguard/ui/

Änderungen:
* control: keydb-server von Depends nach Recommends — single-node v1
  installiert ohne KeyDB. Phase-3.1 multi-node bringt es zurück nach
  Depends sobald ein eigenes APT-Repo das Paket bereitstellt.
* postinst: render-config (--no-reload) + HAProxy-Drop-in installen +
  systemctl restart haproxy als zusätzliche Schritte.
* postrm: drop-in auf remove + purge entfernen, daemon-reload, ggf.
  haproxy auf distro-default zurückreloaden.
* deploy/systemd/haproxy-edgeguard.conf: Drop-in lenkt HAProxy-Unit
  auf /etc/edgeguard/haproxy/haproxy.cfg statt /etc/haproxy/haproxy.cfg.
  After=edgeguard-api.service vermeidet 503-Race in den ersten 5s.
* scripts/apt-repo/build-package.sh: shippt Drop-in unter
  /etc/edgeguard/systemd/haproxy-edgeguard.conf in der edgeguard-api.deb.
* haproxy.cfg.tpl: http-request redirect vor use_backend → keine
  HAProxy-Warning beim Parsen mehr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:02:14 +02:00
Debian
cb5691cf3c feat(cluster): (c) Phase-3 MVP — stable node-id + self-register + Cluster-Page
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>
2026-05-09 11:52:54 +02:00
Debian
6525cb1a41 feat(acme): (b) HTTP-01 Webroot-Handler in der API
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>
2026-05-09 11:24:42 +02:00
Debian
914538eed1 feat(configgen): Phase 2 Config-Generator + nginx → HAProxy-only Pivot
Architektur-Pivot: nginx fällt komplett weg. HAProxy 2.8+ übernimmt
TLS-Termination, L7-Routing per Host-Header und LB. ACME-Webroot
und Management-UI werden von edgeguard-api ausgeliefert (Phase 3
implementiert die zugehörigen Handler); HAProxy proxied
/.well-known/acme-challenge/* und Management-FQDN-Traffic an
127.0.0.1:9443. Eine Distro-Abhängigkeit weniger, ein Renderer
weniger, sauberere Trennung.

Renderer (alle mit Embed-Templates + Tests):
* internal/configgen/  — atomic write + systemctl reload helpers
* internal/haproxy/    — :80 + :443, ACME-ACL, Host-Header-Routing,
                         Stats-Frontend, api_backend Fallback
* internal/firewall/   — default-deny input, stateful baseline,
                         SSH-Rate-Limit, :80/:443 accept,
                         Cluster-Peer-Set für mTLS :8443,
                         Custom-Rules aus PG
* internal/{squid,wireguard,unbound}/ — Stubs (ErrNotImplemented)

Orchestrator + CLI:
* internal/services/configorch/  — fester Reihenfolge-Run, Stubs
                                   sind soft-skip statt fatal
* cmd/edgeguard-ctl render-config [--no-reload] [--only=svc1,svc2]

Packaging:
* postinst: /etc/edgeguard/nginx raus, /var/lib/edgeguard/acme rein,
  self-signed _default.pem via openssl req (damit HAProxy startet
  bevor certbot etwas issuet hat)
* control: Depends nginx raus, openssl rein
* edgeguard-ui: dependency auf nginx weg, "Served by edgeguard-api
  gin StaticFS"

Live-Smoke: render-config gegen lokale PG schreibt /etc/edgeguard/
haproxy/haproxy.cfg + nftables.d/ruleset.nft korrekt; CRUD-Test aus
Phase 2 läuft weiter unverändert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:59:52 +02:00