fix(security-audit): HTTPS/Port-Checks erkennen nftables Set-Syntax nicht

strings.Contains(nftStr, "dport 443") fand den Substring nicht, wenn
nftables mehrere Ports als Set ausgibt ("tcp dport { 80, 443 } accept" statt
"tcp dport 443 accept") — das ist bei archivmail-Installationen der
Normalfall (80+443 stehen zusammen in einer Regel). Dashboard zeigte
dadurch fälschlich "Kein HTTPS — Verbindungen unverschlüsselt", obwohl
Port 443 korrekt offen war (bestätigt auf 131 und 132).

Neue Helper-Funktion nftHasPort() per Regex erkennt beide Formen
(Einzelport und Set). Betrifft die Checks für HTTPS/443, Port 3000 und
Port 8080.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-07-04 00:02:20 +02:00
co-authored by Claude Sonnet 5
parent c274df5023
commit 04e5b0f74a
+30 -3
View File
@@ -6,12 +6,39 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"regexp"
"strings" "strings"
"time" "time"
) )
// ── Security Audit ────────────────────────────────────────────────────────── // ── 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 <port>" 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 { type securityCheck struct {
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` // "ok" | "warning" | "error" 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? // 2. Port 3000 (Next.js) extern erreichbar?
if !firewallActive { if !firewallActive {
checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "error", Message: "Keine Firewall aktiv — Port möglicherweise extern erreichbar"}) 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"}) checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "warning", Message: "Port 3000 explizit in Firewall-Regeln — prüfen ob gewollt"})
} else { } else {
checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "ok", Message: "Blockiert (nicht in Whitelist)"}) 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? // 3. Port 8080 (Go Backend) extern erreichbar?
if !firewallActive { if !firewallActive {
checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "error", Message: "Keine Firewall aktiv — Port möglicherweise extern erreichbar"}) 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"}) checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "warning", Message: "Port 8080 explizit in Firewall-Regeln — prüfen ob gewollt"})
} else { } else {
checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "ok", Message: "Blockiert (nicht in Whitelist)"}) checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "ok", Message: "Blockiert (nicht in Whitelist)"})
} }
// 4. HTTPS aktiv? // 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"}) checks = append(checks, securityCheck{Name: "HTTPS (TLS)", Status: "ok", Message: "Port 443 in Firewall freigegeben"})
} else { } else {
checks = append(checks, securityCheck{Name: "HTTPS (TLS)", Status: "warning", Message: "Kein HTTPS — Verbindungen unverschlüsselt (certbot empfohlen)"}) checks = append(checks, securityCheck{Name: "HTTPS (TLS)", Status: "warning", Message: "Kein HTTPS — Verbindungen unverschlüsselt (certbot empfohlen)"})