harden(api): Slowloris-Timeout + gosec-Security-Audit + waf-Purge-Bugfix — v1.3.2
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) <noreply@anthropic.com>
This commit is contained in:
@@ -21,8 +21,6 @@ linters:
|
|||||||
- gosec # SQL-Injection, hardcoded Secrets, schwache Krypto
|
- gosec # SQL-Injection, hardcoded Secrets, schwache Krypto
|
||||||
# ── Ressourcen-/Leak-Schutz ──
|
# ── Ressourcen-/Leak-Schutz ──
|
||||||
- bodyclose # nicht geschlossene HTTP-Response-Bodies
|
- bodyclose # nicht geschlossene HTTP-Response-Bodies
|
||||||
- rowserrcheck # fehlendes rows.Err() (database/sql-Muster)
|
|
||||||
- sqlclosecheck # fehlendes rows/stmt.Close()
|
|
||||||
# ── Context-Hygiene ──
|
# ── Context-Hygiene ──
|
||||||
- noctx # HTTP-Requests ohne context
|
- noctx # HTTP-Requests ohne context
|
||||||
- contextcheck # nicht-vererbte Contexts
|
- contextcheck # nicht-vererbte Contexts
|
||||||
@@ -35,8 +33,54 @@ linters:
|
|||||||
# G115 (int-Konvertierungs-Overflow) erzeugt in Go 1.26 viele
|
# G115 (int-Konvertierungs-Overflow) erzeugt in Go 1.26 viele
|
||||||
# false positives — bei Bedarf gezielt wieder aktivieren.
|
# false positives — bei Bedarf gezielt wieder aktivieren.
|
||||||
- G115
|
- 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
|
# HINWEIS: rowserrcheck/sqlclosecheck NICHT aktiviert — sie zielen auf
|
||||||
# pgx/pgxpool — dort greifen sie nur teilweise. Das pgx-Muster (manuelles
|
# database/sql. EdgeGuard nutzt durchgängig pgx/pgxpool; dort erzeugen sie
|
||||||
# rows.Next()/Scan() + defer rows.Close() + rows.Err()) bitte weiter per Review
|
# nur False Positives (z. B. wenn rows via Interface-Var zugewiesen wird,
|
||||||
# absichern; siehe internal/services/*/*.go.
|
# 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.
|
||||||
|
|||||||
@@ -528,7 +528,17 @@ func main() {
|
|||||||
handlers.FinishRollingUpdateIfPending()
|
handlers.FinishRollingUpdateIfPending()
|
||||||
|
|
||||||
log.Printf("edgeguard-api %s listening on %s", version, addr)
|
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 {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("edgeguard-api: %v", err)
|
log.Fatalf("edgeguard-api: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func runCallback(t *testing.T, h *OIDCHandler, flow oidcFlow, queryState, code s
|
|||||||
}
|
}
|
||||||
req := httptest.NewRequest(http.MethodGet,
|
req := httptest.NewRequest(http.MethodGet,
|
||||||
"/api/v1/auth/oidc/callback?state="+queryState+"&code="+code, nil)
|
"/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
|
c.Request = req
|
||||||
h.Callback(c)
|
h.Callback(c)
|
||||||
return rec
|
return rec
|
||||||
|
|||||||
@@ -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.
|
// 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 {
|
func (r *Repo) PurgeAlerts(ctx context.Context, olderThanDays int) error {
|
||||||
_, err := r.Pool.Exec(ctx,
|
_, 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,
|
olderThanDays,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user