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

30 KiB
Raw Blame History

EdgeGuard — Architektur

Status: in Produktion (v1.2.x) · Entwurf: 2026-05-08 · Cluster/HA-Abschnitte an Ist-Stand angeglichen: 2026-06-06 · Plattform: Debian 13 (Trixie), amd64 + arm64.

⚠️ Lesehinweis: Dieses Dokument war ursprünglich ein Entwurf. Mehrere Cluster/HA-Annahmen wurden anders umgesetzt — maßgeblich sind die mit „Ist-Stand" markierten Stellen (§0, §6§9). Kurzfassung: Logical Replication statt Streaming, keepalived/VRRP statt Hoster-Floating-IP, KeyDB optional/ungenutzt (Cluster-State in PostgreSQL), kein Write-Proxy.

EdgeGuard ist die native Neufassung des bisherigen Docker-basierten Reverse-Proxy/Loadbalancer/Forward-Proxy/VPN-Stacks. Vorbild für Architektur, Build-System und Cluster-Modell ist mail-gateway (nmg); UI-Pattern und Bootstrap-Onliner stammen aus netcell-webpanel (enconf).


0. Leitplanken (nicht verhandelbar)

  • Kein Docker. Alle Dienste nativ unter systemd, installiert 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.