preRegisterPrimary legte unbedingt eine Platzhalter-Zeile fuer den
Primary an. Sobald der Primary sich aber selbst gemeldet hat, existiert
bereits eine Zeile mit seiner ECHTEN Node-ID und demselben FQDN — und
UpsertSelf nutzt ON CONFLICT (id), greift also nicht. Die Synthetik-ID
"prenode-<fqdn>" lief damit in den fqdn-Unique-Index:
duplicate key value violates unique constraint "ha_nodes_fqdn_unique"
bei JEDEM API-Start auf dem Standby. Folgenlos — die echte Zeile ist ja
korrekt und der Firewall-Reload lief trotzdem — aber es sah nach einem
Defekt aus und verdeckte echte Warnungen im selben Log.
Jetzt wird zuerst die IP einer vorhandenen Zeile aufgefrischt (mehr will
die Funktion gar nicht) und nur bei RowsAffected()==0 der Platzhalter
angelegt — also genau dann, wenn der Primary sich noch nie gemeldet hat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
On API restart, cluster nodes now re-register their primary in the local
ha_nodes and reload nftables so @peer_ipv4 is correct after a package
update or reboot without requiring a re-join.
Also fixes duplicate ha_nodes rows: preRegisterPrimary previously used
time.Now().UnixNano() as node ID, creating a fresh row each call.
Now uses a deterministic ID derived from the FQDN so repeated upserts
are idempotent.
PrimaryFQDN is now persisted in setup.json during CompleteAsNode so the
startup sync knows which primary to contact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nach erfolgreichem cluster-join trägt der Joining-Node (utm-2) den Primary
per DNS-Lookup in seine lokale ha_nodes ein und lädt nftables neu.
Damit ist @peer_ipv4 auf utm-2 sofort mit utm-1's IP befüllt und Port 8443
ist bidirektional offen — der Aggregator auf utm-1 kann utm-2 auf :8443
abklappern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sicherheit des Joins kommt vom HMAC-Token, nicht von TLS-Cert-Trust.
Insecure=true wird jetzt immer im Handler gesetzt — GUI-Nutzer müssen
das nicht kennen oder konfigurieren.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Der API-Boot erstellt für Single-Node-Betrieb automatisch ein selbst-signiertes
peer-Cert. Beim Join über die GUI wurde das als Fehler gemeldet.
clusterjoin.Request.Force=true im Setup-Handler überspringt die HasPeer-Prüfung;
CLI (edgeguard-ctl cluster-join) bleibt vorsichtig (Force=false, manuelles rm nötig).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /setup/join-cluster macht alles was bisher edgeguard-ctl cluster-join
tat: CSR generieren, Certs vom Primary holen, schreiben, auto-registrieren,
Setup als Cluster-Node markieren.
Setup-Wizard Node-Modus fragt jetzt direkt Primary-FQDN + Join-Token ab.
Nach Submit: Erfolgsmeldung + einziger verbleibender Schritt (systemctl restart).
Neue interne Bibliothek: internal/services/clusterjoin — wird von Handler
und CLI (edgeguard-ctl cluster-join) gleichermaßen genutzt, keine Duplizierung.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Setup-Wizard zeigt jetzt beim ersten Aufruf eine Modusauswahl:
- "Neuinstallation" → bisheriger Flow (Admin-Account + FQDN + ACME)
- "Cluster-Knoten beitreten" → nur FQDN + ACME, kein Admin-Account;
nach dem Submit werden die cluster-join-Befehle direkt angezeigt
Backend: NodeRequest + Store.CompleteAsNode() + POST /setup/complete-node
State: is_cluster_node Flag; login via PG-Replikation vom Primary
Install-Script: Setup-URL zeigt jetzt korrekt :3443/setup statt /setup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
REST-API mit Response-Envelope (1:1 mail-gateway), HS256-JWT-Signer
(Secret persistent unter /var/lib/edgeguard/.jwt_fingerprint),
Setup-Wizard (Bcrypt-Admin-Passwort in setup.json), Auth-Middleware
(Cookie + Bearer), Setup-Gate. Update-Banner-Endpoints
/system/package-versions + /system/upgrade ab Tag 1 wired (Pattern
aus enconf-management-agent: systemd-run detached, HTTP-Response
geht VOR dem Self-Replace raus).
CRUD-Repos für domains/backends/routing_rules mit pgxpool +
handgeschriebenem SQL (mail-gateway-Pattern, kein GORM zur Laufzeit).
Audit-Log-Schreiber auf jede Mutation, NodeID aus /etc/machine-id.
DB-Pool öffnet best-effort — ohne erreichbare PG bleiben CRUD-Routen
unregistriert, Auth/Setup/System antworten weiter (Dev ohne PG).
End-to-end live-getestet gegen lokale postgres-16: Setup → Login →
POST/PUT/DELETE Backends + Domains + Routing-Rules → audit_log
schreibt 5 Zeilen mit korrektem actor/action/subject. Graceful
degrade ohne DB ebenfalls verifiziert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>