Files
edgeguard-native/docs/architecture.md
Debian 7611572062 refactor(cluster): promote auf Logical-Replication umgestellt + internal/proxy-Stub entfernt — v1.2.99
Code-Altlasten aus dem Architektur-Audit bereinigt:
- internal/proxy: leerer .gitkeep-Stub (geplanter Write-Proxy nie implementiert) entfernt — keine Go-Referenzen.
- promote.go: war reines Physical-Replication-Failover (standby.signal + pg_ctlcluster promote + pg_is_in_recovery) und damit auf dem Logical-Setup TOT (ein Subscriber hat kein standby.signal / ist nie in recovery → Abbruch bei Schritt 1). Neu Logical-aware: Idempotenz-Check (schon Publisher ohne Subscription → fertig) → Subscription lösen (DISABLE+slot_name=NONE+DROP, hängt nicht am toten Publisher) → setupReplicationPrimary (Publisher werden) → ha_nodes.pg_role=primary → keepalived MASTER. Toter KeyDB-Update (cluster:pg-primary-url, wurde nie gelesen) entfernt.
- setupReplicationPrimary + dropSubscriptionIfExists aus cluster-init-replication/cluster-setup-standby extrahiert (DRY, bewährte SQL wiederverwendet). WICHTIG: setupReplicationPrimary stellt jetzt sicher dass wal_level=logical AKTIV ist — PG-RESTART falls nötig (reload reicht für wal_level/max_wal_senders nicht; Secondary hat wal_level=replica). Idempotent: Restart nur wenn wal_level != logical.
- Doku (CLAUDE.md + architecture.md) auf den bereinigten Stand gezogen.
Hinweis: echtes Cross-Node-Failover ist nur im Drill testbar; Build/vet/Tests grün, Bausteine sind die bereits produktiv genutzten SQL-Primitive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:26:52 +02:00

424 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<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:53` für die EdgeGuard-Box selbst und `<node-internal-ip>: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.
- `<node-name>.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 (~12s 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.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://<existing-node-fqdn> \
--token <cluster-join-token>
```
`edgeguard-ctl cluster-join` führt aus: TLS-Cert-Pull via mTLS (CSR→issue-cert), Node-Registrierung in `ha_nodes` (`autoRegister`), Setup als **Logical-Replication-Subscriber** (`cluster-setup-standby`: `CREATE SUBSCRIPTION … copy_data=true`, Initialkopie der geteilten Tabellen), Config-Regeneration, Service-Start. _(Kein `pg_basebackup`, kein KeyDB-Setup — beides war nur im ursprünglichen Entwurf.)_
---
## 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:<domain>` existiert nicht.)_
- **Deploy-Hook:** schreibt fertiges PEM (cert+chain+key kombiniert) nach `/etc/edgeguard/tls/<domain>.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.