diff --git a/internal/api/admin_security_handlers.go b/internal/api/admin_security_handlers.go index 49721aa..c30899a 100644 --- a/internal/api/admin_security_handlers.go +++ b/internal/api/admin_security_handlers.go @@ -6,12 +6,39 @@ import ( "net/http" "os" "os/exec" + "regexp" "strings" "time" ) // ── Security Audit ────────────────────────────────────────────────────────── +// nftDportRe matches "dport 443" as well as the set form nft uses for +// multiple ports on one rule, e.g. "dport { 80, 443 }". +var nftDportRe = regexp.MustCompile(`dport\s+(\{[^}]*\}|\d+)`) + +// nftHasPort reports whether nft's "list ruleset" output opens the given +// port, either as a standalone "dport " rule or as a member of a +// "dport { ... }" set. A plain strings.Contains(nftStr, "dport 443") misses +// the set form because "{ 80, " sits between "dport" and "443", producing a +// false "no HTTPS" warning even when port 443 is correctly whitelisted. +func nftHasPort(nftStr, port string) bool { + for _, m := range nftDportRe.FindAllStringSubmatch(nftStr, -1) { + group := m[1] + if group == port { + return true + } + if strings.HasPrefix(group, "{") { + for _, p := range strings.Split(strings.Trim(group, "{} "), ",") { + if strings.TrimSpace(p) == port { + return true + } + } + } + } + return false +} + type securityCheck struct { Name string `json:"name"` Status string `json:"status"` // "ok" | "warning" | "error" @@ -49,7 +76,7 @@ func (s *Server) handleSecurityAudit(w http.ResponseWriter, r *http.Request) { // 2. Port 3000 (Next.js) extern erreichbar? if !firewallActive { checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "error", Message: "Keine Firewall aktiv — Port möglicherweise extern erreichbar"}) - } else if strings.Contains(nftStr, "dport 3000") { + } else if nftHasPort(nftStr, "3000") { checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "warning", Message: "Port 3000 explizit in Firewall-Regeln — prüfen ob gewollt"}) } else { checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "ok", Message: "Blockiert (nicht in Whitelist)"}) @@ -58,14 +85,14 @@ func (s *Server) handleSecurityAudit(w http.ResponseWriter, r *http.Request) { // 3. Port 8080 (Go Backend) extern erreichbar? if !firewallActive { checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "error", Message: "Keine Firewall aktiv — Port möglicherweise extern erreichbar"}) - } else if strings.Contains(nftStr, "dport 8080") { + } else if nftHasPort(nftStr, "8080") { checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "warning", Message: "Port 8080 explizit in Firewall-Regeln — prüfen ob gewollt"}) } else { checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "ok", Message: "Blockiert (nicht in Whitelist)"}) } // 4. HTTPS aktiv? - if firewallActive && strings.Contains(nftStr, "dport 443") { + if firewallActive && nftHasPort(nftStr, "443") { checks = append(checks, securityCheck{Name: "HTTPS (TLS)", Status: "ok", Message: "Port 443 in Firewall freigegeben"}) } else { checks = append(checks, securityCheck{Name: "HTTPS (TLS)", Status: "warning", Message: "Kein HTTPS — Verbindungen unverschlüsselt (certbot empfohlen)"})