fix: Audit-Bugfixes (Auth/WAF/Firewall/Cluster/Renderer) — v1.2.94
Verifizierte Bugs aus dem Code-Audit behoben (je mit Test/Build/nft -c geprüft):
- session: IssueWithRoleTTL mutierte geteiltes s.TTL (Data-Race + falsche TTL) → interne issue(); -race-Test.
- auth: Fallback/Federation leiteten role/TOTP nicht aus DB ab (2FA-Bypass auf Secondary, Rolle aus Remote) → viaDB-Flag + DB-Re-Lookup.
- waf: TrustedProxies waren No-op (bogus-Direktive) → XFF-Auflösung im SPOE-Agent (rightmostXFF/ipMatchesAny); RuleExclusions/TrustedProxies validiert (Direktiven-Injection); GetForHost via net.SplitHostPort.
- firewall: Auto-Rule mit IPv6-DstIP erzeugte 'ip daddr <v6>' → bricht ganzes nft-Ruleset; jetzt familienbewusst (ip/ip6, ungültige raus).
- kea: 'interfaces': null bei 0 Subnets → leeres Array.
- cluster_repair: nodeHasPublication schluckte DB-Fehler (Resync auf falschem Node) → (bool,error) fail-closed; IPv6-Primary-URL via net.JoinHostPort.
- cluster_replication: Replikations-Passwort via stdin statt psql -c (nicht mehr in argv/Logs).
- wireguard: Config (Private Key) jetzt configgen.AtomicWrite VOR Symlink/enable; SkipReload-Feld.
- render.go: --no-reload jetzt für alle Renderer (squid/unbound/chrony/wireguard).
- radius: leeres Secret/Passwort + Newlines abgelehnt; freeradius confEscape strippt CR/LF.
- configorch: continue-on-error + errors.Join statt Abbruch mitten in der Sequenz.
- i18n: fehlender Key common.status (de/en).
Verworfen als kein Bug: WAF detection-'blocked' (DetectionOnly liefert keine Interruption), render secrets.New('') (nutzt Default-Masterkey), FanOut-Sort (nur Kommentar), pg_hba (durch nft abgesichert).
Offen/bewusst zurückgestellt (low/risk): AlertWriter-Close (langlebiger Worker, vernachlässigbar), Rolling-Update-Kleinkram (sudoers-gebundener Script-Pfad / GET-State).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,14 @@ func (h *ClusterHandler) RepairReplication(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.nodeHasPublication(ctx) {
|
||||
isPrimary, err := h.nodeHasPublication(ctx)
|
||||
if err != nil {
|
||||
// Primary/Subscriber-Status nicht ermittelbar → NICHT raten
|
||||
// (sonst Resync auf dem falschen Node). Abbrechen.
|
||||
response.Internal(c, fmt.Errorf("primary-status nicht ermittelbar: %w", err))
|
||||
return
|
||||
}
|
||||
if isPrimary {
|
||||
// Primary → an den Subscriber-Peer delegieren, mit eigener Adresse.
|
||||
if h.Aggregator == nil {
|
||||
response.BadRequest(c, errors.New("kein mTLS-Aggregator verfügbar — Resync nicht delegierbar"))
|
||||
@@ -162,7 +169,12 @@ func (h *ClusterHandler) startResync(ctx context.Context, primaryHost string) er
|
||||
}
|
||||
// Niemals auf dem Primary (Publication-Quelle) resyncen — würde die
|
||||
// eigene Config mit sich selbst überschreiben bzw. ist sinnlos.
|
||||
if h.nodeHasPublication(ctx) {
|
||||
// Bei Statusfehler fail-closed (NICHT resyncen).
|
||||
isPrimary, err := h.nodeHasPublication(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("publication-status nicht ermittelbar: %w", err)
|
||||
}
|
||||
if isPrimary {
|
||||
return errors.New("dieser Node ist der Publication-Primary — Resync läuft nur auf einem Subscriber")
|
||||
}
|
||||
if st := repairUnitState(); st == "activating" || st == "active" {
|
||||
@@ -201,9 +213,9 @@ rm -f %[2]s
|
||||
// nodeHasPublication prüft, ob dieser Node die Replikations-Publication
|
||||
// besitzt — das verlässliche Primary-Signal. pg_publication ist für jeden
|
||||
// DB-User lesbar (anders als pg_subscription).
|
||||
func (h *ClusterHandler) nodeHasPublication(ctx context.Context) bool {
|
||||
func (h *ClusterHandler) nodeHasPublication(ctx context.Context) (bool, error) {
|
||||
if h.Store == nil || h.Store.Pool == nil {
|
||||
return false
|
||||
return false, errors.New("no db pool")
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
@@ -211,9 +223,9 @@ func (h *ClusterHandler) nodeHasPublication(ctx context.Context) bool {
|
||||
if err := h.Store.Pool.QueryRow(cctx,
|
||||
`SELECT EXISTS(SELECT 1 FROM pg_publication WHERE pubname = $1)`, repairPubName,
|
||||
).Scan(&exists); err != nil {
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
return exists
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// repairStatusResponse spiegelt den Zustand der transienten Repair-Unit.
|
||||
@@ -231,7 +243,9 @@ type repairStatusResponse struct {
|
||||
// Status vom Subscriber-Peer geholt (dort läuft der Job); sonst lokal.
|
||||
func (h *ClusterHandler) RepairReplicationStatus(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if h.Store != nil && h.nodeHasPublication(ctx) && h.Aggregator != nil {
|
||||
// Status-Poll: bei Fehler kein 500 — einfach lokalen Status liefern.
|
||||
isPrimary, _ := h.nodeHasPublication(ctx)
|
||||
if h.Store != nil && isPrimary && h.Aggregator != nil {
|
||||
if all, err := h.Store.List(ctx); err == nil {
|
||||
if peer := findOtherPeer(all, h.LocalID); peer != nil {
|
||||
results := h.Aggregator.FanOut(ctx,
|
||||
|
||||
Reference in New Issue
Block a user