#!/bin/bash # LibrePortal CrowdSec helper — root-privileged ops for the host-side CrowdSec # install (the agent + firewall bouncer the LibrePortal Traefik bouncer talks # to). Installed root:root 0755 to /usr/local/lib/libreportal/ by init.sh. # Self-contained: each action runs a FIXED set of ops with strictly-validated # args, so the scoped sudoers needn't grant the manager blanket apt/cscli/tee/ # sed/chown on /etc/crowdsec or /var/log. The runtime calls in via # `runCrowdsec [args…]` (run_privileged.sh). set -u [[ $EUID -eq 0 ]] || { echo "libreportal-crowdsec: must run as root" >&2; exit 1; } # Baked at install; unbaked copies keep the "__" sentinel. SYSTEM_DIR="__SYSTEM_DIR__" CONTAINERS_DIR="__CONTAINERS_DIR__" MANAGER_USER="__MANAGER__" [[ "$SYSTEM_DIR" == *"__"* || -z "$SYSTEM_DIR" ]] && SYSTEM_DIR="/libreportal-system" [[ "$CONTAINERS_DIR" == *"__"* || -z "$CONTAINERS_DIR" ]] && CONTAINERS_DIR="/libreportal-containers" [[ "$MANAGER_USER" == *"__"* || -z "$MANAGER_USER" ]] && MANAGER_USER="libreportal" CFG_FILE="/etc/crowdsec/config.yaml" BOUNCER_CFG="/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml" TRAEFIK_KEY_FILE="/etc/crowdsec/traefik_bouncer.key" HOST_LOGS=("/var/log/crowdsec.log" "/var/log/crowdsec-firewall-bouncer.log") # --- install: add repo + apt install + initial config ------------------------- # Idempotent: skips repo add if /etc/apt/sources.list.d/crowdsec_*.list exists, # skips apt install if cscli is on PATH, skips collections install if already # present. crowdsec_install() { if ! command -v cscli >/dev/null 2>&1; then if ! ls /etc/apt/sources.list.d/crowdsec_*.list >/dev/null 2>&1; then curl -fsSL https://install.crowdsec.net | bash >/dev/null 2>&1 || { echo "libreportal-crowdsec: failed to add CrowdSec apt repo" >&2; return 1 } fi DEBIAN_FRONTEND=noninteractive apt-get install -y -q crowdsec /dev/null 2>&1 || { echo "libreportal-crowdsec: apt-get install crowdsec failed" >&2; return 1 } fi if ! dpkg -s crowdsec-firewall-bouncer-nftables >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y -q crowdsec-firewall-bouncer-nftables /dev/null 2>&1 || { echo "libreportal-crowdsec: apt-get install firewall-bouncer failed" >&2; return 1 } fi systemctl enable --now crowdsec >/dev/null 2>&1 || true systemctl enable --now crowdsec-firewall-bouncer >/dev/null 2>&1 || true # Baseline collections. cscli is idempotent ("already installed" exits 0). cscli collections install crowdsecurity/linux >/dev/null 2>&1 || true cscli collections install crowdsecurity/sshd >/dev/null 2>&1 || true systemctl reload crowdsec >/dev/null 2>&1 || true } # --- services {enable|disable|restart} ---------------------------------------- crowdsec_services() { case "${1:-}" in enable) systemctl enable --now crowdsec || return 1 systemctl enable --now crowdsec-firewall-bouncer || return 1 ;; disable) systemctl disable --now crowdsec-firewall-bouncer >/dev/null 2>&1 || true systemctl disable --now crowdsec >/dev/null 2>&1 || true ;; restart) systemctl restart crowdsec || return 1 ;; *) echo "libreportal-crowdsec: services {enable|disable|restart}" >&2; return 2 ;; esac } # --- capi {register|unregister|status} ---------------------------------------- # Community blocklist (subscribe + send anonymous attack signals). crowdsec_capi() { case "${1:-}" in register) cscli capi register 2>&1 ;; unregister) cscli capi unregister 2>&1 ;; status) cscli capi status 2>&1 ;; *) echo "libreportal-crowdsec: capi {register|unregister|status}" >&2; return 2 ;; esac } # --- console {enroll |disenroll|status} -------------------------------- # SaaS dashboard at app.crowdsec.net. Token strictly validated. crowdsec_console() { case "${1:-}" in enroll) local token="${2:-}" [[ "$token" =~ ^[A-Za-z0-9_-]{8,128}$ ]] || { echo "libreportal-crowdsec: console enroll — invalid token format" >&2 return 1 } cscli console enroll "$token" 2>&1 ;; disenroll) cscli console disenroll 2>&1 ;; status) cscli console status 2>&1 ;; *) echo "libreportal-crowdsec: console {enroll |disenroll|status}" >&2; return 2 ;; esac } # --- bouncer-traefik-init: cscli register + write key file -------------------- # Idempotent: if the bouncer is already registered, skips and prints the existing # key-file marker so callers can decide whether to keep or rotate. Returns the # fresh key on stdout when newly generated, "EXISTS" when already registered. crowdsec_bouncer_traefik_init() { if cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then echo "EXISTS" return 0 fi local key key=$(cscli bouncers add traefik-bouncer -o raw 2>&1 | tail -1) [[ -n "$key" && "$key" != *"error"* ]] || { echo "libreportal-crowdsec: cscli bouncers add failed: $key" >&2 return 1 } # Write the key file: bind-mounted RO into Traefik. Manager-owned so the # WebUI/config layer (running as the manager) can read it; mode 0600. printf '%s\n' "$key" > "$TRAEFIK_KEY_FILE" chown "${MANAGER_USER}:${MANAGER_USER}" "$TRAEFIK_KEY_FILE" chmod 0600 "$TRAEFIK_KEY_FILE" echo "GENERATED:$key" } # --- bind-lapi: set listen_uri to 0.0.0.0:8080 in config.yaml ----------------- # Traefik talks to LAPI via host.docker.internal:8080. Bouncer API key gates # external access (HTTP 401 without it). crowdsec_bind_lapi() { [[ -f "$CFG_FILE" ]] || { echo "libreportal-crowdsec: $CFG_FILE not found" >&2; return 1; } if grep -qE 'listen_uri:[[:space:]]*0\.0\.0\.0:8080' "$CFG_FILE"; then echo "ALREADY_BOUND"; return 0 fi sed -i 's|listen_uri:.*|listen_uri: 0.0.0.0:8080|' "$CFG_FILE" } # --- prometheus {on |off} ---------------------------------------- # Flip CrowdSec's Prometheus metrics endpoint. `on` requires an addr + port the # Prometheus container can reach; `off` rebinds to 127.0.0.1. crowdsec_prometheus() { [[ -f "$CFG_FILE" ]] || { echo "libreportal-crowdsec: $CFG_FILE not found" >&2; return 1; } case "${1:-}" in on) local addr="${2:-}" port="${3:-}" [[ "$addr" =~ ^[A-Za-z0-9.-]+$ ]] || { echo "libreportal-crowdsec: invalid addr" >&2; return 1; } [[ "$port" =~ ^[0-9]+$ ]] || { echo "libreportal-crowdsec: invalid port" >&2; return 1; } sed -i "/^prometheus:/,/^[^[:space:]#]/ { s|enabled:.*|enabled: true| s|listen_addr:.*|listen_addr: ${addr}| s|listen_port:.*|listen_port: ${port}| }" "$CFG_FILE" ;; off) sed -i "/^prometheus:/,/^[^[:space:]#]/ s|listen_addr:.*|listen_addr: 127.0.0.1|" "$CFG_FILE" ;; *) echo "libreportal-crowdsec: prometheus {on |off}" >&2; return 2 ;; esac } # --- touch-host-logs: make crowdsec logs readable by the libreportal container -- crowdsec_touch_host_logs() { local l for l in "${HOST_LOGS[@]}"; do touch "$l" 2>/dev/null || true chmod 0644 "$l" 2>/dev/null || true done } # --- bouncer-priority: set nftables ipv4/ipv6 priority to -100 ----------------- # Moved here from libreportal-appcfg.crowdsec_priority. Same transform; lives in # the dedicated crowdsec helper so all CrowdSec-touching root ops are in one # auditable file. crowdsec_bouncer_priority() { [[ -f "$BOUNCER_CFG" ]] || { echo "libreportal-crowdsec: $BOUNCER_CFG not found" >&2; return 1; } cp "$BOUNCER_CFG" "${BOUNCER_CFG}.bak.$(date +%Y%m%d-%H%M%S)" awk -v p="-100" ' BEGIN { in_v4=0; in_v6=0 } /^[[:space:]]*ipv4:/ { in_v4=1; in_v6=0; print; next } /^[[:space:]]*ipv6:/ { in_v6=1; in_v4=0; print; next } /^[a-zA-Z]/ { in_v4=0; in_v6=0 } in_v4 && /^[[:space:]]+priority:/ { sub(/priority:.*/, "priority: " p) } in_v6 && /^[[:space:]]+priority:/ { sub(/priority:.*/, "priority: " p) } { print } ' "$BOUNCER_CFG" > "${BOUNCER_CFG}.new" && mv "${BOUNCER_CFG}.new" "$BOUNCER_CFG" } action="${1:-}"; shift 2>/dev/null || true case "$action" in install) crowdsec_install ;; services) crowdsec_services "${1:-}" ;; capi) crowdsec_capi "${1:-}" ;; console) crowdsec_console "${1:-}" "${2:-}" ;; bouncer-traefik-init) crowdsec_bouncer_traefik_init ;; bouncer-priority) crowdsec_bouncer_priority ;; bind-lapi) crowdsec_bind_lapi ;; prometheus) crowdsec_prometheus "${1:-}" "${2:-}" "${3:-}" ;; touch-host-logs) crowdsec_touch_host_logs ;; *) echo "usage: libreportal-crowdsec {install|services |capi |console |disenroll|status>|bouncer-traefik-init|bouncer-priority|bind-lapi|prometheus |off>|touch-host-logs}" >&2 exit 2 ;; esac