feat: Security Audit Tab im Admin-Panel mit nftables/SSH/Fail2ban-Checks
This commit is contained in:
@@ -105,6 +105,7 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("POST /api/admin/services/{name}/action", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleServiceAction)))
|
||||
|
||||
s.mux.HandleFunc("GET /api/admin/system/stats", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleSystemStats)))
|
||||
s.mux.HandleFunc("GET /api/admin/security/audit", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleSecurityAudit)))
|
||||
|
||||
// Export routes
|
||||
s.mux.HandleFunc("GET /api/export/pdf/{id}", s.authMiddleware(s.requireMailAccess(s.handleExportPDF)))
|
||||
@@ -1489,3 +1490,111 @@ func mailRefToInfo(store *storage.Store, ref *storage.MailRef) *mailInfo {
|
||||
Subject: pm.Subject,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Security Audit ──────────────────────────────────────────────────────────
|
||||
|
||||
type securityCheck struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // "ok" | "warning" | "error"
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (s *Server) handleSecurityAudit(w http.ResponseWriter, r *http.Request) {
|
||||
var checks []securityCheck
|
||||
|
||||
// 1. Firewall (nftables) aktiv?
|
||||
nftOut, err := exec.CommandContext(r.Context(), "nft", "list", "ruleset").Output()
|
||||
nftStr := string(nftOut)
|
||||
firewallActive := err == nil
|
||||
|
||||
if !firewallActive {
|
||||
checks = append(checks, securityCheck{
|
||||
Name: "Firewall (nftables)",
|
||||
Status: "error",
|
||||
Message: "nft konnte nicht ausgeführt werden — Firewall möglicherweise inaktiv",
|
||||
})
|
||||
} else if strings.Contains(nftStr, "policy drop") {
|
||||
checks = append(checks, securityCheck{
|
||||
Name: "Firewall (nftables)",
|
||||
Status: "ok",
|
||||
Message: "Aktiv — Input-Chain policy: drop (Whitelist-Modus)",
|
||||
})
|
||||
} else {
|
||||
checks = append(checks, securityCheck{
|
||||
Name: "Firewall (nftables)",
|
||||
Status: "warning",
|
||||
Message: "nftables aktiv, aber Input-Chain policy ist nicht 'drop'",
|
||||
})
|
||||
}
|
||||
|
||||
// 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") {
|
||||
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)"})
|
||||
}
|
||||
|
||||
// 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") {
|
||||
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") {
|
||||
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)"})
|
||||
}
|
||||
|
||||
// 5. SSH PermitRootLogin + PasswordAuthentication
|
||||
sshConf, err := os.ReadFile("/etc/ssh/sshd_config")
|
||||
if err == nil {
|
||||
lines := strings.Split(string(sshConf), "\n")
|
||||
rootLogin := ""
|
||||
passAuth := ""
|
||||
for _, l := range lines {
|
||||
tl := strings.ToLower(strings.TrimSpace(l))
|
||||
if strings.HasPrefix(tl, "permitrootlogin") && !strings.HasPrefix(tl, "#") {
|
||||
rootLogin = tl
|
||||
}
|
||||
if strings.HasPrefix(tl, "passwordauthentication") && !strings.HasPrefix(tl, "#") {
|
||||
passAuth = tl
|
||||
}
|
||||
}
|
||||
if strings.Contains(rootLogin, "no") || strings.Contains(rootLogin, "prohibit-password") {
|
||||
checks = append(checks, securityCheck{Name: "SSH Root-Login", Status: "ok", Message: rootLogin})
|
||||
} else if rootLogin == "" {
|
||||
checks = append(checks, securityCheck{Name: "SSH Root-Login", Status: "warning", Message: "Nicht explizit gesetzt (Standard: prohibit-password)"})
|
||||
} else {
|
||||
checks = append(checks, securityCheck{Name: "SSH Root-Login", Status: "warning", Message: rootLogin + " — Passwort-Login für root möglich"})
|
||||
}
|
||||
if strings.Contains(passAuth, "no") {
|
||||
checks = append(checks, securityCheck{Name: "SSH Passwort-Auth", Status: "ok", Message: "Nur Key-basierte Authentifizierung"})
|
||||
} else if passAuth == "" {
|
||||
checks = append(checks, securityCheck{Name: "SSH Passwort-Auth", Status: "warning", Message: "Nicht explizit deaktiviert — SSH-Keys empfohlen"})
|
||||
} else {
|
||||
checks = append(checks, securityCheck{Name: "SSH Passwort-Auth", Status: "warning", Message: passAuth + " — Brute-Force-Risiko"})
|
||||
}
|
||||
} else {
|
||||
checks = append(checks, securityCheck{Name: "SSH Konfiguration", Status: "warning", Message: "/etc/ssh/sshd_config nicht lesbar"})
|
||||
}
|
||||
|
||||
// 6. Fail2ban
|
||||
f2bOut, err := exec.CommandContext(r.Context(), "systemctl", "is-active", "fail2ban").Output()
|
||||
if err == nil && strings.TrimSpace(string(f2bOut)) == "active" {
|
||||
checks = append(checks, securityCheck{Name: "Fail2ban", Status: "ok", Message: "Aktiv"})
|
||||
} else {
|
||||
checks = append(checks, securityCheck{Name: "Fail2ban", Status: "warning", Message: "Nicht aktiv — kein Brute-Force-Schutz (apt install fail2ban)"})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"checks": checks,
|
||||
"run_at": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user