Bisher war der zweite Node nach dem Join zwar im Cluster registriert und in der UI sichtbar, replizierte aber keine einzige geteilte Tabelle — dafuer musste jemand manuell `edgeguard-ctl cluster-setup-standby` ausfuehren. Wer das uebersah, merkte es erst beim Failover: der neue Primary stand ohne Domains, Backends, Firewall-Regeln und WireGuard-Keys da. Das ist jetzt Teil des Join-Vorgangs. Beide Seiten muessen dafuer vorbereitet sein: 1) Primary, beim Erzeugen des Join-Tokens: ein frisch installierter Single-Node hat weder Replikations-Rolle noch PUBLICATION noch wal_level=logical. Ohne das liefe das spaetere CREATE SUBSCRIPTION in ein 404. Der Token wird deshalb erst ausgegeben, nachdem die Publisher-Seite steht — inklusive des einmaligen PG-Restarts (wal_level ist ein postmaster-Parameter), der bewusst hier passiert, solange der Admin danebensteht und noch kein Peer Traffic erwartet. WICHTIG dabei: setupReplicationPrimary rotiert bei jedem Lauf das Replikations-Passwort (ALTER ROLE … PASSWORD). Auf einem Cluster mit bereits angebundenem Subscriber wuerde ein zweiter Token-Klick dessen Connection-String ungueltig machen und die Replikation still anhalten. Deshalb laeuft die Initialisierung nur, wenn PUBLICATION und Secret nicht bereits existieren. 2) Neuer Node, nach erfolgreichem Join: cluster-setup-standby laeuft detached (die Initialkopie dauert je nach Datenmenge Minuten), der Wizard pollt GET /setup/replication-status und zeigt running/done/ failed an. Schlaegt es fehl, steht das manuelle Kommando inkl. Primary-Host direkt daneben statt nur einer Fehlermeldung. Beides braucht root (psql als postgres, pg_hba, PG-Restart), die API laeuft als unprivilegierter edgeguard → Aufruf via sudo mit gepinnten Regeln. Das einzige variable Argument (Primary-Host) wird vorher gegen Hostname/IP-Syntax geprueft; der Aufruf laeuft ohne Shell. Test dafuer liegt bei. Ausserdem zwei Doku-Korrekturen: architecture.md behauptete, cluster-join richte die Replikation gleich mit ein (tut es nicht, clusterjoin.Join macht nur Cert + Registrierung), und der Hinweistext von cluster-join verwies noch auf "PG-Basebackup + KeyDB, Phase 3.5" — beides laut Doku laengst verworfen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
31 KiB
EdgeGuard — Architektur
Status: in Produktion (v1.2.x) · Entwurf: 2026-05-08 · Cluster/HA-Abschnitte an Ist-Stand angeglichen: 2026-06-06 · Plattform: Debian 13 (Trixie), amd64 + arm64.
⚠️ Lesehinweis: Dieses Dokument war ursprünglich ein Entwurf. Mehrere Cluster/HA-Annahmen wurden anders umgesetzt — maßgeblich sind die mit „Ist-Stand" markierten Stellen (§0, §6–§9). Kurzfassung: Logical Replication statt Streaming, keepalived/VRRP statt Hoster-Floating-IP, KeyDB optional/ungenutzt (Cluster-State in PostgreSQL), kein Write-Proxy.
EdgeGuard ist die native Neufassung des bisherigen Docker-basierten Reverse-Proxy/Loadbalancer/Forward-Proxy/VPN-Stacks. Vorbild für Architektur, Build-System und Cluster-Modell ist mail-gateway (nmg); UI-Pattern und Bootstrap-Onliner stammen aus netcell-webpanel (enconf).
0. Leitplanken (nicht verhandelbar)
- Kein Docker. Alle Dienste nativ unter
systemd, installiert viaapt. Distro-Pakete für Drittsoftware (HAProxy, Squid, WireGuard, Unbound, PostgreSQL, keepalived, chrony, certbot), eigene.deb-Pakete für EdgeGuard-Code (api, ui, ctl). - Plattform: Debian 13 (Trixie), amd64 + arm64. Nur Trixie — die Build-/Publish-Pipeline (
Makefile,scripts/apt-repo/) zielt ausschließlich auftrixie. (Eine frühere Ubuntu-24.04-Matrix war geplant, ist aber nicht implementiert.) - Auslieferung: signierte
.deb-Pakete + Meta-Paket via APT. Bootstrap ist der enconf-analoge curl-Onlinercurl -fsSL https://get.edgeguard.netcell-it.de | sudo bash. - HA nativ als Cluster (Ist-Stand 2026-06): N symmetrische Peers, PostgreSQL Logical Replication (ein Publisher/Primary
edgeguard_shared→ N Subscriber; node-lokale Tabellen ausgenommen) + keepalived/VRRP für den VIP-Failover (HTTP/HTTPS-Ingress) + mTLS-Cluster-Agent (:8443) für Heartbeat/Cert-Sync/Aggregation. KeyDB ist optional (Recommends) und im Code praktisch ungenutzt; ein Write-Proxy existiert nicht (Writes erfolgen am Primary). (Der ursprüngliche Entwurf — KeyDB Active-Active, PG-Streaming-Replication mit transparentem Write-Proxy, Floating-IP-statt-VRRP — wurde so nicht umgesetzt; Details in §6–§9.) - WAF, IDS/IPS, DHCP, RADIUS inzwischen umgesetzt (Stand 2026-06): WAF via Coraza+SPOE, IDS/IPS via CrowdSec, DHCP via Kea, RADIUS via FreeRADIUS. Mail-Funktion bleibt ausgeschlossen — Mail-Gateway ist eigenes Produkt (
nmg). - Migrations:
goose(SQL-Dateien), nicht GORM AutoMigrate.
Nicht-Ziele (weiterhin): kein Network-IDS via Suricata (IDS/IPS läuft über CrowdSec), keine Mail-Verarbeitung, keine Multi-Tenant-GuardZones, keine ISO-Builds (kein EdgeGuardOS-Klon — nur APT). (Historisch waren auch WAF/DHCP/RADIUS/IPS Nicht-Ziele — siehe oben, inzwischen umgesetzt.)
1. Scope — fünf Daten-Services + Control-Plane
| Service | Rolle | Distro-Paket | Config-Quelle |
|---|---|---|---|
| HAProxy | Public-Ingress :80 + :443, TLS-Termination, L7-Routing per Host-Header, LB. Proxied /.well-known/acme-challenge/* und Management-FQDN-Traffic an edgeguard-api auf 127.0.0.1:9443; rest geht an User-Backends aus backends-Tabelle. |
haproxy (Debian/Ubuntu) |
aus PG generiert, systemctl reload haproxy |
| Squid | Forward-Proxy mit ACL/Auth | squid |
aus PG generiert, systemctl reload squid |
| WireGuard | Site-to-Site- + Road-Warrior-VPN | wireguard-tools (Kernel-Modul ab Kernel 5.6) |
aus PG generiert, wg syncconf |
| Unbound | Caching-Forwarder mit DNSSEC + Cluster-internes Split-Horizon (siehe §7.5) | unbound (Debian/Ubuntu) |
aus PG generiert, unbound-control reload |
| nftables | Firewall (Input + Forward + NAT) | nftables |
aus PG generiert, nft -f /etc/nftables.conf |
Control-Plane:
| Komponente | Rolle |
|---|---|
edgeguard-api |
Go/Gin REST-API, bindet 127.0.0.1:9443, Reads/Writes auf lokaler PG. Geteilte Tabellen werden vom Primary per Logical Replication an Subscriber verteilt; Writes sollen am Primary erfolgen (keine Write-Proxy-Umleitung im Code). |
edgeguard-waf |
Coraza-WAF-Agent (HAProxy SPOE) — Binary im edgeguard-api-Paket, eigene systemd-Unit |
edgeguard-scheduler |
Cron-artige Jobs (ACME-Renewal-Hook, Backup, Health-Aggregation, Stale-Node-Sweep, License-Heartbeat) |
edgeguard-ctl |
CLI für Setup/Wartung (initdb, migrate, cluster-join, promote, cluster-init-replication, cluster-setup-standby, dump-config) |
management-ui |
React 19 + AntD 6 + Vite, statisch unter /usr/share/edgeguard/ui/, von edgeguard-api per gin StaticFS ausgeliefert (HAProxy proxied Management-FQDN dorthin) |
| PostgreSQL 16/17 | Single Source of Truth — Domains, Backends, Routing-Rules, ACLs, Peers, Cluster-State (ha_nodes), Lizenz etc. |
| KeyDB (optional) | Recommends, im Code praktisch ungenutzt — kein Redis-Client in go.mod. Cluster-State/Heartbeat/Locks liegen in PostgreSQL, nicht in KeyDB. |
2. Package-Layout (Repo)
/var/www/edgeguard-native/
├── cmd/ # Go-Binary-Entry-Points
│ ├── edgeguard-api/ # Management-API (HTTP, 127.0.0.1:9443)
│ ├── edgeguard-scheduler/ # Cron-artige Jobs
│ └── edgeguard-ctl/ # CLI für Setup/Wartung
├── internal/
│ ├── database/ # pgxpool + goose-Runner, migrations/ via go:embed
│ │ └── migrations/ # 0001_*.sql … (goose-Format, embedded)
│ ├── models/ # GORM-Models (domain, backend, routing_rule, acl, peer, …)
│ ├── handlers/ # HTTP-Handler (REST)
│ ├── services/ # Business-Logik (config-render, health-check, cluster-sync)
│ ├── haproxy/ # HAProxy-Config-Generator (TLS + Routing + LB)
│ ├── squid/ # Squid-Config-Generator (squid.conf + squid.d/*)
│ ├── wireguard/ # WireGuard-Config-Generator (wg-quick + wg syncconf)
│ ├── unbound/ # Unbound-Config-Generator (Forwarder + Cluster-DNS)
│ ├── firewall/ # nftables-Ruleset-Generator
│ ├── cluster/ # Join/Promote/Peer-Discovery, Heartbeat, Logical-Replication-Setup, confighash
│ ├── keepalived/ # keepalived/VRRP-Config-Generator (VIP-Failover)
│ ├── chrony/ # chrony-Config-Generator (NTP)
│ ├── kea/ # Kea-DHCP4-Config-Generator
│ ├── freeradius/ # FreeRADIUS-Config-Generator (RADIUS)
│ ├── crowdsec/ # CrowdSec-IDS/IPS-Management (managed-wenn-installiert)
│ ├── waf/ # Coraza-WAF-Engine + SPOE-Agent-Logik
│ ├── aggregator/ # Cluster-View-APIs via mTLS (read-only Fan-Out + Trigger-Actions)
│ └── license/ # License-Validation (jeder Node verifiziert eigenständig — KEINE KeyDB-Leader-Election)
├── management-ui/ # React 19 + AntD 6 + Vite (Struktur 1:1 wie netcell-webpanel/management-ui/)
├── packaging/
│ └── debian/
│ ├── edgeguard-api/ # control, postinst, postrm, conffiles, systemd-Units
│ ├── edgeguard-ui/
│ └── edgeguard-meta/ # nur Depends, keine Dateien
├── deploy/
│ ├── systemd/ # *.service, *.target, *.timer
│ ├── haproxy/ # (Templates jetzt embedded neben Renderer)
│ ├── squid/ # squid.conf.tpl
│ ├── unbound/ # unbound.conf.tpl
│ └── nftables/ # ruleset.nft.tpl
├── scripts/
│ ├── apt-repo/ # build-package.sh, publish.sh, setup-repo.sh
│ ├── install.sh # Bootstrap-Onliner
│ └── release.sh # CI Release-Helper
├── docs/
├── Makefile
├── go.mod # module git.netcell-it.de/projekte/edgeguard-native
└── go.sum
Go-Module-Name: git.netcell-it.de/projekte/edgeguard-native
Build-System: Makefile (POSIX-kompatibel). Targets: build, test, lint, deb, clean, install-local, release.
3. Debian-Pakete
Drei Pakete + Meta — analog nmg. Der WAF-Agent edgeguard-waf ist kein eigenes Paket, sondern liegt als zusätzliches Binary im edgeguard-api-Paket (eigene systemd-Unit).
| Paket | Arch | Inhalt | Depends |
|---|---|---|---|
edgeguard-api |
amd64, arm64 | /usr/bin/edgeguard-{api,scheduler,ctl,waf}, Unit-Files, Migrations, Default-Configs |
postgresql-16 | postgresql-17, haproxy (>=2.8), squid, wireguard-tools, unbound, chrony, kea-dhcp4-server, freeradius, nftables, keepalived, certbot, openssl, sudo, adduser, systemd, ca-certificates, ulogd2, ulogd2-json u. a. · Recommends: edgeguard-keydb, apparmor, fail2ban · CrowdSec: managed-wenn-installiert (kein Depends) |
edgeguard-ui |
all | /usr/share/edgeguard/ui/ (statische Build-Artefakte) |
edgeguard-api (= ${binary:Version}) |
edgeguard-meta |
all | keine Dateien, nur Depends |
edgeguard-api, edgeguard-ui |
Pro Release: 1 arch-spezifisches Paket (edgeguard-api) × 1 Dist (trixie) × 2 Arches = 2 .deb + 2 arch-agnostische (edgeguard-ui, edgeguard-meta) = 4 Artefakte je Release. (Build/Publish-Pipeline zielt nur auf trixie.)
KeyDB-Herkunft: KeyDB ist optional (Recommends: edgeguard-keydb), nicht in den offiziellen trixie-Repos. Falls genutzt, aus Source gebaut + im eigenen APT-Repo veröffentlicht. Im aktuellen Code wird KeyDB nicht benötigt — siehe §7.
Build-Werkzeug: direkter dpkg-deb-Build analog WebPanel/EdgeGuardOS-Pattern. Nicht dh_make/debhelper, nicht fpm. Konsistenz mit existierendem Workflow.
postinst (edgeguard-api):
- User
edgeguardanlegen (adduser --system --group --home /var/lib/edgeguard). /etc/edgeguard/,/var/lib/edgeguard/,/var/log/edgeguard/mit0750,chown edgeguard:edgeguard.- Default-Configs nur anlegen wenn nicht vorhanden (
conffilesverhindert Überschreiben). - PostgreSQL:
edgeguard-ctl initdb(idempotent — prüft DB/User). - DB-Migration:
edgeguard-ctl migrate up. systemctl daemon-reload && systemctl enable --now edgeguard-api.service edgeguard-scheduler.service.
postrm (purge): DB + User nur bei purge, niemals bei remove.
4. Verzeichnis-Layout auf Zielsystem
/etc/edgeguard/
├── edgeguard.yaml # Hauptconfig (conffile)
├── api.env # API-Secrets (mode 0600, edgeguard:edgeguard)
├── haproxy/ # haproxy.cfg (von edgeguard-api generiert)
├── squid/ # squid.conf-Fragmente
├── wireguard/ # wg0.conf etc. (generiert)
├── unbound/ # unbound.conf + cluster-zone.conf (generiert)
├── nftables.d/ # Ruleset-Fragmente
└── tls/ # ACME-verwaltete Zertifikate (0750, edgeguard:edgeguard)
/var/lib/edgeguard/
├── state/ # Migrations-Marker, Cluster-Cursor
├── trial.json # Lizenz-Trial-File
├── .jwt_fingerprint # JWT-Secret-Fingerprint (analog enconf — Schutz vor Rotation)
└── backups/ # lokale PG-Dumps (vor Migration)
/var/log/edgeguard/
├── api.log
├── scheduler.log
└── audit.log
/usr/bin/
├── edgeguard-api
├── edgeguard-scheduler
└── edgeguard-ctl
/usr/share/edgeguard/
├── ui/ # statische React-Build-Artefakte
└── templates/ # Config-Templates für squid/wireguard/unbound (haproxy + nftables sind im Binary embedded)
Entspricht FHS — keine Überraschungen für Admins, Lintian-clean.
5. systemd-Units
| Unit | Typ | Depends-on | User | Restart |
|---|---|---|---|---|
edgeguard-api.service |
simple |
Requires=postgresql.service; After=/Wants=keydb-server.service (KeyDB nur weich/optional) |
edgeguard |
on-failure, RestartSec=5 |
edgeguard-waf.service |
simple |
edgeguard-api.service (Coraza SPOE-Agent) |
edgeguard |
on-failure |
edgeguard-scheduler.service |
simple |
edgeguard-api.service |
edgeguard |
on-failure |
edgeguard-cert-deploy.path |
path |
— | — | — |
edgeguard-firewall.service |
oneshot, RemainAfterExit=true |
— | root | — |
edgeguard.target |
target |
api+scheduler | — | — |
Hardening-Defaults pro Unit (außer edgeguard-firewall, das braucht CAP_NET_ADMIN):
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
PrivateTmp=true
PrivateDevices=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallFilter=@system-service
ReadWritePaths=/var/lib/edgeguard /var/log/edgeguard /etc/edgeguard
Drittsoftware läuft als Distro-Units — EdgeGuard generiert deren Config + signalisiert Reload/Restart, übernimmt aber die Service-Verwaltung weitgehend nicht. Renderer existieren für: HAProxy, Squid, WireGuard (wg-quick@.service), Unbound, nftables, keepalived, chrony, Kea (kea-dhcp4-server), FreeRADIUS (letzte beide default-off). CrowdSec (crowdsec + crowdsec-firewall-bouncer) wird gemanagt, wenn installiert (kein Depends).
API bindet auf 127.0.0.1:9443 (nicht öffentlich). HAProxy terminiert TLS auf :443, leitet /.well-known/acme-challenge/* und Management-FQDN-Traffic an die API weiter, routet alle anderen Hosts per ACL an die User-Backends.
6. Datenbank-Setup
- PostgreSQL 16/17, Distro-Paket
postgresql-16 | postgresql-17. - Verbindung: Unix-Socket (
/var/run/postgresql) für lokale Reads + Writes der API. TCP/5432 (Rolleedgeguard_replicator) nur zwischen Cluster-Peers für die Logical-Replication-Verbindung. - Topologie (Ist-Stand): Logical Replication — ein Primary publiziert
edgeguard_shared(alle Tabellen außerlocalOnlyTables), N Subscriber (edgeguard_sub,wal_level=logical, Initialkopie viacopy_data=true). Jeder Node hat eine eigene beschreibbare PG-Instanz; geteilte Config fließt vom Primary zu den Subscribern. Es gibt keinen Write-Proxy — Schreibzugriffe auf geteilte Tabellen müssen am Primary erfolgen; ein Subscriber-Write auf eine replizierte Tabelle würde nicht propagieren (Drift-Banner erkennt das viaconfig_hash). Primary-Erkennung zuverlässig überpg_publication; der Standby-Bootstrap läuft per Logical Subscription (keinpg_basebackupim aktiven Pfad). - node-lokale Tabellen (nicht repliziert):
ha_nodes,network_interfaces,ip_addresses,static_routes,cluster_settings,dns_settings,ntp_settings,dhcp_settings,radius_settings,system_settings,join_tokens_used,audit_log,alert_events,backups,goose_db_version(Liste:cmd/edgeguard-ctl/cluster_replication.golocalOnlyTables). - Migrations:
goose(SQL-Dateien ininternal/database/migrations/, via//go:embed). Nicht GORM AutoMigrate.
GORM bleibt als ORM für Query-Komfort; Schema-Management läuft über goose.
7. Cluster-State & KeyDB (Ist-Stand: PostgreSQL-zentrisch)
Hinweis: Der ursprüngliche Entwurf sah KeyDB Active-Active als Cluster-State-Layer vor. Im Code ist das nicht umgesetzt — es gibt keinen Redis/KeyDB-Client (
go.modenthält nurpgx). KeyDB ist optional (Recommends) und wird vom laufenden System nicht benötigt.
Wie Cluster-State tatsächlich gehalten wird:
- PG-Primary — über
pg_publication(edgeguard_shared) ermittelt; die Peer-Adresse für Pushes stammt aussetup.jsonPrimaryFQDN. - Node-Heartbeat/-Status — Spalten
last_seen/statusin PGha_nodes. Jeder Node bumpt seine Row alle 30s (runClusterHeartbeat); Secondary→Primary (runPrimaryPush) und Primary→Secondary (runPeerPush) pushen sich gegenseitig per mTLS (30s, bidirektional).SweepStaleNodes(Scheduler) flippt Peers nach 2 min ohne Heartbeat aufoffline. - Lizenz — jeder Node verifiziert eigenständig gegen
license.netcell-it.com(kein Leader-Lock); Ergebnis in PGlicenses. - ACME — kein verteilter Issue-Lock implementiert (Single-Node-Default; bei Cluster Issue am aktiven/Primary-Node).
cluster:pg-primary-urlin KeyDB wird vonedgeguard-ctl promotegeschrieben, falls KeyDB läuft, aber von der API nie gelesen (advisory/Altlast).
Falls KeyDB künftig wieder eingeführt wird (Rate-Limiting-Counter, Pub/Sub-Config-Reload): hört auf 127.0.0.1:6379 lokal und <node-ip>:16379 (TLS) für Peer-Replication. Derzeit ungenutzt.
7.5 Unbound — DNS Forwarder + Cluster-DNS
Unbound erfüllt zwei Rollen, beide aus PG generiert:
Rolle 1 — Caching-Forwarder mit DNSSEC
- Forwardet rekursive Queries an Upstream-Resolver (default
1.1.1.1,9.9.9.9; per UI/PG konfigurierbar). - DNSSEC-Validation aktiv (
auto-trust-anchor-file). - Lokaler Cache (TTL nach Upstream-Antwort).
- Listen:
127.0.0.1:53für die EdgeGuard-Box selbst und<node-internal-ip>:53für VPN- und LAN-Clients (über nftables-ACL gefiltert). - Genutzt von
edgeguard-api,edgeguard-scheduler(License-Heartbeat, ACME), Squid (für Forward-Proxy-Resolutions), HAProxy (Backend-Health-Checks).
Rolle 2 — Cluster-internes Split-Horizon
- Local-Zone
eg.cluster.enthält A/AAAA-Records aller Cluster-Peers (Node-Hostnamen aus PGha_nodes). - Beispiel:
node1.eg.cluster → 10.42.0.11,node2.eg.cluster → 10.42.0.12. - Wird bei jedem Node-Join/-Leave aus PG regeneriert + via
edgeguard:config-changedPub/Sub auf allen Peers neu geladen (unbound-control reload). - Cluster-interner Traffic (PG-Logical-Replication, mTLS-Agent-Calls auf :8443, Cert-Push) löst Peer-Adressen ausschließlich über diese Zone auf — kein DNS-Roundtrip ins öffentliche Internet, keine
/etc/hosts-Synchronisation. <node-name>.eg.clusterist nicht extern erreichbar (nur über Unbound der Cluster-Peers).
Config-Schichten
/etc/edgeguard/unbound/unbound.conf ist Distro-Konfig-Datei. Generator schreibt drei Includes:
# /etc/edgeguard/unbound/forwarders.conf — Upstream-Resolver
# /etc/edgeguard/unbound/cluster-zone.conf — Local-Zone eg.cluster
# /etc/edgeguard/unbound/access.conf — access-control: pro CIDR
Reload via unbound-control reload (kein Restart, keine Cache-Invalidierung außer für die geänderte Zone — unbound-control auth_zone_reload eg.cluster).
8. Cluster-Topologie & HA pro Service
N symmetrische Peers (1 … N Nodes, jeder vollwertig). Public-IP-Failover via VIP/VRRP (keepalived) — siehe §9 (der ursprünglich geplante „Floating-IP statt VRRP"-Ansatz wurde nicht umgesetzt).
| Service | HA-Strategie |
|---|---|
| VIP/keepalived | VRRP (vrrp_instance), MASTER/BACKUP per pg_role (primary→prio 200/MASTER, standby→100/BACKUP). VIPs aus ip_addresses (is_vip=true). Trägt den HTTP/HTTPS-Ingress. |
| HAProxy | stateless, pro Node identisch. Hört auf der VIP des aktiven Node. ACME-Issue ohne verteilten Lock (Single-/Primary-Node); Zerts werden via mTLS (/agent/cluster/tls-certs) an alle verteilt. |
| Squid | stateless (Cache lokal). Pro Node identische ACL-Config. |
| WireGuard | siehe §8.1 |
| Unbound | stateless (Cache lokal). Pro Node identische Forwarder-Config + Cluster-Local-Zones (§7.5). |
| nftables | pro Node, Ruleset aus PG generiert. CrowdSec-Blocklist via crowdsec-firewall-bouncer (eigene Sets), wenn CrowdSec installiert. |
| edgeguard-api | pro Node, Reads lokal. Writes auf geteilte Tabellen am Primary (kein Write-Proxy). |
| edgeguard-ui / edgeguard-waf | statisch bzw. pro Node identisch. |
| PostgreSQL | Logical Replication (Publisher→Subscriber), manueller Promote (§8.2). |
| KeyDB | optional/ungenutzt (§7). |
8.1 WireGuard im Cluster
Drei Optionen, für v1 wählen wir Option A:
- A — Geteilte Server-Identität (gewählt): alle Peers haben denselben Server-Privatkey + dasselbe Listen-Port. Die VIP (keepalived) trägt das WireGuard-UDP zum aktiven Node. Bei Failover: VIP wandert, Clients schicken Pakete zum neuen Node, neuer Handshake (~1–2s Latenz beim ersten Paket). Replay-Protection-Counter werden nicht repliziert — beim Failover macht der Client neuen Handshake, alte Counter sind irrelevant.
- B — Pro Node eigene Identität, Client kennt alle: Client-Configs haben mehrere
[Peer]-Blöcke. Aufwendiger zu provisionieren, kein Failover-Vorteil. - C — Aktiv/Standby per License-Leader-Pattern: nur ein Node hat WireGuard aktiv, andere idle. Verschwendet Kapazität.
Begründung A: Privatkey liegt verschlüsselt in PG, wird per Logical Replication an die Peers verteilt. WireGuard handelt selbständig neue Sessions aus, kein State-Sync nötig. Peer-Änderungen propagieren über die Logical Replication; Secondaries erkennen die Änderung am config_hash (runSecondaryConfigRender, 5-min-Tick) und re-rendern lokal → wg syncconf.
8.2 Manual Promote (PG-Primary-Failover)
Bei Ausfall des Primary läuft die Datenebene (HAProxy/Squid/WireGuard/Unbound) weiter, weil jeder Node eine lokale, lesbare PG-Instanz (Logical-Subscriber) hat. Schreibzugriffe auf geteilte Config müssen am Primary erfolgen — fällt der Primary aus, promotet der Admin manuell via edgeguard-ctl promote. Das ist Logical-Replication-aware: es löst die Subscription zum toten Primary (DISABLE + slot_name=NONE + DROP, hängt also nicht am toten Publisher), richtet die Node via setupReplicationPrimary als Publisher ein (Rolle/Secret/wal_level=logical inkl. PG-Restart falls nötig/Publication), setzt ha_nodes.pg_role='primary' und rendert keepalived (→ MASTER, übernimmt die VIP). Erholte Nodes danach mit edgeguard-ctl cluster-setup-standby <neuer-primary> zurückhängen. Achtung: echtes Cross-Node-Failover ist nur im Drill testbar — die Bausteine (Drop-Subscription, Publication, Restart) sind dieselben wie in cluster-init-replication/cluster-setup-standby.
8.3 License-Verifikation
Kein Leader-Election (anders als ursprünglich geplant). Jeder Node verifiziert eigenständig gegen license.netcell-it.com (Scheduler-Tick), Ergebnis in PG licenses. active_servers = Anzahl Peers mit Heartbeat < 2 min (aus ha_nodes). Ein KeyDB-Lock existiert nicht.
9. Public-Ingress — VIP via keepalived/VRRP
Ist-Stand: Umgesetzt ist VIP-Failover über keepalived (VRRP) — nicht der ursprünglich angedachte „Floating-IP des Hosters"-Ansatz. Es gibt keinen Hoster-API-Code und keinen
POST /cluster/promote-this-node-Endpoint.
Mechanik (internal/keepalived):
- Renderer erzeugt
/etc/keepalived/keepalived.confmitvrrp_instance(unicast peer,virtual_router_id,authentication). - State/Priorität aus
pg_role: Primary →state MASTER,priority 200; Standby →state BACKUP,priority 100. - VIPs kommen aus
ip_addresses(is_vip=true,active=true), inkl. Interface; managed viasystemctl reload-or-restart keepalived. - Bei Node-/PG-Ausfall übernimmt VRRP die VIP auf den verbleibenden Node (Sekundenbereich).
Tooling: GET/PUT /cluster/vip-settings, GET /cluster/vip-status, POST /cluster/vip-test (Letzteres bewegt eine VIP testweise per ip addr add/del zwischen Nodes — kein Hoster-Call).
v1-Default: Single-Node. Im Cluster trägt der MASTER (Primary) die VIP.
⚑ OFFEN (Altlast-Bereinigung): Doku-Abschnitte/Code, die noch „Floating-IP des Hosters" implizieren, sind historisch — der reale Pfad ist keepalived/VRRP.
10. Erst-Einrichtung — curl-Onliner
curl -fsSL https://get.edgeguard.netcell-it.de | sudo bash
Schritte (idempotent, analog netcell-webpanel/install.sh):
- OS-Detection (
/etc/os-release): nur Debian 13 (Trixie), sonst Abbruch. - Arch-Detection: nur amd64 oder arm64.
- Base-Deps:
curl gnupg ca-certificates apt-transport-https. - APT-Keyrings:
https://apt.netcell-it.de/edgeguard/repository.key→/etc/apt/keyrings/netcell-edgeguard.gpg
- APT-Sources:
/etc/apt/sources.list.d/netcell-edgeguard.list. - Install:
apt-get install -y edgeguard(Meta-Paket). - Auto-Security-Updates:
unattended-upgrades+apt-listchanges(nach enconf-Muster). - Setup-Modus:
edgeguard-apiläuft im Setup-Modus bis Admin-User existiert. UI leitet alle Anfragen auf/setupum. Wizard: Admin-Account, FQDN, ACME-Email, Lizenz oder Trial.
Cluster-Join (zusätzlicher Peer):
curl -fsSL https://get.edgeguard.netcell-it.de | sudo bash -s -- \
--join https://<existing-node-fqdn> \
--token <cluster-join-token>
edgeguard-ctl cluster-join führt aus: TLS-Cert-Pull via mTLS (CSR→issue-cert) und Node-Registrierung in ha_nodes (autoRegister) — mehr nicht. Die Logical Replication ist ein eigener Schritt (cluster-setup-standby: CREATE SUBSCRIPTION … copy_data=true, Initialkopie der geteilten Tabellen, Master-Key-Sync, Config-Regeneration). (Kein pg_basebackup, kein KeyDB-Setup — beides war nur im ursprünglichen Entwurf.)
Join über den Setup-Wizard (empfohlener Weg) macht beides automatisch:
- Auf dem Primary erzeugt
POST /cluster/join-tokensden Token — und stellt dabei vorher viacluster-init-replicationsicher, dass die Publisher-Seite steht (Replikations-Rolle + Secret,wal_level=logical,pg_hba, PUBLICATION). Ein frisch installierter Single-Node hat das alles noch nicht; ohne diesen Schritt liefe das spätereCREATE SUBSCRIPTIONin ein 404. Idempotent; der einmalige PG-Restart (wal_levelist ein postmaster-Parameter) passiert bewusst hier, solange noch kein zweiter Node Traffic erwartet. - Auf dem neuen Node startet
POST /setup/join-clusternach erfolgreichem Joincluster-setup-standbydetached (viasudo, da root nötig). Fortschritt pollbar überGET /setup/replication-status(running/done/failed); der Wizard zeigt ihn an und gibt bei Fehlschlag das manuelle Kommando aus.
Der reine CLI-Pfad (cluster-join) bleibt der manuelle Weg und erfordert cluster-setup-standby weiterhin explizit.
11. Update-Pfad (apt-Repo)
- Primärquelle: Gitea Package Registry (
https://git.netcell-it.de/api/packages/projekte/debian). - Kunden-Mirror:
https://apt.netcell-it.de/edgeguard/(rsync von Gitea). - Suiten:
stable·testing·security— Codenametrixie. - Signatur: GPG-Key
netcell-edgeguard-signing, ausgeliefert in/etc/apt/keyrings/. - Update-Check-API:
GET /api/v1/system/package-versions→ proedgeguard-*-Paket{name, installed, available, reboot_required}. - Upgrade-Trigger:
POST /api/v1/system/upgradestartetsystemd-run --unit=edgeguard-upgrade.service --collect …(HTTP-Response geht VOR dem Upgrade raus, weil API beim Self-Update stirbt — Pattern ausnetcell-webpanel/management-agent/internal/handlers/update.go:105).
Build-/Release-Scripts identisch zu mail-gateway/scripts/apt-repo/.
12. Lizenz & ACME
12.1 Lizenz
1:1 nach netcell-webpanel/docs/licensing-integration.md. Verbrauchswert: active_domains (Anzahl konfigurierter EdgeGuard-Domains).
- Lizenzserver:
https://license.netcell-it.com(öffentlich, kein API-Key). - Verify-Endpoint:
GET /api/v1/licenses/{key}/verify?system_id={fp}&system_name={host}&active_domains={n}. - Fingerprint:
SHA256(/etc/machine-id + erste-aktive-MAC + hostname). - Caching: Live-Verify → Ergebnis in PG
licenses→/var/lib/edgeguard/trial.json(30-Tage-Trial-Fallback) →expired. - Keine Leader-Election — jeder Node verifiziert eigenständig (§8.3).
12.2 ACME
- certbot (Distro-Paket) mit
--webroot=/var/lib/edgeguard/acme— HAProxy ACLpath_beg /.well-known/acme-challenge/proxied diese Pfade anedgeguard-api, das die Challenge-Tokens aus der Webroot-Dir ausliefert. - Cluster-Locking: derzeit kein verteilter Issue-Lock implementiert (Single-Node-Default; im Cluster sollte ACME am Primary/aktiven Node laufen). (Der ursprünglich geplante KeyDB-
acme:lock:<domain>existiert nicht.) - Deploy-Hook: schreibt fertiges PEM (cert+chain+key kombiniert) nach
/etc/edgeguard/tls/<domain>.pemund triggertsystemctl reload haproxy. HAProxy lädt dencrt /etc/edgeguard/tls/-Verzeichnisinhalt neu. - Cert-Verteilung im Cluster: Issuing-Node pushed via mTLS-API an alle Peers, Zerts landen in
/etc/edgeguard/tls/.
13. UI — 100% enconf-WebPanel-Pattern
Komponentenbibliothek, Theme, Layouts, Navigations-Struktur, Form-Patterns, i18n-Setup vollständig 1:1 aus netcell-webpanel/management-ui/. Keine eigenen Design-Entscheidungen.
Pflichtlektüre:
netcell-webpanel/docs/design-system.mdnetcell-webpanel/docs/design.mdnetcell-webpanel/docs/frontend-reference.md
Stack: React 19 + TypeScript strict, Vite, Ant Design 6.x, TanStack Query 5, axios, i18next (de/en), @uiw/react-codemirror, recharts, ESLint flat config.
Seiten v1: Dashboard · Domains · Backends · Routing-Rules · SSL · ForwardProxy (Squid) · VPN (WireGuard) · Firewall · Cluster · Logs · Settings · Setup-Wizard.
14. Plattform-Matrix
| Distribution | Codename | Arch | Status |
|---|---|---|---|
| Debian 13 | trixie | amd64 | Tier 1 |
| Debian 13 | trixie | arm64 | Tier 1 |
Nur Debian 13 (Trixie). Die Build-/Publish-Pipeline (Makefile, scripts/apt-repo/) zielt ausschließlich auf trixie; der Installer bricht auf anderem OS hart ab. (Eine ursprünglich geplante Ubuntu-24.04-„noble"-Matrix ist nicht implementiert.) Andere Distributionen (Debian 12, Ubuntu, RHEL/Rocky) sind nicht unterstützt.
15. Migration vom Docker-Stand
EdgeGuard-Native ist eigenes Repo (git.netcell-it.de/projekte/edgeguard-native), parallel zum bestehenden proxy-lb-waf. Migration:
- Frische Installation auf Test-VM via
install.sh. - Config-Export aus altem Stack (
edgeguard-ctl export --from-docker) — liest aus alter PG, schreibt in neues Format. - Validierung Side-by-Side (alter Stack auf einem Server, neuer Stack auf anderem, Traffic vergleichen).
- Cutover via VIP-Umzug (keepalived) bzw. DNS-Umstellung.
Der alte proxy-lb-waf-Code bleibt für Bestandskunden im Wartungsmodus, keine neuen Features.
Offene Punkte
- Failover-Drill:
edgeguard-ctl promote(Logical-aware) + anschließendescluster-setup-standbyin einem echten 2-Node-Failover durchspielen (inkl. VIP-Umzug, WireGuard-Handshake-Latenz). (Code-Altlasteninternal/proxy-Stub undpromote.go-standby.signalwurden 2026-06 bereinigt.) - Optional KeyDB (Rate-Limit-Counter, Pub/Sub-Config-Reload) — falls je benötigt; aktuell ungenutzt.
get.edgeguard.netcell-it.deanlegen oder Übergangs-URL aufapt.netcell-it.de/edgeguard/install.shnutzen.