Running any tool jumped to the Tasks tab and left the user stranded there. That is right for an install — long, log-heavy, worth watching — and wrong for a tool, which is a short admin action whose answer is one line. Worse, half of these are only meaningful back on Tools: List Users opens a modal over that tab, and Create User Account returns a generated password that was being buried in a log the user then had to go read. Tools now stay put. On completion the tool's own outcome lines — the isSuccessful/isError/isNotice output, ANSI stripped and framework boilerplate filtered — are shown in a small result modal, with a View log button for anything needing the full detail. list_users is left alone because the existing account-list modal is already a better result view. Also stops generate_arrays.sh walking scripts/dev. That directory is `export-ignore`d, so it exists in a working clone but never in a shipped install; generating a files_dev.sh entry from it wrote a reference into files_source.sh that no install could satisfy, and the loader treats a missing array file as a broken installation — every libreportal command stopped with "files_dev.sh is missing from your LibrePortal Installation". Excluded alongside unused/, system/ and release/. Regenerating also picked up scripts/validation, which had never had an array file. And Matrix's account listing prints its aligned line from python rather than re-splitting the marker line in bash: TAB is IFS whitespace, so an empty display name collapsed into the previous delimiter and shifted every later column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
233 lines
11 KiB
Bash
233 lines
11 KiB
Bash
#!/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 <action> [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 >/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 >/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 <token>|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 <token> — 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 <token>|disenroll|status}" >&2; return 2 ;;
|
|
esac
|
|
}
|
|
|
|
# --- traefik bouncer: register + write key file -------------------------------
|
|
# Shared by init and rotate: registers the bouncer with cscli and writes the key
|
|
# file. Prints GENERATED:<key> so the caller can mirror it into the config.
|
|
_crowdsec_bouncer_traefik_add() {
|
|
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"
|
|
}
|
|
|
|
# init: idempotent. "EXISTS" when the bouncer is already registered (the caller
|
|
# recovers the key from the key file), GENERATED:<key> when newly created.
|
|
crowdsec_bouncer_traefik_init() {
|
|
if cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then
|
|
echo "EXISTS"
|
|
return 0
|
|
fi
|
|
_crowdsec_bouncer_traefik_add
|
|
}
|
|
|
|
# --- bouncer-traefik-rotate: issue a new key for the Traefik bouncer ----------
|
|
# Delete then re-add: cscli cannot re-issue a key for an existing bouncer, and it
|
|
# cannot print an existing one either. The old key stops working the moment the
|
|
# bouncer is deleted, so the caller must restart Traefik to pick up the new one —
|
|
# there is a window where Traefik authenticates with a dead key.
|
|
crowdsec_bouncer_traefik_rotate() {
|
|
if cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then
|
|
cscli bouncers delete traefik-bouncer >/dev/null 2>&1 || {
|
|
echo "libreportal-crowdsec: could not delete the existing traefik-bouncer" >&2
|
|
return 1
|
|
}
|
|
fi
|
|
_crowdsec_bouncer_traefik_add
|
|
}
|
|
|
|
# --- bind-lapi <addr:port>: set listen_uri in config.yaml ---------------------
|
|
# Traefik talks to LAPI via host.docker.internal. The bouncer API key gates
|
|
# external access (HTTP 401 without it).
|
|
# Takes the bind target as <addr>:<port>, validated the same way the prometheus
|
|
# action validates its own, so the scoped sudoers still only ever sees a fixed
|
|
# edit. Defaults to 0.0.0.0:8080 when the caller passes nothing.
|
|
crowdsec_bind_lapi() {
|
|
local target="${1:-0.0.0.0:8080}"
|
|
local addr="${target%%:*}" port="${target##*:}"
|
|
[[ "$addr" =~ ^[A-Za-z0-9.-]+$ ]] || { echo "libreportal-crowdsec: bind-lapi invalid addr" >&2; return 1; }
|
|
[[ "$port" =~ ^[0-9]+$ ]] || { echo "libreportal-crowdsec: bind-lapi invalid port" >&2; return 1; }
|
|
[[ -f "$CFG_FILE" ]] || { echo "libreportal-crowdsec: $CFG_FILE not found" >&2; return 1; }
|
|
|
|
local esc_addr="${addr//./\\.}"
|
|
if grep -qE "listen_uri:[[:space:]]*${esc_addr}:${port}([[:space:]]|$)" "$CFG_FILE"; then
|
|
echo "ALREADY_BOUND"; return 0
|
|
fi
|
|
sed -i "s|listen_uri:.*|listen_uri: ${addr}:${port}|" "$CFG_FILE"
|
|
}
|
|
|
|
# --- prometheus {on <addr> <port>|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 <addr> <port>|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-traefik-rotate) crowdsec_bouncer_traefik_rotate ;;
|
|
bouncer-priority) crowdsec_bouncer_priority ;;
|
|
bind-lapi) crowdsec_bind_lapi "${1:-}" ;;
|
|
prometheus) crowdsec_prometheus "${1:-}" "${2:-}" "${3:-}" ;;
|
|
touch-host-logs) crowdsec_touch_host_logs ;;
|
|
*)
|
|
echo "usage: libreportal-crowdsec {install|services <enable|disable|restart>|capi <register|unregister|status>|console <enroll <token>|disenroll|status>|bouncer-traefik-init|bouncer-traefik-rotate|bouncer-priority|bind-lapi <addr:port>|prometheus <on <addr> <port>|off>|touch-host-logs}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|