feat(cluster): PG Logical Replication setup + Master-Key-Sync + Firewall-Ports
cluster-init-replication: - listen_addresses = '*' damit Cluster-Peers PG auf :5432 erreichen können - max_replication_slots = 20 / max_wal_senders = 10 (verhindert Slot-Erschöpfung bei initaler Tabellen-Synchronisation mit vielen gleichzeitigen Sync-Workern) - pg-replication-secret: Ownership an edgeguard-User (API-Lesezugriff) - detectPGConfig() statt hardcoded PG 16 (System läuft PG 17) cluster-setup-standby: - syncMasterKey(): holt /var/lib/edgeguard/.master_key via mTLS vom Primary — ohne identischen Master-Key können replizierte WireGuard-Keys nicht entschlüsselt werden - render-config: sudo -u edgeguard statt als root (DB-Zugriff) nftables Template: - Port 5432 (PG) + 6379 (KeyDB) für Cluster-Peers (@peer_ipv4/@peer_ipv6) freigegeben handlers/cluster.go: - GET /agent/cluster/master-key: gibt .master_key via mTLS zurück (hex-kodiert) v1.2.15 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,22 +20,55 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pgVersion = "16"
|
|
||||||
pgCluster = "main"
|
|
||||||
pgDataDir = "/var/lib/postgresql/16/main"
|
|
||||||
pgHBAPath = "/etc/postgresql/16/main/pg_hba.conf"
|
|
||||||
pgConfD = "/etc/postgresql/16/main/conf.d"
|
|
||||||
egReplSecret = "/var/lib/edgeguard/pg-replication-secret"
|
egReplSecret = "/var/lib/edgeguard/pg-replication-secret"
|
||||||
egReplUser = "edgeguard_replicator"
|
egReplUser = "edgeguard_replicator"
|
||||||
egPubName = "edgeguard_shared"
|
egPubName = "edgeguard_shared"
|
||||||
egSubName = "edgeguard_sub"
|
egSubName = "edgeguard_sub"
|
||||||
|
|
||||||
// Tabellen die NODE-SPEZIFISCH sind und NICHT repliziert werden.
|
|
||||||
// Jede Node hat eigene Interfaces, IPs, Routen, VIP-Einstellungen,
|
|
||||||
// Listener-Adressen, Token-Tracking und Audit-Log.
|
|
||||||
// Analog zu OPNsense: Interface-IPs und Hostname bleiben immer lokal.
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// pgConfig hält die zur Laufzeit erkannten PG-Pfade.
|
||||||
|
type pgConfig struct {
|
||||||
|
Version string // z.B. "17"
|
||||||
|
Cluster string // z.B. "main"
|
||||||
|
DataDir string // /var/lib/postgresql/17/main
|
||||||
|
HBAPath string // /etc/postgresql/17/main/pg_hba.conf
|
||||||
|
ConfD string // /etc/postgresql/17/main/conf.d
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectPGConfig ermittelt Version, Cluster und Pfade aus der laufenden
|
||||||
|
// PG-Instanz via SHOW hba_file / SHOW data_directory. Damit ist der Code
|
||||||
|
// unabhängig von der PG-Hauptversion (16, 17, …).
|
||||||
|
func detectPGConfig() (pgConfig, error) {
|
||||||
|
hbaRaw, err := psqlRun([]string{"-tA", "-c", "SHOW hba_file;"})
|
||||||
|
if err != nil {
|
||||||
|
return pgConfig{}, fmt.Errorf("cannot detect pg hba_file: %w", err)
|
||||||
|
}
|
||||||
|
hbaPath := strings.TrimSpace(string(hbaRaw))
|
||||||
|
|
||||||
|
dataRaw, err := psqlRun([]string{"-tA", "-c", "SHOW data_directory;"})
|
||||||
|
if err != nil {
|
||||||
|
return pgConfig{}, fmt.Errorf("cannot detect pg data_directory: %w", err)
|
||||||
|
}
|
||||||
|
dataDir := strings.TrimSpace(string(dataRaw))
|
||||||
|
|
||||||
|
// hbaPath: /etc/postgresql/<version>/<cluster>/pg_hba.conf
|
||||||
|
parts := strings.Split(filepath.ToSlash(hbaPath), "/")
|
||||||
|
if len(parts) < 6 {
|
||||||
|
return pgConfig{}, fmt.Errorf("unexpected hba_file path: %s", hbaPath)
|
||||||
|
}
|
||||||
|
version := parts[3]
|
||||||
|
cluster := parts[4]
|
||||||
|
confD := filepath.Join("/etc/postgresql", version, cluster, "conf.d")
|
||||||
|
|
||||||
|
return pgConfig{
|
||||||
|
Version: version,
|
||||||
|
Cluster: cluster,
|
||||||
|
DataDir: dataDir,
|
||||||
|
HBAPath: hbaPath,
|
||||||
|
ConfD: confD,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// localOnlyTables listet alle Tabellen die nicht in die Replikations-
|
// localOnlyTables listet alle Tabellen die nicht in die Replikations-
|
||||||
// Publication aufgenommen werden. Alles andere wird automatisch repliziert.
|
// Publication aufgenommen werden. Alles andere wird automatisch repliziert.
|
||||||
var localOnlyTables = []string{
|
var localOnlyTables = []string{
|
||||||
@@ -70,6 +105,13 @@ func cmdClusterInitReplication(args []string) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pg, err := detectPGConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "cluster-init-replication: PG-Erkennung:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Printf("→ PostgreSQL %s/%s erkannt\n", pg.Version, pg.Cluster)
|
||||||
|
|
||||||
// 1. Passwort generieren
|
// 1. Passwort generieren
|
||||||
pass, err := generatePassword(32)
|
pass, err := generatePassword(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -102,24 +144,34 @@ $$`, egReplUser, egReplUser, pass, egReplUser, pass)
|
|||||||
fmt.Fprintln(os.Stderr, "cluster-init-replication: write secret:", err)
|
fmt.Fprintln(os.Stderr, "cluster-init-replication: write secret:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
// Ownership an edgeguard-api-User übergeben damit die API lesen kann
|
||||||
|
if u, err := user.Lookup("edgeguard"); err == nil {
|
||||||
|
uid, _ := strconv.Atoi(u.Uid)
|
||||||
|
gid, _ := strconv.Atoi(u.Gid)
|
||||||
|
_ = os.Chown(egReplSecret, uid, gid)
|
||||||
|
}
|
||||||
fmt.Printf("✓ Replication-Secret gespeichert: %s\n", egReplSecret)
|
fmt.Printf("✓ Replication-Secret gespeichert: %s\n", egReplSecret)
|
||||||
|
|
||||||
// 4. conf.d/edgeguard-replication.conf schreiben
|
// 4. conf.d/edgeguard-replication.conf schreiben
|
||||||
// wal_level=logical ist eine Obermenge von replica — unterstützt
|
// wal_level=logical ist eine Obermenge von replica — unterstützt
|
||||||
// sowohl Logical Replication als auch ggfs. physisches WAL-Archiving.
|
// sowohl Logical Replication als auch ggfs. physisches WAL-Archiving.
|
||||||
if err := os.MkdirAll(pgConfD, 0o755); err != nil {
|
if err := os.MkdirAll(pg.ConfD, 0o755); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "cluster-init-replication: conf.d mkdir:", err)
|
fmt.Fprintln(os.Stderr, "cluster-init-replication: conf.d mkdir:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
replConf := `# EdgeGuard Logical Replication — automatisch generiert
|
replConf := `# EdgeGuard Logical Replication — automatisch generiert
|
||||||
# Nicht manuell bearbeiten; wird von edgeguard-ctl cluster-init-replication verwaltet.
|
# Nicht manuell bearbeiten; wird von edgeguard-ctl cluster-init-replication verwaltet.
|
||||||
wal_level = logical
|
wal_level = logical
|
||||||
max_wal_senders = 5
|
max_wal_senders = 10
|
||||||
max_replication_slots = 5
|
max_replication_slots = 20
|
||||||
max_logical_replication_workers = 4
|
max_logical_replication_workers = 4
|
||||||
wal_keep_size = 512MB
|
wal_keep_size = 512MB
|
||||||
|
# Lausche auf localhost + alle konfigurierten Interfaces damit Cluster-Peers
|
||||||
|
# sich verbinden können. '*' ist sicher weil pg_hba.conf den Zugriff auf
|
||||||
|
# bekannte Replikations-User beschränkt.
|
||||||
|
listen_addresses = '*'
|
||||||
`
|
`
|
||||||
confPath := filepath.Join(pgConfD, "edgeguard-replication.conf")
|
confPath := filepath.Join(pg.ConfD, "edgeguard-replication.conf")
|
||||||
if err := os.WriteFile(confPath, []byte(replConf), 0o644); err != nil {
|
if err := os.WriteFile(confPath, []byte(replConf), 0o644); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "cluster-init-replication: write postgresql conf:", err)
|
fmt.Fprintln(os.Stderr, "cluster-init-replication: write postgresql conf:", err)
|
||||||
return 1
|
return 1
|
||||||
@@ -127,18 +179,18 @@ wal_keep_size = 512MB
|
|||||||
fmt.Printf("✓ %s geschrieben (wal_level=logical)\n", confPath)
|
fmt.Printf("✓ %s geschrieben (wal_level=logical)\n", confPath)
|
||||||
|
|
||||||
// 5. pg_hba.conf aktualisieren
|
// 5. pg_hba.conf aktualisieren
|
||||||
if err := ensureHBAReplication(); err != nil {
|
if err := ensureHBAReplication(pg.HBAPath); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "cluster-init-replication: pg_hba.conf:", err)
|
fmt.Fprintln(os.Stderr, "cluster-init-replication: pg_hba.conf:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
fmt.Printf("✓ %s aktualisiert\n", pgHBAPath)
|
fmt.Printf("✓ %s aktualisiert\n", pg.HBAPath)
|
||||||
|
|
||||||
// 6. PG reload (damit wal_level + pg_hba aktiv werden)
|
// 6. PG reload (damit wal_level + pg_hba aktiv werden)
|
||||||
if out, err := exec.Command("pg_ctlcluster", pgVersion, pgCluster, "reload").CombinedOutput(); err != nil {
|
if out, err := exec.Command("pg_ctlcluster", pg.Version, pg.Cluster, "reload").CombinedOutput(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "cluster-init-replication: pg reload failed: %v\n%s\n", err, out)
|
fmt.Fprintf(os.Stderr, "cluster-init-replication: pg reload failed: %v\n%s\n", err, out)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
fmt.Printf("✓ PostgreSQL %s/%s neu geladen\n", pgVersion, pgCluster)
|
fmt.Printf("✓ PostgreSQL %s/%s neu geladen\n", pg.Version, pg.Cluster)
|
||||||
|
|
||||||
// 7. SELECT-Grants: edgeguard_replicator muss alle zu replizierenden
|
// 7. SELECT-Grants: edgeguard_replicator muss alle zu replizierenden
|
||||||
// Tabellen lesen können. DEFAULT PRIVILEGES sichert zukünftige Tabellen.
|
// Tabellen lesen können. DEFAULT PRIVILEGES sichert zukünftige Tabellen.
|
||||||
@@ -213,8 +265,8 @@ func createPublication() error {
|
|||||||
// in pg_hba.conf ein. Für Logical Replication brauchen wir einen
|
// in pg_hba.conf ein. Für Logical Replication brauchen wir einen
|
||||||
// normalen "host edgeguard"-Eintrag (nicht "host replication").
|
// normalen "host edgeguard"-Eintrag (nicht "host replication").
|
||||||
// Idempotent via Marker-Kommentar.
|
// Idempotent via Marker-Kommentar.
|
||||||
func ensureHBAReplication() error {
|
func ensureHBAReplication(hbaPath string) error {
|
||||||
data, err := os.ReadFile(pgHBAPath)
|
data, err := os.ReadFile(hbaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read: %w", err)
|
return fmt.Errorf("read: %w", err)
|
||||||
}
|
}
|
||||||
@@ -222,8 +274,6 @@ func ensureHBAReplication() error {
|
|||||||
if strings.Contains(string(data), marker) {
|
if strings.Contains(string(data), marker) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Logical Replication: Subscriber verbindet sich auf die DB (nicht "replication"-Typ)
|
|
||||||
// Physical/WAL-Archiving: "replication"-Typ bleibt für Kompatibilität
|
|
||||||
entry := fmt.Sprintf(`
|
entry := fmt.Sprintf(`
|
||||||
%s
|
%s
|
||||||
host edgeguard %s 0.0.0.0/0 scram-sha-256
|
host edgeguard %s 0.0.0.0/0 scram-sha-256
|
||||||
@@ -231,7 +281,7 @@ host edgeguard %s ::/0 scram-sha-256
|
|||||||
host replication %s 0.0.0.0/0 scram-sha-256
|
host replication %s 0.0.0.0/0 scram-sha-256
|
||||||
host replication %s ::/0 scram-sha-256
|
host replication %s ::/0 scram-sha-256
|
||||||
`, marker, egReplUser, egReplUser, egReplUser, egReplUser)
|
`, marker, egReplUser, egReplUser, egReplUser, egReplUser)
|
||||||
f, err := os.OpenFile(pgHBAPath, os.O_APPEND|os.O_WRONLY, 0o640)
|
f, err := os.OpenFile(hbaPath, os.O_APPEND|os.O_WRONLY, 0o640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open: %w", err)
|
return fmt.Errorf("open: %w", err)
|
||||||
}
|
}
|
||||||
@@ -345,11 +395,19 @@ WHERE srsubid = (SELECT oid FROM pg_subscription WHERE subname = '%s')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. render-config ausführen
|
// 5. Master-Key vom Primary holen — für WireGuard-Key-Entschlüsselung
|
||||||
|
fmt.Println("→ Secrets Master-Key vom Primary synchronisieren...")
|
||||||
|
if err := syncMasterKey(primaryHost, *agentPort, *tlsDir); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "cluster-setup-standby: master-key: %v (WireGuard-Keys können nicht entschlüsselt werden)\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("✓ Master-Key synchronisiert")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. render-config ausführen — muss als edgeguard-User laufen (DB-Zugriff)
|
||||||
fmt.Println("→ Service-Configs neu rendern...")
|
fmt.Println("→ Service-Configs neu rendern...")
|
||||||
if out, err := exec.Command("edgeguard-ctl", "render-config", "--no-reload").CombinedOutput(); err != nil {
|
if out, err := exec.Command("sudo", "-u", "edgeguard", "edgeguard-ctl", "render-config").CombinedOutput(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "cluster-setup-standby: render-config: %v\n%s\n", err, out)
|
fmt.Fprintf(os.Stderr, "cluster-setup-standby: render-config: %v\n%s\n", err, out)
|
||||||
fmt.Println(" → Manuell nachholen: edgeguard-ctl render-config")
|
fmt.Println(" → Manuell nachholen: sudo -u edgeguard edgeguard-ctl render-config")
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(string(out))
|
fmt.Print(string(out))
|
||||||
fmt.Println("✓ Service-Configs aktualisiert")
|
fmt.Println("✓ Service-Configs aktualisiert")
|
||||||
@@ -429,6 +487,66 @@ func fetchReplicationCreds(host string, agentPort int, tlsDir string) (*pgReplic
|
|||||||
return &result.Data, nil
|
return &result.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// syncMasterKey holt den Secrets-Master-Key vom Primary via mTLS und schreibt
|
||||||
|
// ihn nach /var/lib/edgeguard/.master_key. Dadurch können replizierte
|
||||||
|
// verschlüsselte WireGuard-Keys und PSKs auf dem Secondary entschlüsselt werden.
|
||||||
|
func syncMasterKey(host string, agentPort int, tlsDir string) error {
|
||||||
|
caPath := filepath.Join(tlsDir, "ca.crt")
|
||||||
|
certPath := filepath.Join(tlsDir, "peer.crt")
|
||||||
|
keyPath := filepath.Join(tlsDir, "peer.key")
|
||||||
|
|
||||||
|
caCert, err := os.ReadFile(caPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read ca.crt: %w", err)
|
||||||
|
}
|
||||||
|
rootPool := x509.NewCertPool()
|
||||||
|
rootPool.AppendCertsFromPEM(caCert)
|
||||||
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load peer cert: %w", err)
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: rootPool,
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://%s:%d/agent/cluster/master-key", host, agentPort)
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GET %s: %w", url, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("GET %s: HTTP %d", url, resp.StatusCode)
|
||||||
|
}
|
||||||
|
var result struct {
|
||||||
|
Data struct {
|
||||||
|
KeyHex string `json:"key_hex"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
key := make([]byte, 32)
|
||||||
|
if _, err := fmt.Sscanf(result.Data.KeyHex, "%x", &key); err != nil {
|
||||||
|
return fmt.Errorf("decode key_hex: %w", err)
|
||||||
|
}
|
||||||
|
const masterKeyPath = "/var/lib/edgeguard/.master_key"
|
||||||
|
if err := os.WriteFile(masterKeyPath, key, 0o600); err != nil {
|
||||||
|
return fmt.Errorf("write master key: %w", err)
|
||||||
|
}
|
||||||
|
if u, err := user.Lookup("edgeguard"); err == nil {
|
||||||
|
uid, _ := strconv.Atoi(u.Uid)
|
||||||
|
gid, _ := strconv.Atoi(u.Gid)
|
||||||
|
_ = os.Chown(masterKeyPath, uid, gid)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// generatePassword erzeugt ein kryptographisch sicheres Passwort.
|
// generatePassword erzeugt ein kryptographisch sicheres Passwort.
|
||||||
func generatePassword(n int) (string, error) {
|
func generatePassword(n int) (string, error) {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.2.3"
|
var version = "1.2.15"
|
||||||
|
|
||||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,14 @@ import (
|
|||||||
// 6. keepalived.conf neu rendern (Primary bekommt Priorität 200)
|
// 6. keepalived.conf neu rendern (Primary bekommt Priorität 200)
|
||||||
// 7. keepalived reload
|
// 7. keepalived reload
|
||||||
func cmdPromote(args []string) int {
|
func cmdPromote(args []string) int {
|
||||||
|
pg, err := detectPGConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "promote: PG-Erkennung:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Standby-Signal prüfen
|
// 1. Standby-Signal prüfen
|
||||||
signalPath := filepath.Join(pgDataDir, "standby.signal")
|
signalPath := filepath.Join(pg.DataDir, "standby.signal")
|
||||||
if _, err := os.Stat(signalPath); os.IsNotExist(err) {
|
if _, err := os.Stat(signalPath); os.IsNotExist(err) {
|
||||||
fmt.Fprintf(os.Stderr,
|
fmt.Fprintf(os.Stderr,
|
||||||
"promote: %s nicht gefunden — diese Node ist kein PG-Standby oder wurde bereits promoted.\n",
|
"promote: %s nicht gefunden — diese Node ist kein PG-Standby oder wurde bereits promoted.\n",
|
||||||
@@ -36,8 +42,8 @@ func cmdPromote(args []string) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("→ Promoting PostgreSQL zu Primary...")
|
fmt.Printf("→ Promoting PostgreSQL %s/%s zu Primary...\n", pg.Version, pg.Cluster)
|
||||||
if out, err := exec.Command("pg_ctlcluster", pgVersion, pgCluster, "promote").
|
if out, err := exec.Command("pg_ctlcluster", pg.Version, pg.Cluster, "promote").
|
||||||
CombinedOutput(); err != nil {
|
CombinedOutput(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "promote: pg_ctlcluster promote: %v\n%s\n", err, out)
|
fmt.Fprintf(os.Stderr, "promote: pg_ctlcluster promote: %v\n%s\n", err, out)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.2.3"
|
var version = "1.2.15"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ table inet edgeguard {
|
|||||||
# Cluster-internal: peers reach edgeguard-api over mTLS on :8443
|
# Cluster-internal: peers reach edgeguard-api over mTLS on :8443
|
||||||
tcp dport 8443 ip saddr @peer_ipv4 accept
|
tcp dport 8443 ip saddr @peer_ipv4 accept
|
||||||
tcp dport 8443 ip6 saddr @peer_ipv6 accept
|
tcp dport 8443 ip6 saddr @peer_ipv6 accept
|
||||||
|
# Cluster-internal: PG Logical Replication (:5432) + KeyDB Active-Active (:6379)
|
||||||
|
tcp dport 5432 ip saddr @peer_ipv4 accept
|
||||||
|
tcp dport 5432 ip6 saddr @peer_ipv6 accept
|
||||||
|
tcp dport 6379 ip saddr @peer_ipv4 accept
|
||||||
|
tcp dport 6379 ip6 saddr @peer_ipv6 accept
|
||||||
|
|
||||||
# ── Service-Auto-Rules (DNS/Squid/WG/...) ──
|
# ── Service-Auto-Rules (DNS/Squid/WG/...) ──
|
||||||
# Aus dem laufenden Service-State abgeleitet — Operator
|
# Aus dem laufenden Service-State abgeleitet — Operator
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ func (h *ClusterHandler) RegisterAgent(rg *gin.RouterGroup) {
|
|||||||
g.POST("/peers", h.AgentRegisterPeer)
|
g.POST("/peers", h.AgentRegisterPeer)
|
||||||
g.GET("/identity", h.AgentIdentity)
|
g.GET("/identity", h.AgentIdentity)
|
||||||
g.GET("/pg-replication-info", h.AgentPGReplicationInfo)
|
g.GET("/pg-replication-info", h.AgentPGReplicationInfo)
|
||||||
|
g.GET("/master-key", h.AgentMasterKey)
|
||||||
g.GET("/version", h.AgentVersion)
|
g.GET("/version", h.AgentVersion)
|
||||||
g.POST("/trigger-update", h.AgentTriggerUpdate)
|
g.POST("/trigger-update", h.AgentTriggerUpdate)
|
||||||
}
|
}
|
||||||
@@ -270,6 +271,20 @@ func (h *ClusterHandler) AgentPGReplicationInfo(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AgentMasterKey gibt den Secrets-Master-Key zurück, damit cluster-setup-standby
|
||||||
|
// ihn auf dem Secondary synchronisieren kann. Nur über den mTLS-Agent-Listener
|
||||||
|
// erreichbar. Ohne gemeinsamen Master-Key können replizierte verschlüsselte
|
||||||
|
// Felder (WireGuard private keys, PSKs) auf dem Secondary nicht entschlüsselt werden.
|
||||||
|
func (h *ClusterHandler) AgentMasterKey(c *gin.Context) {
|
||||||
|
const keyPath = "/var/lib/edgeguard/.master_key"
|
||||||
|
data, err := os.ReadFile(keyPath)
|
||||||
|
if err != nil {
|
||||||
|
response.NotFound(c, simpleError("master key nicht gefunden"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, gin.H{"key_hex": fmt.Sprintf("%x", data)})
|
||||||
|
}
|
||||||
|
|
||||||
func readFileString(path string) (string, error) {
|
func readFileString(path string) (string, error) {
|
||||||
b, err := os.ReadFile(path)
|
b, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -719,7 +734,7 @@ rm -f /var/lib/edgeguard/upgrade.sh
|
|||||||
_ = exec.Command("sudo", "-n", "/usr/bin/systemctl", "reset-failed", unitName).Run()
|
_ = exec.Command("sudo", "-n", "/usr/bin/systemctl", "reset-failed", unitName).Run()
|
||||||
cmd := exec.Command("sudo", "-n", "/usr/bin/systemd-run",
|
cmd := exec.Command("sudo", "-n", "/usr/bin/systemd-run",
|
||||||
"--unit="+unitName,
|
"--unit="+unitName,
|
||||||
"--description=EdgeGuard rolling-update (triggered by primary)",
|
"--description=EdgeGuard self-upgrade",
|
||||||
"--collect",
|
"--collect",
|
||||||
"bash", scriptPath)
|
"bash", scriptPath)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user