User-Frage: „Werden via haproxy die echten IPs durchgereicht?". Antwort:
X-Forwarded-For ja (option forwardfor), aber Apps wie WordPress/Mailcow
brauchen zusätzlich X-Forwarded-Proto=https um Redirect-Loops zu
vermeiden, und X-Real-IP ist die bequeme single-value-Variante die viele
Tools out-of-the-box lesen (ohne die XFF-Chain parsen zu müssen).
Beide Frontends (public_https + mgmt_https) emittieren jetzt:
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
Was Backends sehen:
X-Forwarded-For: <client-ip> (defaults: option forwardfor)
X-Forwarded-Proto: https (NEW)
X-Real-IP: <client-ip> (NEW, single value)
PROXY-Protocol-Toggle pro Backend kommt nicht in diesem Release — der
Operator hat „nur Header-Variante" gewählt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Peers als „offline"
Bug-Report: Unify-Home-Tunnel ist aktiv (handshake live, 2.8 GB rx),
UI zeigte „Letzter Handshake: nie / Traffic: — / Offline".
Root cause: handlers/wireguard.go Status() parsed `wg show all dump`
mit `PeerPublicKey: fields[2]` — fields[2] ist aber der pre-shared
key (PSK), NICHT der peer_pubkey. Format mit "all"-prefix ist:
iface lines (5 cols): iface priv_key pubkey listen_port fwmark
peer lines (9 cols): iface peer_pubkey psk endpoint allowed_ips
last_hs rx tx keepalive
Damit matched die UI gegen den PSK → DB-Peer-Row (gespeichert per
pubkey) traf nie zu, Status blieb „nie/—" auch bei aktivem Tunnel.
Fix: fields[1] statt fields[2]. Endpoint/AllowedIPs/Handshake/RX/TX
waren bereits korrekt verschoben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator hat Admin-Passwort vergessen aber SSH-Zugang zur Box →
schneller Reset ohne SMTP/Email-Setup.
Flow:
1. `sudo edgeguard-ctl reset-password` auf der Box → 32-hex-Token
+ ISO-Expiry werden nach /var/lib/edgeguard/.reset-token (mode
0600 edgeguard:edgeguard) geschrieben, Token kommt auf stdout.
TTL: 30 min.
2. Login-Seite hat „Passwort vergessen?"-Link → /reset-password.
3. Reset-Page: Token + neues Passwort (min. 12). POST /auth/reset-
password validiert Token (constant-time compare), prüft Expiry,
löscht das File (single-use), hash't das Passwort + speichert
in setup.json.
internal/services/setup/:
- SetAdminPassword() — bcrypt-hash + save, fehler wenn setup nicht
completed
- GenerateResetToken() / ConsumeResetToken() — File-basiert,
Format: "<token>|<RFC3339-expiry>"
internal/handlers/auth.go: POST /api/v1/auth/reset-password.
cmd/edgeguard-ctl/main.go: `reset-password` command.
UI: /reset-password Page mit Info-Alert für CLI-Snippet
(„sudo edgeguard-ctl reset-password" im dunklen Code-Block); Login-
Seite bekommt den „Passwort vergessen?"-Link.
Verifiziert auf 1.0.76: CLI druckt Token + schreibt File mit 0600
edgeguard:edgeguard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schutz gegen Box-Total-Loss — lokale Backups in /var/backups/edgeguard
helfen nicht, wenn die Disk stirbt oder die Box brennt. Nach jedem
erfolgreichen lokalen Backup wird die tar.gz an alle aktiven
Off-Site-Ziele hochgeladen.
Migration 0022: backup_remotes (kind=s3|sftp, target_url, settings
JSONB, active, last_upload_at, last_error) + backups.remote_uploads
JSONB (per-Target-Result).
internal/services/backup/remote/:
- UploadAll() — pro aktivem Target ein Upload, Failures non-fatal
- S3 via minio-go/v7 — funktioniert mit AWS, MinIO, Backblaze B2,
Cloudflare R2, Hetzner Object Storage (alle S3-API-kompatibel)
- SFTP via golang.org/x/crypto/ssh + pkg/sftp. Password + Private-
Key (OpenSSH, base64-encoded) als Auth. Optional host_key_
fingerprint-Pinning (SHA256:...); leer = TOFU (unsicher vs MitM,
OK für initial setup).
- Test() lädt eine 1KB-Probe + löscht sie wieder — Operator-UI hat
einen „Verbindung testen"-Button.
backup.Service.RemoteUploader-Interface: nach erfolgreichem
recordSuccess() läuft UploadAll, Results landen in backups.remote_
uploads JSONB. last_upload_at/last_error in backup_remotes pro Target
gepflegt. API + Scheduler injizieren beide den Adapter.
internal/handlers/backup_remotes.go: CRUD + POST /:id/test. Sensitive
Felder (secret_key, password, private_key) werden in GET-Responses
durch ***SET*** maskiert; UpdateChannel merged das zurück damit der
Operator bei Edit ohne Re-Eingabe speichern kann.
UI: Backups-Page jetzt mit Tabs "Sicherungen" + "Off-Site-Ziele".
Tab 2 hat CRUD-Tabelle mit kind-konditionalem Form (S3-Felder oder
SFTP-Felder), Test-Button pro Row, last_upload-Status mit FAIL-Tag
bei Errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
User-Feedback: Tab fühlt sich falsch an, will eine eigene Page mit
URL-Pfad unter /firewall.
UI:
- pages/Firewall/LiveLog.tsx → pages/FirewallLive/index.tsx
- FirewallPage entfernt den live-Tab aus tabs[]
- App.tsx routet /firewall/live → FirewallLivePage
- Sidebar: neuer Eintrag „Firewall-Log" eingerückt direkt unter
„Firewall" in der Security-Section (child: true Flag → CSS-Klasse
sidebar-menu-item--child mit padding-left 28px + dünnem vertikalem
Trenn-Stab links). Sibling-Active-Logik exklusiv: /firewall matched
NICHT mehr wenn /firewall/live aktiv ist.
- AppLayout PAGE_TITLES bekommt /firewall/live VOR /firewall damit
der Title-Lookup den spezifischeren Pfad zuerst trifft.
Keine Backend-Änderungen.
Bekanntes Verhalten zu erklären: Im Live-Log sehen User aktuell nur
Smoke-Test-Events (oob.prefix=edgeguard:smoke / edgeguard:42, src/dst
127.0.0.1) — das sind die manuell-injizierten nft-Rules vom End-to-
End-Test der Pipeline. Reale Pakete fließen erst durch, wenn der
Operator auf einer firewall_rule den Log-Switch aktiviert (Firewall
→ Regeln → bearbeiten → Logging an). Aktuell hat keine einzige Rule
log=true.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI-Restruktur nach User-Feedback:
- Sidebar-Eintrag „Firewall-Live" entfernt — gehört thematisch unter
Firewall, kein Top-Level-Item. Standalone-Page /firewall-live raus.
- Neuer Firewall-Tab „Live-Log" zwischen NAT und Zonen.
- Default = AUS: zeigt Empty-State mit Start-Button. WebSocket
verbindet erst nach Klick. Stop-Button schließt explizit.
- Filter-Inputs (src/dst/rule_id) jetzt 300ms debounced — vorher
triggerte jeder Tastendruck einen WS-Reconnect.
Server-Pipeline „wirklich live" gepinnt:
- ulogd.conf NFLOG-Plugin bekommt qthreshold=1 + qtimeout=1. Default
des Kernels batched Pakete bis 1024 oder 1s; mit 1/1 fließt jedes
Paket sofort. Critical für die Wahrnehmung „live" statt „bursty".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>