# 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`](../../mail-gateway/docs/architecture.md) (`nmg`); UI-Pattern und Bootstrap-Onliner stammen aus [`netcell-webpanel`](../../netcell-webpanel/CLAUDE.md) (`enconf`). --- ## 0. Leitplanken (nicht verhandelbar) - **Kein Docker.** Alle Dienste nativ unter `systemd`, installiert via `apt`. 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 auf `trixie`. _(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-Onliner `curl -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`):** 1. User `edgeguard` anlegen (`adduser --system --group --home /var/lib/edgeguard`). 2. `/etc/edgeguard/`, `/var/lib/edgeguard/`, `/var/log/edgeguard/` mit `0750`, `chown edgeguard:edgeguard`. 3. Default-Configs nur anlegen wenn nicht vorhanden (`conffiles` verhindert Überschreiben). 4. PostgreSQL: `edgeguard-ctl initdb` (idempotent — prüft DB/User). 5. DB-Migration: `edgeguard-ctl migrate up`. 6. `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 (Rolle `edgeguard_replicator`) nur zwischen Cluster-Peers für die Logical-Replication-Verbindung. - **Topologie (Ist-Stand):** **Logical Replication** — ein Primary publiziert `edgeguard_shared` (alle Tabellen außer `localOnlyTables`), N Subscriber (`edgeguard_sub`, `wal_level=logical`, Initialkopie via `copy_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 via `config_hash`). Primary-Erkennung zuverlässig über `pg_publication`; der Standby-Bootstrap läuft per Logical Subscription (kein `pg_basebackup` im 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.go` `localOnlyTables`). - **Migrations:** `goose` (SQL-Dateien in `internal/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.mod` enthält nur `pgx`). 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 aus `setup.json` `PrimaryFQDN`. - **Node-Heartbeat/-Status** — Spalten `last_seen`/`status` in PG `ha_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 auf `offline`. - **Lizenz** — jeder Node verifiziert **eigenständig** gegen `license.netcell-it.com` (kein Leader-Lock); Ergebnis in PG `licenses`. - **ACME** — kein verteilter Issue-Lock implementiert (Single-Node-Default; bei Cluster Issue am aktiven/Primary-Node). - `cluster:pg-primary-url` in KeyDB wird von `edgeguard-ctl promote` **geschrieben, 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 `: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:53` für die EdgeGuard-Box selbst und `:53` fü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 PG `ha_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-changed` Pub/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. - `.eg.cluster` ist **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 ` 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.conf` mit `vrrp_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 via `systemctl 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`): 1. **OS-Detection** (`/etc/os-release`): nur Debian 13 (Trixie), sonst Abbruch. 2. **Arch-Detection**: nur amd64 *oder* arm64. 3. **Base-Deps:** `curl gnupg ca-certificates apt-transport-https`. 4. **APT-Keyrings:** - `https://apt.netcell-it.de/edgeguard/repository.key` → `/etc/apt/keyrings/netcell-edgeguard.gpg` 5. **APT-Sources:** `/etc/apt/sources.list.d/netcell-edgeguard.list`. 6. **Install:** `apt-get install -y edgeguard` (Meta-Paket). 7. **Auto-Security-Updates:** `unattended-upgrades` + `apt-listchanges` (nach enconf-Muster). 8. **Setup-Modus:** `edgeguard-api` läuft im Setup-Modus bis Admin-User existiert. UI leitet alle Anfragen auf `/setup` um. 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:// \ --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:** 1. Auf dem Primary erzeugt `POST /cluster/join-tokens` den Token — und stellt dabei vorher via `cluster-init-replication` sicher, 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ätere `CREATE SUBSCRIPTION` in ein 404. Idempotent; der einmalige PG-Restart (`wal_level` ist ein postmaster-Parameter) passiert bewusst hier, solange noch kein zweiter Node Traffic erwartet. 2. Auf dem neuen Node startet `POST /setup/join-cluster` nach erfolgreichem Join `cluster-setup-standby` detached (via `sudo`, da root nötig). Fortschritt pollbar über `GET /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` — Codename `trixie`. - **Signatur:** GPG-Key `netcell-edgeguard-signing`, ausgeliefert in `/etc/apt/keyrings/`. - **Update-Check-API:** `GET /api/v1/system/package-versions` → pro `edgeguard-*`-Paket `{name, installed, available, reboot_required}`. - **Upgrade-Trigger:** `POST /api/v1/system/upgrade` startet `systemd-run --unit=edgeguard-upgrade.service --collect …` (HTTP-Response geht VOR dem Upgrade raus, weil API beim Self-Update stirbt — Pattern aus `netcell-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 ACL `path_beg /.well-known/acme-challenge/` proxied diese Pfade an `edgeguard-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:` existiert nicht.)_ - **Deploy-Hook:** schreibt fertiges PEM (cert+chain+key kombiniert) nach `/etc/edgeguard/tls/.pem` und triggert `systemctl reload haproxy`. HAProxy lädt den `crt /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.md` - `netcell-webpanel/docs/design.md` - `netcell-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: 1. **Frische Installation** auf Test-VM via `install.sh`. 2. **Config-Export** aus altem Stack (`edgeguard-ctl export --from-docker`) — liest aus alter PG, schreibt in neues Format. 3. **Validierung** Side-by-Side (alter Stack auf einem Server, neuer Stack auf anderem, Traffic vergleichen). 4. **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ßendes `cluster-setup-standby` in einem echten 2-Node-Failover durchspielen (inkl. VIP-Umzug, WireGuard-Handshake-Latenz). _(Code-Altlasten `internal/proxy`-Stub und `promote.go`-`standby.signal` wurden 2026-06 bereinigt.)_ - **Optional KeyDB** (Rate-Limit-Counter, Pub/Sub-Config-Reload) — falls je benötigt; aktuell ungenutzt. - **`get.edgeguard.netcell-it.de`** anlegen oder Übergangs-URL auf `apt.netcell-it.de/edgeguard/install.sh` nutzen.