sudo_user_name (the real manager — owns the install, runs the runtime, baked as __MANAGER__ into the root helpers) was hardcoded to 'libreportal'. Make it configurable, consistent with the relocatable roots: - --manager-user=NAME flag + LP_MANAGER_USER env (default libreportal); resolved early in init.sh and in scripts/source/paths.sh (so the standalone processors get it too), validated as a real Linux username in libreportalValidatePaths. - Baked everywhere it must be stable: the helpers + CLI wrapper (CHECK_USER now __MANAGER__, exports LP_MANAGER_USER) via the install-time sed; the systemd unit exports LP_MANAGER_USER=<manager>. User creation (initUsers), the sudoers drop-in, and ~35 call sites already used $sudo_user_name, so they follow. - Fix the stray manager-name literals: install_crowdsec.sh chown, the check_install_type fallback. (Brand/identity strings like the backup engine:libreportal tag are left — they're not the username.) Verified: resolves default/env/flag; wrapper bakes a custom name (admin) with no placeholders left; validation rejects invalid usernames. The footprint paths (/etc, /usr/local) stay fixed by design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
325 lines
15 KiB
Bash
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="${containers_dir}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 "${containers_dir}libreportal" && runAsManager 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="${configs_dir}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 "$sudo_user_name:$sudo_user_name" "$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
|
|
}
|