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

@@ -19,6 +19,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/secrets"
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
@@ -27,10 +28,11 @@ import (
const ConfDir = "/etc/edgeguard/wireguard"
type Generator struct {
Pool *pgxpool.Pool
Box *secrets.Box
Ifaces *wgsvc.InterfacesRepo
Peers *wgsvc.PeersRepo
Pool *pgxpool.Pool
Box *secrets.Box
Ifaces *wgsvc.InterfacesRepo
Peers *wgsvc.PeersRepo
SkipReload bool // nur Configs schreiben, keine wg-quick@-Service-Aktionen
}
func New(pool *pgxpool.Pool, box *secrets.Box) *Generator {
@@ -151,8 +153,10 @@ func (g *Generator) Render(ctx context.Context) error {
continue
}
_ = os.Remove(filepath.Join(ConfDir, e.Name()))
_ = stopWGQuick(ifaceName)
_ = disableWGQuick(ifaceName)
if !g.SkipReload {
_ = stopWGQuick(ifaceName)
_ = disableWGQuick(ifaceName)
}
}
}
return nil
@@ -228,21 +232,31 @@ func (g *Generator) renderIface(ctx context.Context, ifc models.WireguardInterfa
}
path := filepath.Join(ConfDir, ifc.Name+".conf")
// Config (enthält den Private Key) ZUERST atomar schreiben — vorher
// keinen Symlink/Service auf eine evtl. fehlende/abgeschnittene Datei
// zeigen lassen. AtomicWrite = temp+fsync+rename, 0600.
changed := true
if existing, err := os.ReadFile(path); err == nil && bytes.Equal(existing, body.Bytes()) {
changed = false
}
if changed {
if err := configgen.AtomicWrite(path, body.Bytes(), 0o600); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
}
if g.SkipReload {
return nil
}
// wg-quick@<iface>.service liest /etc/wireguard/<iface>.conf (Distro-
// Default), nicht unseren ConfDir. Wir lassen die Quelle of truth in
// /etc/edgeguard/wireguard/ und symlinken via sudo — /etc/wireguard/
// ist root:root 700, daher braucht es sudo /bin/ln. Das sudoers-Entry
// wird von postinst angelegt.
// Default), nicht unseren ConfDir. Symlink via sudo (/etc/wireguard/
// ist root:root 700). Das sudoers-Entry wird von postinst angelegt.
if err := symlinkWGQuickConf(ifc.Name, path); err != nil {
return fmt.Errorf("symlink: %w", err)
}
_ = enableWGQuick(ifc.Name)
if existing, err := os.ReadFile(path); err == nil && bytes.Equal(existing, body.Bytes()) {
if !changed {
return startWGQuick(ifc.Name)
}
if err := os.WriteFile(path, body.Bytes(), 0o600); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return restartWGQuick(ifc.Name)
}