#!/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="/docker/containers/libreportal/docker-compose.yml" [[ -f "$compose" ]] || return 0 case "$mode" in on) sudo 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 /docker/containers/libreportal && sudo -u libreportal docker compose up -d >/dev/null 2>&1 ) || true fi } installCrowdsecHost() { # Touch + chmod 0644 the host log files so the libreportal container # (UID 1001) can read them. Then enable the bind-mounts. for _l in /var/log/crowdsec.log /var/log/crowdsec-firewall-bouncer.log; do sudo touch "$_l" 2>/dev/null || true sudo chmod 0644 "$_l" 2>/dev/null || true done 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. Adding the CrowdSec apt repository." echo "" local result=$(curl -fsSL https://install.crowdsec.net | sudo bash) checkSuccess "Adding CrowdSec repository" ((menu_number++)) echo "" echo "---- $menu_number. Installing the CrowdSec agent." echo "" isNotice "First-time install ~30-70 MB GeoLite2 DB + parser hub, 1-3 mins." local result=$(sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q crowdsec &1) checkSuccess "Installing CrowdSec package" ((menu_number++)) echo "" echo "---- $menu_number. Installing the CrowdSec firewall bouncer." echo "" local result=$(sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q crowdsec-firewall-bouncer-nftables &1) checkSuccess "Installing CrowdSec firewall bouncer (nftables)" ((menu_number++)) echo "" echo "---- $menu_number. Enabling CrowdSec services." echo "" local result=$(sudo systemctl enable --now crowdsec) checkSuccess "Enabling CrowdSec agent" local result=$(sudo systemctl enable --now crowdsec-firewall-bouncer) checkSuccess "Enabling CrowdSec firewall bouncer" ((menu_number++)) echo "" echo "---- $menu_number. Installing baseline collections." echo "" local result=$(sudo cscli collections install crowdsecurity/linux) checkSuccess "Installing crowdsecurity/linux collection" local result=$(sudo cscli collections install crowdsecurity/sshd) checkSuccess "Installing crowdsecurity/sshd collection" local result=$(sudo systemctl reload crowdsec) checkSuccess "Reloading CrowdSec to pick up 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 sudo cscli capi status 2>&1 | grep -qi 'You can successfully'; then isNotice "Community blocklist already registered." else local result=$(sudo cscli capi register 2>&1) checkSuccess "Registered with CrowdSec Central API (community blocklist)" fi else local result=$(sudo cscli 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 "" # cscli 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 sudo cscli 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=$(sudo cscli console enroll "$console_token" 2>&1) checkSuccess "Enrolled with app.crowdsec.net SaaS console" fi else if [[ "$enrolled" == true ]]; then local result=$(sudo cscli 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 lapi_cfg="/etc/crowdsec/config.yaml" if [[ -f "$lapi_cfg" ]] && ! sudo grep -qE 'listen_uri:[[:space:]]*0\.0\.0\.0:8080' "$lapi_cfg"; then sudo sed -i 's|listen_uri:.*|listen_uri: 0.0.0.0:8080|' "$lapi_cfg" checkSuccess "LAPI bound to 0.0.0.0:8080" sudo systemctl restart crowdsec checkSuccess "CrowdSec restarted" else isNotice "LAPI already bound to 0.0.0.0:8080 — skipping." 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). The sed is scoped to the prometheus: # block. When off, rebind to localhost if a prior run opened it. local cs_cfg="/etc/crowdsec/config.yaml" 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" && -f "$cs_cfg" ]]; then if ! sudo grep -qE "listen_addr:[[:space:]]*${prom_addr}" "$cs_cfg"; then sudo sed -i "/^prometheus:/,/^[^[:space:]#]/ { s|enabled:.*|enabled: true| s|listen_addr:.*|listen_addr: ${prom_addr}| s|listen_port:.*|listen_port: ${prom_port}| }" "$cs_cfg" checkSuccess "CrowdSec metrics endpoint bound to ${prom_listen}" sudo systemctl restart crowdsec checkSuccess "CrowdSec restarted" else isNotice "CrowdSec metrics already bound to ${prom_addr} — skipping." fi elif [[ -f "$cs_cfg" ]] && sudo grep -qE 'listen_addr:[[:space:]]*0\.0\.0\.0' "$cs_cfg"; then sudo sed -i "/^prometheus:/,/^[^[:space:]#]/ s|listen_addr:.*|listen_addr: 127.0.0.1|" "$cs_cfg" checkSuccess "CrowdSec metrics endpoint rebound to 127.0.0.1 (monitoring off)" sudo systemctl restart crowdsec checkSuccess "CrowdSec restarted" else isNotice "Monitoring off — CrowdSec metrics endpoint left at its default." fi # Generate a dedicated bouncer key for Traefik (idempotent: skip if # already registered). 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) /docker/configs/network/network_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. local key_file="/etc/crowdsec/traefik_bouncer.key" local cfg_file="/docker/configs/security/security_crowdsec" if ! sudo cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then local bouncer_key bouncer_key=$(sudo cscli bouncers add traefik-bouncer -o raw 2>&1 | tail -1) if [[ -n "$bouncer_key" && "$bouncer_key" != *"error"* ]]; then echo "$bouncer_key" | sudo tee "$key_file" >/dev/null sudo chown libreportal:libreportal "$key_file" sudo chmod 0600 "$key_file" checkSuccess "Traefik bouncer API key generated" # Write the key into the live config file so it's visible / # editable via the framework's config page like any other # CFG_* setting. if [[ -f "$cfg_file" ]]; then sudo 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 — Traefik integration won't authenticate. Re-run installCrowdsecHost to retry." fi else isNotice "Bouncer 'traefik-bouncer' already registered — leaving existing key file untouched at $key_file." 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 sudo nft list tables 2>/dev/null | grep -qiE 'crowdsec' || [[ $_wait -ge 10 ]]; do sleep 1; _wait=$((_wait+1)) done if ! sudo 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=$(sudo 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=$(sudo 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=$(sudo systemctl enable --now crowdsec) checkSuccess "Enabling CrowdSec agent" local result=$(sudo systemctl enable --now crowdsec-firewall-bouncer) checkSuccess "Enabling CrowdSec 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=$(sudo systemctl disable --now crowdsec-firewall-bouncer 2>&1) checkSuccess "Disabling CrowdSec firewall bouncer" local result=$(sudo systemctl disable --now crowdsec 2>&1) checkSuccess "Disabling CrowdSec agent" isSuccessful "CrowdSec disabled. Package remains installed — set CFG_CROWDSEC_ENABLED=true to re-enable, or uninstall via the Tools tab." menu_number=0 fi }