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>
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>
architecture.md war Entwurfsstand; Cluster/HA wich stark vom Code ab. Per 5 parallelen Code-Audits verifiziert + korrigiert:
- Replikation: Streaming/physisch → LOGICAL (edgeguard_shared/edgeguard_sub, wal_level=logical, copy_data=true); localOnlyTables dokumentiert; pg_basebackup nur Legacy.
- VIP/Ingress (§9): 'Floating-IP statt VRRP' war invertiert → real keepalived/VRRP (prio aus pg_role, VIPs aus ip_addresses); kein Hoster-API/promote-this-node.
- KeyDB (§7): Active-Active-State-Layer NICHT umgesetzt (kein Redis-Client in go.mod); Cluster-State/Heartbeat/Locks in PostgreSQL; KeyDB optional (Recommends). license-leader/acme:lock/cluster:nodes = nur Kommentare.
- Write-Path: internal/proxy ist leerer Stub; kein Write-Proxy → Writes am Primary.
- Plattform: nur Debian 13 trixie (Pipeline), Ubuntu/noble nicht implementiert.
- §1/§5/§8 + Strukturbaum/Depends/Units an reale Renderer (keepalived/chrony/kea/freeradius/crowdsec/waf) angeglichen. Offene Punkte: Code-Altlasten (proxy-Stub, standby.signal in promote.go).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Initialer Schema-Set (8 Migrationen, 13 Tabellen) für EdgeGuard v1:
users + audit_log + system_settings, ha_nodes, backends/domains/
routing_rules/tls_certs, forward_proxy_acls, wireguard_peers,
firewall_rules, dns_zones/dns_records, licenses. Migrations liegen
in internal/database/migrations/ (analog mail-gateway) und werden
per //go:embed ins Binary gepackt — keine separate SQL-Dateien im
.deb. ValidateMigrations + Test schützen vor Duplicate-Versionen
(mail-gateway 2026-05-08-Vorfall). GORM-Models für alle Tabellen,
sensible Felder (password_hash, private_key_enc) sind json:"-".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverse-Proxy von Angie (eigenes APT-Repo) auf nginx (Distro) umgestellt
— vereinfacht Bootstrap (kein angie.software-Repo mehr), reduziert
Offene-Punkte (arm64-Verfügbarkeit entfällt). Neuer Service Unbound
übernimmt zwei Rollen: Caching-Forwarder mit DNSSEC und Cluster-internes
Split-Horizon (Local-Zone eg.cluster, Peer-Adressen aus PG ha_nodes,
Reload via unbound-control). Architektur-Spec §7.5 dokumentiert beide
Rollen + Config-Schichtung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>