- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung - keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload) - confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix) - TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login - Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator - fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen - VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034) - Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032) - unbound-control: edgeguard in unbound-Gruppe via postinst Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
119 lines
3.2 KiB
Go
119 lines
3.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/aggregator"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
|
)
|
|
|
|
const tlsCertDir = "/etc/edgeguard/tls"
|
|
|
|
// AgentTLSCerts liefert alle .pem-Dateien aus /etc/edgeguard/tls/ als
|
|
// Base64-Map. Wird vom Secondary via mTLS aufgerufen um Zertifikate
|
|
// des Primary zu spiegeln.
|
|
func (h *ClusterHandler) AgentTLSCerts(c *gin.Context) {
|
|
entries, err := os.ReadDir(tlsCertDir)
|
|
if err != nil {
|
|
response.Internal(c, err)
|
|
return
|
|
}
|
|
certs := make(map[string]string, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".pem") {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(tlsCertDir, e.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
certs[e.Name()] = base64.StdEncoding.EncodeToString(data)
|
|
}
|
|
response.OK(c, gin.H{"certs": certs})
|
|
}
|
|
|
|
// SyncTLSCertsFromPrimary holt alle TLS-Zertifikate vom Primary via mTLS
|
|
// und schreibt geänderte Dateien nach /etc/edgeguard/tls/. Relädt HAProxy
|
|
// wenn mindestens ein Zertifikat aktualisiert wurde.
|
|
//
|
|
// Läuft auf dem Secondary bei jedem runSecondaryConfigRender-Tick —
|
|
// nicht hash-gated, da certbot-Renewals den config_hash nicht ändern.
|
|
func SyncTLSCertsFromPrimary(ctx context.Context, pool *pgxpool.Pool, agg *aggregator.Aggregator, localID string) error {
|
|
if agg == nil {
|
|
return nil
|
|
}
|
|
// Primary-Peer aus ha_nodes ermitteln
|
|
rows, err := pool.Query(ctx,
|
|
`SELECT id, fqdn, api_url FROM ha_nodes WHERE id != $1 LIMIT 1`, localID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
var primary *models.HANode
|
|
for rows.Next() {
|
|
n := &models.HANode{}
|
|
if err := rows.Scan(&n.ID, &n.FQDN, &n.APIURL); err != nil {
|
|
continue
|
|
}
|
|
primary = n
|
|
}
|
|
if primary == nil {
|
|
return nil // kein Peer → Single-Node
|
|
}
|
|
|
|
results := agg.FanOut(ctx, []models.HANode{*primary}, "/agent/cluster/tls-certs", localID)
|
|
if len(results) == 0 || !results[0].OK {
|
|
return nil // Primary nicht erreichbar — nächster Tick
|
|
}
|
|
|
|
var payload struct {
|
|
Certs map[string]string `json:"certs"`
|
|
}
|
|
if err := json.Unmarshal(results[0].Data, &payload); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.MkdirAll(tlsCertDir, 0o750); err != nil {
|
|
return err
|
|
}
|
|
|
|
changed := false
|
|
for name, b64 := range payload.Certs {
|
|
data, err := base64.StdEncoding.DecodeString(b64)
|
|
if err != nil {
|
|
slog.Warn("cert-sync: base64 decode failed", "file", name, "error", err)
|
|
continue
|
|
}
|
|
path := filepath.Join(tlsCertDir, name)
|
|
existing, readErr := os.ReadFile(path)
|
|
if readErr == nil && bytes.Equal(existing, data) {
|
|
continue // unverändert
|
|
}
|
|
if err := os.WriteFile(path, data, 0o640); err != nil {
|
|
slog.Warn("cert-sync: write failed", "file", name, "error", err)
|
|
continue
|
|
}
|
|
changed = true
|
|
slog.Info("cert-sync: updated", "file", name)
|
|
}
|
|
|
|
if changed {
|
|
if err := exec.Command("sudo", "-n", "/usr/bin/systemctl", "reload", "haproxy.service").Run(); err != nil {
|
|
slog.Warn("cert-sync: haproxy reload failed", "error", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|