LibrePortal/containers/crowdsec/scripts/crowdsec_install_host.sh
librelad 053a620e22 fix(reliability): split local result=$(cmd) so $? survives for checkSuccess
'local result=$(cmd)' resets $? to 0 (the local builtin's own exit), so the
following checkSuccess always saw success regardless of cmd's real exit — the
mechanism that masked the de-sudo write failures. Split declaration from
assignment ('local result; result=$(cmd)') across all 235 active-code sites
(84 files) so the command's exit reaches checkSuccess. No behaviour change
beyond $? now being accurate (no set -e in runtime code; multi-line
assignments transform safely).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 03:09:25 +01:00

271 lines
13 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="${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.*\.log:/ s/^\([[:space:]]*\)#-/\1-/
}' "$compose"
;;
off)
runFileOp 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 "${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; 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; result=$(runCrowdsec capi register 2>&1)
checkSuccess "Registered with CrowdSec Central API (community blocklist)"
fi
else
local result; 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 <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
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; result=$(runCrowdsec console enroll "$console_token" 2>&1)
checkSuccess "Enrolled with app.crowdsec.net SaaS console"
fi
else
if [[ "$enrolled" == true ]]; then
local result; 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; 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; 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; 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; 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
}