#!/bin/bash # Toggle the crowdsec-host-logs marker block in libreportal's live compose # ("on" uncomments, "off" re-comments) and recreate the container so the # mount set takes effect. crowdsecToggleLibrePortalLogMounts() { local mode="$1" local compose="${containers_dir}libreportal/docker-compose.yml" [[ -f "$compose" ]] || return 0 case "$mode" in on) # Lines inside the marker block that look like `#-/var/log/crowdsec...:` # become `-/var/log/...:` so docker compose picks up the bind-mounts. runFileOp sed -i '/# >>> crowdsec-host-logs >>>/,/# <<< crowdsec-host-logs <<>> crowdsec-host-logs >>>/,/# <<< crowdsec-host-logs <</dev/null | grep -q '^libreportal-service$'; then isNotice "Recreating libreportal so log mount toggle takes effect..." ( cd "${containers_dir}libreportal" && runAsManager docker compose up -d >/dev/null 2>&1 ) || true fi } installCrowdsecHost() { # Make /var/log/crowdsec*.log world-readable so the libreportal container # (UID 1001) can tail them via the bind-mount we're about to enable. runCrowdsec touch-host-logs crowdsecToggleLibrePortalLogMounts on local desired_state="${CFG_CROWDSEC_ENABLED:-true}" local is_installed="false" command -v cscli >/dev/null 2>&1 && is_installed="true" if [[ "$desired_state" == "true" && "$is_installed" == "false" ]]; then isHeader "Install CrowdSec" ((menu_number++)) echo "" echo "---- $menu_number. Installing the CrowdSec agent + firewall bouncer." echo "" isNotice "First-time install ~30-70 MB GeoLite2 DB + parser hub, 1-3 mins." # One-shot: adds the apt repo, installs both packages, enables both # services, installs the crowdsecurity/linux + /sshd collections, then # reloads the agent. All of it lives in libreportal-crowdsec so the # manager never needs `sudo apt-get` / `sudo bash`. local result=$(runCrowdsec install) checkSuccess "Installing CrowdSec agent + firewall bouncer + baseline collections" ((menu_number++)) echo "" echo "---- $menu_number. Community blocklist (CAPI) toggle." echo "" # CAPI registration is what subscribes to the community blocklist # AND sends anonymous attack signals back. apt postinst registers # by default; honour CFG_CROWDSEC_COMMUNITY_BLOCKLIST=false by # unregistering. Idempotent on either branch. local community_blocklist="${CFG_CROWDSEC_COMMUNITY_BLOCKLIST:-true}" if [[ "$community_blocklist" == "true" ]]; then if runCrowdsec capi status 2>&1 | grep -qi 'You can successfully'; then isNotice "Community blocklist already registered." else local result=$(runCrowdsec capi register 2>&1) checkSuccess "Registered with CrowdSec Central API (community blocklist)" fi else local result=$(runCrowdsec capi unregister 2>&1) checkSuccess "Unregistered from CrowdSec Central API (community blocklist disabled)" fi ((menu_number++)) echo "" echo "---- $menu_number. SaaS Console enrollment toggle." echo "" # `console enroll ` registers this agent with the hosted # dashboard at app.crowdsec.net. Idempotent: if already enrolled, # skip. If toggled off, disenroll. Quietly skipped when the flag # is on but the token field is empty (user hasn't pasted one yet). local console_enroll="${CFG_CROWDSEC_CONSOLE_ENROLL:-false}" local console_token="${CFG_CROWDSEC_CONSOLE_TOKEN:-}" local enrolled=false runCrowdsec console status 2>&1 | grep -qi 'enrolled' && enrolled=true if [[ "$console_enroll" == "true" ]]; then if [[ -z "$console_token" ]]; then isNotice "Console enrollment ON but CFG_CROWDSEC_CONSOLE_TOKEN is empty — paste your token from app.crowdsec.net to complete." elif [[ "$enrolled" == true ]]; then isNotice "Already enrolled with the SaaS console — skipping." else local result=$(runCrowdsec console enroll "$console_token" 2>&1) checkSuccess "Enrolled with app.crowdsec.net SaaS console" fi else if [[ "$enrolled" == true ]]; then local result=$(runCrowdsec console disenroll 2>&1) checkSuccess "Disenrolled from app.crowdsec.net SaaS console" else isNotice "SaaS console enrollment disabled — skipping." fi fi ((menu_number++)) echo "" echo "---- $menu_number. Wiring LAPI for Traefik bouncer access." echo "" # Bind LAPI to all interfaces so the Traefik container can reach it # via host.docker.internal:host-gateway. The bouncer API key is # required (HTTP 401 without it), so internet exposure is gated. # External access on 8080 should still be blocked at UFW. local bind_result bind_result=$(runCrowdsec bind-lapi 2>&1) if [[ "$bind_result" == "ALREADY_BOUND" ]]; then isNotice "LAPI already bound to 0.0.0.0:8080 — skipping." else checkSuccess "LAPI bound to 0.0.0.0:8080" runCrowdsec services restart checkSuccess "CrowdSec restarted" fi ((menu_number++)) echo "" echo "---- $menu_number. Prometheus metrics endpoint." echo "" # When monitoring is on, bind CrowdSec's Prometheus metrics endpoint to # a docker-reachable address (the 127.0.0.1 default can't be scraped # from the Prometheus container). When off, rebind to localhost if a # prior run opened it. The helper does the scoped edit in the # prometheus: block. local mon_enabled="${CFG_CROWDSEC_MONITORING:-false}" local prom_listen="${CFG_CROWDSEC_PROMETHEUS_LISTEN:-0.0.0.0:6060}" local prom_addr="${prom_listen%%:*}" local prom_port="${prom_listen##*:}" if [[ "$mon_enabled" == "true" ]]; then local result=$(runCrowdsec prometheus on "$prom_addr" "$prom_port") checkSuccess "CrowdSec metrics endpoint bound to ${prom_listen}" runCrowdsec services restart checkSuccess "CrowdSec restarted" else local result=$(runCrowdsec prometheus off) checkSuccess "CrowdSec metrics endpoint rebound to 127.0.0.1 (monitoring off)" runCrowdsec services restart checkSuccess "CrowdSec restarted" fi ((menu_number++)) echo "" echo "---- $menu_number. Traefik bouncer API key." echo "" # Generate a dedicated bouncer key for Traefik. Two sinks for the value: # 1) /etc/crowdsec/traefik_bouncer.key — raw key, bind-mounted into # the Traefik container read-only; the plugin reads it via # crowdsecLapiKeyFile. /etc/crowdsec/ is outside the framework's # sourceScanFiles sweep so a bare key file is safe here. # 2) ${configs_dir}security/security_crowdsec — CFG_CROWDSEC_TRAEFIK_LAPI_KEY # line, sourced by the framework and visible on the config page. # Editing the CFG var manually does not re-register the bouncer # (use the rotate Tools action for that); this is a visibility # surface, not the auth source of truth. # The helper handles cscli + tee + chown + chmod atomically. local init_result init_result=$(runCrowdsec bouncer-traefik-init 2>&1) if [[ "$init_result" == "EXISTS" ]]; then isNotice "Bouncer 'traefik-bouncer' already registered — leaving existing key file untouched at /etc/crowdsec/traefik_bouncer.key." elif [[ "$init_result" == GENERATED:* ]]; then local bouncer_key="${init_result#GENERATED:}" checkSuccess "Traefik bouncer API key generated" # Mirror the key into the live config file so it's visible / # editable via the framework's config page like any other CFG_* # setting. configs/ is manager-owned, so runInstallOp suffices. local cfg_file="${configs_dir}security/security_crowdsec" if [[ -f "$cfg_file" ]]; then runInstallOp sed -i "s|^CFG_CROWDSEC_TRAEFIK_LAPI_KEY=.*|CFG_CROWDSEC_TRAEFIK_LAPI_KEY=${bouncer_key}|" "$cfg_file" checkSuccess "Key mirrored to CFG_CROWDSEC_TRAEFIK_LAPI_KEY" else isNotice "Live config not present yet — key applied on next install." fi else isNotice "Failed to generate bouncer key: $init_result" isNotice "Traefik integration won't authenticate. Re-run installCrowdsecHost to retry." fi ((menu_number++)) echo "" echo "---- $menu_number. Verifying CrowdSec / host firewall coexistence." echo "" # The firewall bouncer needs a moment to install its nftables table # after enable. Poll up to ~10s before deciding it's missing. local _wait=0 until runSystem nft list tables 2>/dev/null | grep -qiE 'crowdsec' || [[ $_wait -ge 10 ]]; do sleep 1; _wait=$((_wait+1)) done if ! runSystem nft list tables 2>/dev/null | grep -qiE 'crowdsec'; then isNotice "CrowdSec nftables table not yet present after ${_wait}s. Bouncer may still be starting; re-run the verification Tools action in a minute if rules don't appear." else local cs_prio ufw_prio cs_prio=$(runSystem nft list ruleset 2>/dev/null | awk '/table .* crowdsec/{flag=1} flag && /priority/{match($0,/priority [-0-9]+/); print substr($0,RSTART+9,RLENGTH-9); exit}') ufw_prio=$(runSystem nft list ruleset 2>/dev/null | awk '/chain ufw[a-z0-9-]*input/{flag=1} flag && /priority/{match($0,/priority [-0-9]+/); print substr($0,RSTART+9,RLENGTH-9); exit}') if [[ -z "$ufw_prio" ]]; then isSuccessful "UFW not in nftables — no ordering needed (CrowdSec prio: ${cs_prio:-?})." elif [[ -n "$cs_prio" && "$cs_prio" -lt "$ufw_prio" ]]; then isSuccessful "Chain priority correct: CrowdSec ($cs_prio) runs before UFW ($ufw_prio) — bans take precedence." else isNotice "WARNING: CrowdSec priority (${cs_prio:-unknown}) is not lower than UFW ($ufw_prio)." isNotice " Packets accepted by UFW first won't reach CrowdSec drop rules." isNotice " Fix: run the 'crowdsec_fix_priority' Tools action, or manually edit" isNotice " /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml — set" isNotice " nftables.ipv4.priority and nftables.ipv6.priority to -100, then" isNotice " 'sudo systemctl restart crowdsec-firewall-bouncer'." fi fi isSuccessful "CrowdSec installed. Console enrollment OFF (no signals sent). Opt in via Tools tab for community blocklists." menu_number=0 cd elif [[ "$desired_state" == "true" && "$is_installed" == "true" ]]; then # Already installed — make sure services are running. Quiet path (no header) # so re-runs of pre-install don't spam the log when state already matches. if ! systemctl is-active --quiet crowdsec; then isHeader "Enable CrowdSec" ((menu_number++)) echo "" echo "---- $menu_number. Re-enabling CrowdSec services." echo "" local result=$(runCrowdsec services enable) checkSuccess "Enabling CrowdSec agent + firewall bouncer" isSuccessful "CrowdSec services re-enabled." menu_number=0 fi elif [[ "$desired_state" != "true" && "$is_installed" == "true" ]]; then # User flipped CFG_CROWDSEC_ENABLED away from "true" — disable, don't # uninstall. Package stays so flipping back is fast; explicit uninstall is # a separate Tools action. isHeader "Disable CrowdSec" ((menu_number++)) echo "" echo "---- $menu_number. Stopping and disabling CrowdSec services." echo "" local result=$(runCrowdsec services disable) checkSuccess "Disabling CrowdSec agent + firewall bouncer" isSuccessful "CrowdSec disabled. Package remains installed — set CFG_CROWDSEC_ENABLED=true to re-enable, or uninstall via the Tools tab." menu_number=0 fi }