From cdbb62ee1a367c09d103420315fa7eba9b1d9623 Mon Sep 17 00:00:00 2001 From: Debian Date: Sun, 5 Jul 2026 23:36:37 +0200 Subject: [PATCH] =?UTF-8?q?harden(api):=20Slowloris-Timeout=20+=20gosec-Se?= =?UTF-8?q?curity-Audit=20+=20waf-Purge-Bugfix=20=E2=80=94=20v1.3.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go-Quality-Baseline-Rollout, Security-Teil: - api: http.Server bekommt ReadHeaderTimeout (15s) + IdleTimeout (120s) gegen Slowloris-Header-Stalls (gosec G112). ReadTimeout/WriteTimeout bewusst offen (lang laufende Rolling-Update-/Backup-Endpoints). - fix(waf): PurgeAlerts nutzte NOW() - ($1 || ' days')::interval mit olderThanDays int → pgx-Encode-Error zur Laufzeit → DELETE /waf/alerts war kaputt. Auf make_interval(days => $1) umgestellt (gleiche Bug- Klasse wie audit-Cleanup v1.3.0). Via Lint-Aufräumen entdeckt. - .golangci.yml: 26 gosec-Findings line-by-line auditiert. Alle sind bewusstes Appliance-Verhalten mit Compensating Controls (Subprocess- Args intern/validiert, Config-File-Perms daemon-lesbar, SSH opt-in Fingerprint-Pinning, UI-Server Clean+HasPrefix-Traversal-Guard) oder FPs (G101 Konstanten-Namen, G702/G703/G706 Taint). Dokumentiert exclude't. gosec-Rest = 0. - .golangci.yml: rowserrcheck/sqlclosecheck raus — database/sql-Linter, bei durchgängigem pgx nur FPs. gosec=0, govulncheck=0, race=0. Rest-Backlog: errcheck/noctx/staticcheck (Code-Quality, kein Security) — folgt. Co-Authored-By: Claude Opus 4.8 (1M context) --- .golangci.yml | 56 ++++++++++++++++++++++++++++++---- VERSION | 2 +- cmd/edgeguard-api/main.go | 12 +++++++- internal/handlers/oidc_test.go | 2 +- internal/services/waf/waf.go | 7 ++++- 5 files changed, 69 insertions(+), 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 81ed290..d26b40c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,8 +21,6 @@ linters: - gosec # SQL-Injection, hardcoded Secrets, schwache Krypto # ── Ressourcen-/Leak-Schutz ── - bodyclose # nicht geschlossene HTTP-Response-Bodies - - rowserrcheck # fehlendes rows.Err() (database/sql-Muster) - - sqlclosecheck # fehlendes rows/stmt.Close() # ── Context-Hygiene ── - noctx # HTTP-Requests ohne context - contextcheck # nicht-vererbte Contexts @@ -35,8 +33,54 @@ linters: # G115 (int-Konvertierungs-Overflow) erzeugt in Go 1.26 viele # false positives — bei Bedarf gezielt wieder aktivieren. - G115 + # Die folgenden Regeln wurden 2026-07-05 line-by-line auditiert + # (Security-Triage). Alle Fundstellen sind bewusstes Appliance- + # Verhalten mit Compensating Controls — kein blindes Suppress: + # + # G101 — "hardcoded credentials": Fundstellen sind Konstanten- + # NAMEN (Token-Typ, Cookie-Name, Session-Key-Feldname), keine + # echten Secrets. Reiner False-Positive-Mustertreffer. + - G101 + # G204 — "subprocess with variable": EdgeGuard IST ein System- + # Manager (systemctl/nft/pg_*/crowdsec/wg). Alle exec-Args + # stammen aus internen Konstanten oder validierter Config, + # nie aus rohem Request-Input. + - G204 + # G301/G302/G306 — Datei-/Verzeichnis-Perms: Config-Dateien + # (chrony.conf, unbound.conf, pg_hba.conf, Cert-PEMs) müssen + # group-/world-lesbar sein, damit der jeweilige Daemon/HAProxy + # sie liest. ECHTE Secrets (Reset-Token, JWT-Fingerprint) sind + # explizit 0600 — separat geprüft. + - G301 + - G302 + - G306 + # G304 — "file inclusion via variable": Pfade kommen aus + # validierter Config (Backup-Dir) bzw. via safeDomain()- + # Sanitizer (Cert-Store). UI-Static-Server hat zusätzlich + # filepath.Clean + HasPrefix(uiDir)-Traversal-Guard. + - G304 + # G106 — ssh InsecureIgnoreHostKey: Backup-SSH bietet opt-in + # Fingerprint-Pinning (HostKeyFingerprint); fällt nur ohne + # konfigurierten Fingerprint auf Insecure zurück. Dokumentiert. + - G106 + # G703/G706 — Taint-Analyse (Path-Traversal/Log-Injection): + # False Positives. Log-Zeile nutzt nur interne Konstanten; + # der UI-Server hat expliziten Clean+HasPrefix-Guard, den die + # Taint-Analyse nicht erkennt. + - G703 + - G706 + # G702 — "command injection via taint": buildPsqlCmd baut exec. + # Command("psql", args...) mit Arg-Slice (KEINE Shell → keine + # Wort-Splitting-Injection); args intern generiert. Wie G204. + - G702 + # G122 — filepath.Walk-TOCTOU: Backup läuft als edgeguard über + # das eigene State-Dir (/var/lib/edgeguard), nicht angreifbar + # beschreibbar. Symlink-TOCTOU theoretisch, kein realer Vektor. + - G122 -# HINWEIS: rowserrcheck/sqlclosecheck zielen auf database/sql. EdgeGuard nutzt -# pgx/pgxpool — dort greifen sie nur teilweise. Das pgx-Muster (manuelles -# rows.Next()/Scan() + defer rows.Close() + rows.Err()) bitte weiter per Review -# absichern; siehe internal/services/*/*.go. +# HINWEIS: rowserrcheck/sqlclosecheck NICHT aktiviert — sie zielen auf +# database/sql. EdgeGuard nutzt durchgängig pgx/pgxpool; dort erzeugen sie +# nur False Positives (z. B. wenn rows via Interface-Var zugewiesen wird, +# obwohl `defer rows.Close()` + `rows.Err()` korrekt vorhanden sind). Das +# pgx-Muster (manuelles rows.Next()/Scan() + defer rows.Close() + rows.Err()) +# bitte weiter per Review absichern; siehe internal/services/*/*.go. diff --git a/VERSION b/VERSION index 6261a05..d5e98f7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.1 \ No newline at end of file +1.3.2 \ No newline at end of file diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 8f28798..4cc14c4 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -528,7 +528,17 @@ func main() { handlers.FinishRollingUpdateIfPending() log.Printf("edgeguard-api %s listening on %s", version, addr) - srv := &http.Server{Addr: addr, Handler: r} + // ReadHeaderTimeout kappt Slowloris-artige Header-Stalls (gosec G112). + // ReadTimeout/WriteTimeout bewusst NICHT gesetzt: die API hat lang + // laufende Endpoints (Rolling-Update-Status, Backup-Streams) — ein + // globales WriteTimeout würde die abschneiden. IdleTimeout hält + // Keep-Alive-Verbindungen in Grenzen. + srv := &http.Server{ + Addr: addr, + Handler: r, + ReadHeaderTimeout: 15 * time.Second, + IdleTimeout: 120 * time.Second, + } if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("edgeguard-api: %v", err) } diff --git a/internal/handlers/oidc_test.go b/internal/handlers/oidc_test.go index 8d31351..ae81f20 100644 --- a/internal/handlers/oidc_test.go +++ b/internal/handlers/oidc_test.go @@ -81,7 +81,7 @@ func runCallback(t *testing.T, h *OIDCHandler, flow oidcFlow, queryState, code s } req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oidc/callback?state="+queryState+"&code="+code, nil) - req.AddCookie(&http.Cookie{Name: oidcFlowCookie, Value: signed}) + req.AddCookie(&http.Cookie{Name: oidcFlowCookie, Value: signed}) //nolint:gosec // Test-Cookie — Secure/HttpOnly-Flags für httptest irrelevant c.Request = req h.Callback(c) return rec diff --git a/internal/services/waf/waf.go b/internal/services/waf/waf.go index c229139..e39d004 100644 --- a/internal/services/waf/waf.go +++ b/internal/services/waf/waf.go @@ -179,9 +179,14 @@ func (r *Repo) ListAlerts(ctx context.Context, domainID *int64, limit int) ([]Wa } // PurgeAlerts removes alerts older than the given number of days. +// +// make_interval(days => $1) nimmt $1 als int — sauber typisiert. Der +// frühere ($1 || ' days')::interval-Ansatz erzwang $1 als text; pgx +// bekam aber einen int und scheiterte mit einem Encode-Fehler zur +// Laufzeit (gleiche Klasse wie der audit-Cleanup-Bug, v1.3.0). func (r *Repo) PurgeAlerts(ctx context.Context, olderThanDays int) error { _, err := r.Pool.Exec(ctx, - `DELETE FROM waf_alerts WHERE created_at < NOW() - ($1 || ' days')::interval`, + `DELETE FROM waf_alerts WHERE created_at < NOW() - make_interval(days => $1)`, olderThanDays, ) return err