LibrePortal/scripts/install/install_crowdsec.sh
librelad 3ecf213cab refactor(de-sudo): docker calls via runFileOp/dockerCommandRun, drop sudo
Container-plane docker now routes through the mode-aware helpers instead of
sudo: simple calls (exec/ps/run/build/images/inspect/port/logs across ~15
app/check scripts) -> runFileOp docker (rootless socket as the install user;
rooted via the docker group). The cd && docker compose paths drop the sudo on
the rooted branch (the rootless branch already used dockerCommandRunInstallUser
-- byte-identical now, manager-ready later); gluetun, which had no rootless
branch, now uses dockerCommandRun so force-recreate works in both modes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 16:29:22 +01:00

325 lines
15 KiB
Bash

#!/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.*\.log:/ s/^\([[:space:]]*\)#-/\1-/
}' "$compose"
;;
off)
sudo sed -i '/# >>> crowdsec-host-logs >>>/,/# <<< crowdsec-host-logs <<</ {
/crowdsec.*\.log:/ s/^\([[:space:]]*\)-/\1#-/
}' "$compose"
;;
*) isError "crowdsecToggleLibrePortalLogMounts: bad mode '$mode'"; return 1 ;;
esac
if runFileOp docker ps --format '{{.Names}}' 2>/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 </dev/null 2>&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 </dev/null 2>&1)
checkSuccess "Installing CrowdSec firewall bouncer (nftables)"
((menu_number++))
echo ""
echo "---- $menu_number. Enabling CrowdSec services."
echo ""
local result=$(runSystem systemctl enable --now crowdsec)
checkSuccess "Enabling CrowdSec agent"
local result=$(runSystem systemctl enable --now crowdsec-firewall-bouncer)
checkSuccess "Enabling CrowdSec firewall bouncer"
((menu_number++))
echo ""
echo "---- $menu_number. Installing baseline collections."
echo ""
local result=$(runSystem cscli collections install crowdsecurity/linux)
checkSuccess "Installing crowdsecurity/linux collection"
local result=$(runSystem cscli collections install crowdsecurity/sshd)
checkSuccess "Installing crowdsecurity/sshd collection"
local result=$(runSystem 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 runSystem cscli capi status 2>&1 | grep -qi 'You can successfully'; then
isNotice "Community blocklist already registered."
else
local result=$(runSystem cscli capi register 2>&1)
checkSuccess "Registered with CrowdSec Central API (community blocklist)"
fi
else
local result=$(runSystem 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 <token> 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
runSystem 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=$(runSystem cscli console enroll "$console_token" 2>&1)
checkSuccess "Enrolled with app.crowdsec.net SaaS console"
fi
else
if [[ "$enrolled" == true ]]; then
local result=$(runSystem 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"
runSystem 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}"
runSystem 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)"
runSystem 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 ! runSystem cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then
local bouncer_key
bouncer_key=$(runSystem 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 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=$(runSystem systemctl enable --now crowdsec)
checkSuccess "Enabling CrowdSec agent"
local result=$(runSystem 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=$(runSystem systemctl disable --now crowdsec-firewall-bouncer 2>&1)
checkSuccess "Disabling CrowdSec firewall bouncer"
local result=$(runSystem 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
}