#!/bin/sh # HAPX-UI privileged CrowdSec provisioning helper. # # Installed root:root mode 0755 — NOT writable by the hapx-ui service user. # Invoked ONLY through the pinned sudoers rule (two verbs): # hapx-ui ALL=(root) NOPASSWD: /usr/local/sbin/hapx-ui-crowdsec preflight, # /usr/bin/systemctl start --no-block hapx-ui-crowdsec-setup.service # # Same security boundary as the updater: keeping the privileged logic in THIS # fixed, root-owned script means the unprivileged web user can trigger exactly # `preflight` (read-only) and the fixed `setup` oneshot — nothing else, and it # can never inject arbitrary root commands. `setup` takes NO arguments; its only # input from the service is the request file below, whose fields are strictly # validated here before use. # # Verbs: # preflight → read-only status probe, prints one JSON line to stdout. # setup → idempotent provisioning: install CrowdSec (apt) if missing, # pin the LAPI to :8090, ensure HAProxy log acquisition + the # haproxy collection, (re)create the hapx-ui bouncer and hand its # API key back to the service, and — for mode=account — enroll the # engine in the CrowdSec Console with the supplied token. set -u # ── Fixed locations ────────────────────────────────────────────────────────── DATA=/var/lib/hapx-ui # helper→service files live in a ROOT-owned dir (root:hapx-ui 0750) that the # unprivileged service user can READ but NOT write. Writing them in the # service-writable $DATA let the service pre-plant a symlink and redirect root's # create/chown/chmod onto an arbitrary path (CWE-59 link-following LPE). CS_STATE="$DATA/cs-state" REQUEST="$DATA/crowdsec-setup-request.json" # service → helper (mode, token) — service-writable STATE="$CS_STATE/setup-state.json" # helper → service (progress) LOG="$CS_STATE/setup.log" # helper → service (detail log) LOCK="$CS_STATE/setup.lock" KEYFILE="$CS_STATE/bouncer-key" # helper → service (the bouncer key) # ── CrowdSec locations CS_CONFIG=/etc/crowdsec/config.yaml CS_CREDS=/etc/crowdsec/local_api_credentials.yaml CS_ACQUIS_DIR=/etc/crowdsec/acquis.d CS_ONLINE_CREDS=/etc/crowdsec/online_api_credentials.yaml LAPI_PORT=8090 # state writes the machine-readable progress file, owned by the service user (= # the data dir owner) so the web UI can read it and reset it on the next run. # $1 = phase, $2 = optional short message (ASCII, no quotes/backslashes). state() { printf '{"phase":"%s","message":"%s"}' "$1" "${2:-}" >"$STATE.tmp" \ && mv "$STATE.tmp" "$STATE" && chmod 0640 "$STATE" 2>/dev/null || true chgrp hapx-ui "$STATE" 2>/dev/null || true } # fail records a terminal failure and exits. Reason must be fixed ASCII. fail() { echo "[FEHLER] $1" state failed "$1" exit 1 } # Serialize concurrent setups; steal a stale lock (>30m). acquire_lock() { if ! mkdir "$LOCK" 2>/dev/null; then if [ -d "$LOCK" ]; then age=$(( $(date +%s) - $(stat -c %Y "$LOCK" 2>/dev/null || echo 0) )) if [ "$age" -gt 1800 ]; then rmdir "$LOCK" 2>/dev/null || true mkdir "$LOCK" 2>/dev/null || { echo "[lock] konnte Lock nicht übernehmen"; exit 3; } else echo "[lock] Ein anderes CrowdSec-Setup läuft bereits (seit ${age}s). Abbruch."; exit 3 fi fi fi trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT INT TERM } have() { command -v "$1" >/dev/null 2>&1; } # lapi_up reports whether the local API answers (only meaningful once installed). lapi_up() { cscli lapi status >/dev/null 2>&1; } bouncer_present() { cscli bouncers list -o raw 2>/dev/null | cut -d, -f1 | grep -qx hapx-ui; } # enrolled is best-effort: enrollment writes login/password into the online creds. enrolled() { [ -f "$CS_ONLINE_CREDS" ] && grep -qE '^[[:space:]]*login:[[:space:]]*\S' "$CS_ONLINE_CREDS" 2>/dev/null; } lapi_port() { sed -n 's/.*listen_uri:[[:space:]]*[0-9.]*:\([0-9]*\).*/\1/p' "$CS_CONFIG" 2>/dev/null | head -1; } case "${1:-}" in preflight) installed=false; have cscli && installed=true apt_ok=false; have apt-get && apt_ok=true up=false; { $installed && lapi_up; } && up=true bnc=false; { $installed && bouncer_present; } && bnc=true enr=false; { $installed && enrolled; } && enr=true port="$( $installed && lapi_port || true )" ready=false; { $installed && $up && $bnc; } && ready=true printf '{"installed":%s,"aptAvailable":%s,"lapiUp":%s,"lapiPort":"%s","bouncerPresent":%s,"enrolled":%s,"ready":%s}\n' \ "$installed" "$apt_ok" "$up" "${port:-}" "$bnc" "$enr" "$ready" ;; setup) # Create the root-owned coordination dir BEFORE any write. root:hapx-ui 0750 # → service can read/traverse but cannot create entries (no symlink planting). install -d -m 0750 -o root -g hapx-ui "$CS_STATE" 2>/dev/null \ || { mkdir -p "$CS_STATE"; chgrp hapx-ui "$CS_STATE" 2>/dev/null || true; chmod 0750 "$CS_STATE"; } exec >"$LOG" 2>&1 chgrp hapx-ui "$LOG" 2>/dev/null || true chmod 0640 "$LOG" 2>/dev/null || true acquire_lock echo "== HAPX-UI CrowdSec-Setup $(date -u +%Y-%m-%dT%H:%M:%SZ) ==" # ── Read + strictly validate the request (service-writable → untrusted) ── [ -f "$REQUEST" ] || fail "Keine Setup-Anfrage gefunden" [ -L "$REQUEST" ] && fail "Setup-Anfrage ist ein Symlink — abgelehnt" mode=$(sed -n 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$REQUEST" | head -1) token=$(sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$REQUEST" | head -1) echo "$mode" | grep -qE '^(local|account)$' || fail "Ungueltiger Modus" # Token charset is deliberately narrow (no quote/backslash/space) so it is # safe to pass to cscli. Enforced even though the value never hits a shell eval. if [ -n "$token" ]; then echo "$token" | grep -qE '^[A-Za-z0-9._-]{1,200}$' || fail "Ungueltiges Enrollment-Token" fi [ "$mode" = account ] && [ -z "$token" ] && fail "Account-Modus benoetigt ein Enrollment-Token" echo "Modus: $mode" # ── 1. Install CrowdSec if missing ────────────────────────────────────── state installing "CrowdSec wird installiert" if ! have cscli; then have apt-get || fail "Automatische Installation nur auf Debian/Ubuntu (apt) moeglich" echo "-- CrowdSec-Repo einrichten --" curl -s https://install.crowdsec.net | sh || fail "Repo-Setup fehlgeschlagen" echo "-- apt-get install crowdsec --" DEBIAN_FRONTEND=noninteractive apt-get install -y crowdsec || fail "apt-get install crowdsec fehlgeschlagen" have cscli || fail "CrowdSec nach Installation nicht gefunden" else echo "CrowdSec bereits installiert — ueberspringe apt." fi # ── 2. Configure: LAPI port, acquisition, collections ─────────────────── state configuring "CrowdSec wird konfiguriert" # Pin the LAPI to :8090 (only when it is on the colliding :8080/default). if [ -f "$CS_CONFIG" ] && grep -qE 'listen_uri:[[:space:]]*127\.0\.0\.1:8080' "$CS_CONFIG"; then echo "-- LAPI listen_uri 8080 -> $LAPI_PORT --" sed -i "s|listen_uri:[[:space:]]*127\.0\.0\.1:8080|listen_uri: 127.0.0.1:$LAPI_PORT|" "$CS_CONFIG" fi if [ -f "$CS_CREDS" ] && grep -qE 'url:[[:space:]]*http://127\.0\.0\.1:8080' "$CS_CREDS"; then sed -i "s|url:[[:space:]]*http://127\.0\.0\.1:8080|url: http://127.0.0.1:$LAPI_PORT|" "$CS_CREDS" fi # Ensure a HAProxy log acquisition exists (skip if any is already present so # we never read the same source twice). Prefer a real log file; fall back to # journald (robust on boxes that only log HAProxy to the journal). mkdir -p "$CS_ACQUIS_DIR" if ! grep -rqsE 'type:[[:space:]]*haproxy' "$CS_ACQUIS_DIR" /etc/crowdsec/acquis.yaml 2>/dev/null; then if [ -r /var/log/haproxy.log ]; then echo "-- Acquisition: /var/log/haproxy.log --" cat >"$CS_ACQUIS_DIR/hapx-haproxy.yaml" <<'ACQ' filenames: - /var/log/haproxy.log labels: type: haproxy ACQ else echo "-- Acquisition: journald (haproxy.service) --" cat >"$CS_ACQUIS_DIR/hapx-haproxy.yaml" <<'ACQ' source: journalctl journalctl_filter: - "_SYSTEMD_UNIT=haproxy.service" labels: type: haproxy ACQ fi else echo "HAProxy-Acquisition bereits vorhanden — ueberspringe." fi echo "-- Collections --" cscli collections install crowdsecurity/haproxy crowdsecurity/base-http-scenarios >/dev/null 2>&1 || \ echo "WARN: Collection-Install meldete einen Fehler (evtl. bereits vorhanden)." # ── 3. Start engine, wait for the LAPI ────────────────────────────────── state starting "CrowdSec wird gestartet" systemctl enable --now crowdsec >/dev/null 2>&1 || true systemctl restart crowdsec || fail "systemctl restart crowdsec fehlgeschlagen" i=0 while [ $i -lt 30 ]; do lapi_up && break; i=$((i+1)); sleep 1; done lapi_up || fail "Local API kam nicht hoch" echo "LAPI erreichbar." # ── 4. (Re)create the bouncer + hand the key back ─────────────────────── state registering "Bouncer wird angelegt" # An existing bouncer's key can't be read back, so recreate it cleanly. bouncer_present && cscli bouncers delete hapx-ui >/dev/null 2>&1 || true key="$(cscli bouncers add hapx-ui -o raw 2>/dev/null | tr -d '[:space:]')" [ -n "$key" ] || fail "Bouncer-Key konnte nicht erzeugt werden" umask 077 printf '%s' "$key" >"$KEYFILE.tmp" && mv "$KEYFILE.tmp" "$KEYFILE" chmod 0640 "$KEYFILE" 2>/dev/null || true chgrp hapx-ui "$KEYFILE" 2>/dev/null || true echo "Bouncer 'hapx-ui' angelegt, Key hinterlegt." # ── 5. Account enrollment (non-fatal) ─────────────────────────────────── if [ "$mode" = account ]; then state enrolling "Console-Enrollment" echo "-- cscli console enroll --" if cscli console enroll "$token" >/dev/null 2>&1; then systemctl restart crowdsec >/dev/null 2>&1 || true echo "Enrollment gestartet — bitte die Engine in der CrowdSec-Console bestaetigen." else echo "WARN: Enrollment fehlgeschlagen — lokale Erkennung laeuft trotzdem." fi fi # Token hygiene: the request carried the enroll token — remove it now. rm -f "$REQUEST" 2>/dev/null || true state "done" "Fertig" echo "== Setup abgeschlossen ==" ;; *) echo "usage: $0 {preflight|setup}" >&2 exit 2 ;; esac