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>
431 lines
31 KiB
Markdown
431 lines
31 KiB
Markdown
# 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 (~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.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) 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:<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.
|