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:
@@ -44,6 +44,7 @@ func Run(ctx context.Context, gens []configgen.Generator, only []string) ([]Resu
|
||||
whitelist[n] = true
|
||||
}
|
||||
out := make([]Result, 0, len(gens))
|
||||
var errs []error
|
||||
for _, g := range gens {
|
||||
if len(whitelist) > 0 && !whitelist[g.Name()] {
|
||||
out = append(out, Result{Name: g.Name(), Skipped: true})
|
||||
@@ -52,11 +53,14 @@ func Run(ctx context.Context, gens []configgen.Generator, only []string) ([]Resu
|
||||
err := g.Render(ctx)
|
||||
out = append(out, Result{Name: g.Name(), Err: err})
|
||||
if err != nil && !errors.Is(err, configgen.ErrNotImplemented) {
|
||||
// hard failure — surface it but return what's done so far
|
||||
return out, fmt.Errorf("%s: %w", g.Name(), err)
|
||||
// Weitermachen: die Generatoren sind unabhängig und reloaden
|
||||
// inline (nft/Service-Reload sind atomar). Abbrechen würde die
|
||||
// restlichen Dienste auf altem Stand lassen → halb angewandt.
|
||||
// Stattdessen alle versuchen und Fehler gesammelt zurückgeben.
|
||||
errs = append(errs, fmt.Errorf("%s: %w", g.Name(), err))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
return out, errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Summarise turns the result slice into a human-readable multiline
|
||||
|
||||
@@ -96,14 +96,15 @@ func loadOrCreateSecret(path string) ([]byte, error) {
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// IssueWithRole returns a signed token for the given actor + role.
|
||||
func (s *Signer) IssueWithRole(actor, role string) (string, *Token, error) {
|
||||
// issue builds + signs a token with an explicit TTL. No shared-state
|
||||
// mutation — safe for concurrent use of the shared Signer singleton.
|
||||
func (s *Signer) issue(actor, role string, ttl time.Duration) (string, *Token, error) {
|
||||
now := s.Now()
|
||||
t := Token{
|
||||
Actor: actor,
|
||||
Role: role,
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(s.TTL).Unix(),
|
||||
Exp: now.Add(ttl).Unix(),
|
||||
}
|
||||
data, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
@@ -117,18 +118,20 @@ func (s *Signer) IssueWithRole(actor, role string) (string, *Token, error) {
|
||||
return encoded, &t, nil
|
||||
}
|
||||
|
||||
// Issue is IssueWithRole with empty role.
|
||||
func (s *Signer) Issue(actor string) (string, *Token, error) {
|
||||
return s.IssueWithRole(actor, "")
|
||||
// IssueWithRole returns a signed token for the given actor + role.
|
||||
func (s *Signer) IssueWithRole(actor, role string) (string, *Token, error) {
|
||||
return s.issue(actor, role, s.TTL)
|
||||
}
|
||||
|
||||
// IssueWithRoleTTL issues a token with a custom TTL (overrides s.TTL for this call).
|
||||
// Issue is IssueWithRole with empty role.
|
||||
func (s *Signer) Issue(actor string) (string, *Token, error) {
|
||||
return s.issue(actor, "", s.TTL)
|
||||
}
|
||||
|
||||
// IssueWithRoleTTL issues a token with a custom TTL — no longer mutates
|
||||
// the shared Signer (previously a data race under concurrent logins).
|
||||
func (s *Signer) IssueWithRoleTTL(actor, role string, ttl time.Duration) (string, *Token, error) {
|
||||
orig := s.TTL
|
||||
s.TTL = ttl
|
||||
raw, tok, err := s.IssueWithRole(actor, role)
|
||||
s.TTL = orig
|
||||
return raw, tok, err
|
||||
return s.issue(actor, role, ttl)
|
||||
}
|
||||
|
||||
// Verify checks a token. Returns ErrInvalidToken or ErrExpiredToken.
|
||||
|
||||
47
internal/services/session/ttl_test.go
Normal file
47
internal/services/session/ttl_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestSigner_TTLNotShared beweist Fix #1: IssueWithRoleTTL darf das geteilte
|
||||
// s.TTL nicht mehr mutieren. Unter `go test -race` schlägt die alte Version
|
||||
// als Data-Race an; zusätzlich prüfen wir, dass parallele normale Logins nie
|
||||
// die kurze TOTP-TTL erben.
|
||||
func TestSigner_TTLNotShared(t *testing.T) {
|
||||
s := NewSigner([]byte("0123456789abcdef0123456789abcdef"), nil, time.Hour)
|
||||
var wg sync.WaitGroup
|
||||
var bad int32
|
||||
for i := 0; i < 200; i++ {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _, _ = s.IssueWithRoleTTL("a", "totp_pending", 2*time.Minute)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, tok, err := s.IssueWithRole("b", "admin")
|
||||
if err != nil {
|
||||
atomic.AddInt32(&bad, 1)
|
||||
return
|
||||
}
|
||||
// Normale Session muss ~1h gelten, nie die 2-Min-TOTP-TTL.
|
||||
if tok.Exp-tok.Iat < int64((30 * time.Minute).Seconds()) {
|
||||
atomic.AddInt32(&bad, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if bad > 0 {
|
||||
t.Fatalf("%d normale Tokens bekamen eine zu kurze TTL → geteilter Zustand", bad)
|
||||
}
|
||||
|
||||
// TTL-Override wirkt weiterhin korrekt für den TOTP-Token.
|
||||
_, ptok, _ := s.IssueWithRoleTTL("x", "totp_pending", 2*time.Minute)
|
||||
if d := ptok.Exp - ptok.Iat; d > int64((3 * time.Minute).Seconds()) {
|
||||
t.Fatalf("totp-pending TTL = %ds, want ~120s", d)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user