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:
Debian
2026-06-06 10:55:21 +02:00
parent 5f92851a96
commit df31bfa720
22 changed files with 338 additions and 97 deletions

View File

@@ -3,6 +3,7 @@ package waf
import (
"fmt"
"log/slog"
"net"
"sync"
"github.com/corazawaf/coraza/v3"
@@ -12,8 +13,9 @@ import (
// DomainEngine bundles a Coraza WAF with its operating mode.
type DomainEngine struct {
WAF coraza.WAF
Mode string // "detection" | "blocking"
WAF coraza.WAF
Mode string // "detection" | "blocking"
TrustedProxies []string // wenn src ∈ diese → echte Client-IP aus X-Forwarded-For
}
// Manager holds per-domain Coraza engine instances. Engines are
@@ -90,7 +92,7 @@ func (m *Manager) Reload(domains []DomainConfig) error {
if err != nil {
return fmt.Errorf("waf: build engine for %s: %w", dc.Hostname, err)
}
newEngines[dc.Hostname] = &DomainEngine{WAF: waf, Mode: dc.Config.Mode}
newEngines[dc.Hostname] = &DomainEngine{WAF: waf, Mode: dc.Config.Mode, TrustedProxies: dc.Config.TrustedProxies}
slog.Info("waf: engine (re)loaded",
"host", dc.Hostname,
"mode", dc.Config.Mode,
@@ -110,8 +112,9 @@ func (m *Manager) Reload(domains []DomainConfig) error {
// (nil, false) when the domain has no WAF or WAF is disabled.
func (m *Manager) GetForHost(host string) (*DomainEngine, bool) {
// Strip port if present (e.g. "example.com:443" → "example.com").
if i := lastColon(host); i >= 0 {
host = host[:i]
// SplitHostPort errors for a bare host or bare IPv6 literal → keep as-is.
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
m.mu.RLock()
de, ok := m.engines[host]
@@ -122,36 +125,3 @@ func (m *Manager) GetForHost(host string) (*DomainEngine, bool) {
return de, true
}
// lastColon returns the index of the last ':' in s that looks like a
// port separator (after the final ']' for IPv6), or -1.
func lastColon(s string) int {
// IPv6 addresses in brackets: "[::1]:443"
if len(s) > 0 && s[0] == '[' {
if rb := lastByte(s, ']'); rb >= 0 && rb < len(s)-1 && s[rb+1] == ':' {
return rb + 1
}
return -1
}
// Plain host — only strip port if there's exactly one colon.
count := 0
idx := -1
for i, c := range s {
if c == ':' {
count++
idx = i
}
}
if count == 1 {
return idx
}
return -1
}
func lastByte(s string, b byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == b {
return i
}
}
return -1
}