From 414dad6b3b64bd85634e49597479369aaa3263a0 Mon Sep 17 00:00:00 2001 From: Debian Date: Mon, 1 Jun 2026 22:17:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(crowdsec):=20IDS=20ein-/ausschalten=20via?= =?UTF-8?q?=20Switch=20=E2=80=94=20v1.2.63?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- VERSION | 2 +- internal/handlers/system.go | 49 +++++++++++++ management-ui/src/pages/CrowdSec/index.tsx | 70 ++++++++++++++----- .../debian/edgeguard-api/DEBIAN/postinst | 9 +++ 4 files changed, 111 insertions(+), 19 deletions(-) diff --git a/VERSION b/VERSION index 8bfc8db..32b12cc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.62 +1.2.63 diff --git a/internal/handlers/system.go b/internal/handlers/system.go index 5576922..9440dcc 100644 --- a/internal/handlers/system.go +++ b/internal/handlers/system.go @@ -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 diff --git a/management-ui/src/pages/CrowdSec/index.tsx b/management-ui/src/pages/CrowdSec/index.tsx index e5f4d1d..6775483 100644 --- a/management-ui/src/pages/CrowdSec/index.tsx +++ b/management-ui/src/pages/CrowdSec/index.tsx @@ -8,6 +8,7 @@ import { Popconfirm, Select, Space, + Switch, Tag, Tabs, Table, @@ -22,6 +23,7 @@ import { } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useAuthStore } from '../../stores/auth' import apiClient, { isEnvelope } from '../../api/client' import PageHeader from '../../components/PageHeader' @@ -73,27 +75,45 @@ async function fetchCollections(): Promise { return (r.data.data as { collections: HubItem[] }).collections ?? [] } +async function toggleService(service: string, enabled: boolean): Promise { + await apiClient.post('/system/service-toggle', { service, enabled }) +} + // ---------- Status strip ---------------------------------------------------- -function StatusStrip({ status }: { status: CrowdSecStatus | undefined }) { +function StatusStrip({ status, onToggle }: { status: CrowdSecStatus | undefined; onToggle: () => void }) { const { t } = useTranslation() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' - const agentColor = status?.agent_running ? 'green' : 'red' - const bouncerColor = status?.bouncer_running ? 'green' : 'red' - const agentLabel = status?.agent_running ? t('cs.status.running') : t('cs.status.stopped') - const bouncerLabel = status?.bouncer_running ? t('cs.status.running') : t('cs.status.stopped') + const toggleAgent = useMutation({ + mutationFn: (enabled: boolean) => toggleService('crowdsec', enabled), + onSuccess: onToggle, + }) + const toggleBouncer = useMutation({ + mutationFn: (enabled: boolean) => toggleService('crowdsec-firewall-bouncer', enabled), + onSuccess: onToggle, + }) return (
{t('cs.status.agent')}
- : } - color={agentColor} - > - {agentLabel} - + + : } + color={status?.agent_running ? 'green' : 'red'} + > + {status?.agent_running ? t('cs.status.running') : t('cs.status.stopped')} + + toggleAgent.mutate(checked)} + /> +
{status?.version && (
{status.version}
@@ -102,12 +122,21 @@ function StatusStrip({ status }: { status: CrowdSecStatus | undefined }) {
{t('cs.status.bouncer')}
- : } - color={bouncerColor} - > - {bouncerLabel} - + + : } + color={status?.bouncer_running ? 'green' : 'red'} + > + {status?.bouncer_running ? t('cs.status.running') : t('cs.status.stopped')} + + toggleBouncer.mutate(checked)} + /> +
@@ -496,6 +525,7 @@ function CollectionsTab() { export default function CrowdSecPage() { const { t } = useTranslation() + const queryClient = useQueryClient() const { data: status } = useQuery({ queryKey: ['crowdsec', 'status'], @@ -503,6 +533,10 @@ export default function CrowdSecPage() { refetchInterval: 10_000, }) + const invalidateStatus = () => { + void queryClient.invalidateQueries({ queryKey: ['crowdsec', 'status'] }) + } + const tabs = [ { key: 'decisions', label: t('cs.tabs.decisions'), children: }, { key: 'alerts', label: t('cs.tabs.alerts'), children: }, @@ -526,7 +560,7 @@ export default function CrowdSecPage() { className="mb-2" /> )} - +
) diff --git a/packaging/debian/edgeguard-api/DEBIAN/postinst b/packaging/debian/edgeguard-api/DEBIAN/postinst index 4320832..8e6fbd7 100755 --- a/packaging/debian/edgeguard-api/DEBIAN/postinst +++ b/packaging/debian/edgeguard-api/DEBIAN/postinst @@ -165,6 +165,15 @@ edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli collections list -o json edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli collections install * edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli collections remove * edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli version +# CrowdSec service toggle (start/stop/enable/disable) +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl start crowdsec.service +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop crowdsec.service +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable crowdsec.service +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable crowdsec.service +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl start crowdsec-firewall-bouncer.service +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop crowdsec-firewall-bouncer.service +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable crowdsec-firewall-bouncer.service +edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable crowdsec-firewall-bouncer.service SUDOERS # ── Distro-Conf-Includes für die per-Service Renderer ─────────