feat(crowdsec): IDS ein-/ausschalten via Switch — v1.2.63

- system.go: ServiceToggle-Endpoint (POST /system/service-toggle)
  start/stop + enable/disable für crowdsec + crowdsec-firewall-bouncer
- system.go: crowdsec + crowdsec-firewall-bouncer in servicesToCheck
- postinst: sudoers-Einträge für systemctl start/stop/enable/disable
  beider CrowdSec-Units
- UI: Switch im StatusStrip für Agent + Bouncer, getrennt schaltbar,
  disabled wenn CrowdSec nicht installiert oder Viewer-Rolle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-06-01 22:17:22 +02:00
parent 112a945b5c
commit 414dad6b3b
4 changed files with 111 additions and 19 deletions

View File

@@ -118,6 +118,7 @@ func (h *SystemHandler) Register(rg *gin.RouterGroup) {
g.POST("/haproxy-reload", h.HAProxyReload)
g.POST("/render-configs", h.RenderConfigs)
g.POST("/service-restart", h.ServiceRestart)
g.POST("/service-toggle", h.ServiceToggle)
g.GET("/upgrade-status", h.UpgradeStatus)
g.GET("/ipv6", h.IPv6)
g.POST("/ipv6", h.SetIPv6)
@@ -194,6 +195,8 @@ var servicesToCheck = []struct{ Label, Unit string }{
{"chrony", "chrony"},
{"squid", "squid"},
{"postgresql", "postgresql"},
{"crowdsec", "crowdsec"},
{"crowdsec-firewall-bouncer", "crowdsec-firewall-bouncer"},
}
type serviceStatus struct {
@@ -632,6 +635,52 @@ func (h *SystemHandler) ServiceRestart(c *gin.Context) {
response.OK(c, gin.H{"ok": true, "service": svc})
}
// toggleAllowlist defines which services may be started/stopped via the UI.
var toggleAllowlist = map[string]bool{
"crowdsec": true,
"crowdsec-firewall-bouncer": true,
"squid": true,
"unbound": true,
}
// ServiceToggle starts or stops (and enables/disables) a service.
// Body: {"service": "crowdsec", "enabled": true}
func (h *SystemHandler) ServiceToggle(c *gin.Context) {
var req struct {
Service string `json:"service" binding:"required"`
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Err(c, http.StatusBadRequest, simpleErr("service and enabled required"))
return
}
svc := strings.TrimSpace(req.Service)
if !toggleAllowlist[svc] {
response.Err(c, http.StatusBadRequest, simpleErr("service not in toggle allowlist: "+svc))
return
}
unit := svc + ".service"
action := "stop"
sysdAction := "disable"
if req.Enabled {
action = "start"
sysdAction = "enable"
}
if out, err := exec.Command("sudo", "-n", "/usr/bin/systemctl", sysdAction, unit).CombinedOutput(); err != nil {
response.Err(c, http.StatusInternalServerError, simpleErr(strings.TrimSpace(string(out))+": "+err.Error()))
return
}
if out, err := exec.Command("sudo", "-n", "/usr/bin/systemctl", action, unit).CombinedOutput(); err != nil {
response.Err(c, http.StatusInternalServerError, simpleErr(strings.TrimSpace(string(out))+": "+err.Error()))
return
}
if h.Audit != nil {
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "system.service_toggle",
svc, gin.H{"service": svc, "enabled": req.Enabled}, h.NodeID)
}
response.OK(c, gin.H{"ok": true, "service": svc, "enabled": req.Enabled})
}
// RenderConfigs erzwingt ein Re-Render aller Service-Configs aus dem
// aktuellen DB-State. Läuft haproxy + alle ExtraReloaders (nftables,
// wireguard, squid, unbound, chrony) durch. Fehler werden gesammelt