#!/usr/bin/env bash set -Eeuo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$DIR" ENV_FILE="$DIR/.env" COMPOSE_FILE="deployment/crowdsec/compose.crowdsec.yml" PROJECT_NAME="epicnext-crowdsec" CONTAINER_NAME="epicnext-crowdsec" DEFAULT_PORT="18080" mode="${1:-enable}" case "$mode" in enable|--enable) ;; status|--status) ;; disable|--disable) ;; blocklists|blocklists-install-cron|blocklists-uninstall-cron) ;; *) echo "Usage: bash cms security [enable|status|disable|blocklists|blocklists-install-cron|blocklists-uninstall-cron]" >&2; exit 1 ;; esac umask 077 fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } for command in docker flock; do command -v "$command" >/dev/null || fail "Required command: $command"; done docker info >/dev/null 2>&1 || fail "Docker is not reachable." docker compose version >/dev/null 2>&1 || fail "Docker Compose plugin required." exec 9>"$DIR/.deploy.lock" flock -w 30 9 || fail "Another installation or update is running." [[ -f "$ENV_FILE" ]] || fail "Create .env first (bash cms install)." case "$mode" in blocklists) exec bash "$DIR/scripts/blocklists-sync.sh" sync "${2:-}" ;; blocklists-install-cron) exec bash "$DIR/scripts/blocklists-sync.sh" install-cron ;; blocklists-uninstall-cron) exec bash "$DIR/scripts/blocklists-sync.sh" uninstall-cron ;; esac env_get() { local key="$1" line while IFS= read -r line || [[ -n "$line" ]]; do case "$line" in "$key="*) line="${line#*=}"; line="${line%\"}"; line="${line#\"}"; printf '%s' "$line"; return 0 ;; esac done < "$ENV_FILE" return 1 } env_set() { local key="$1" value="$2" tmp tmp="$(mktemp "$DIR/.env.crowdsec.XXXXXX")" if awk -v k="$key" -v v="$value" 'BEGIN{FS=OFS="=";done=0} { if ($1==k) { print k "=" v; done=1 } else print } END { if (!done) print k "=" v }' "$ENV_FILE" > "$tmp"; then chmod 600 "$tmp" mv -f -- "$tmp" "$ENV_FILE" else rm -f -- "$tmp" fail "Could not update .env" fi } compose_cmd() { docker compose --project-name "$PROJECT_NAME" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" --profile security "$@" } health_probe() { local url="$1" if command -v curl >/dev/null 2>&1; then curl -fsS --max-time 3 "$url" >/dev/null 2>&1 else compose_cmd exec -T crowdsec wget -q -O - "$url" >/dev/null 2>&1 fi } container_running() { [[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || true)" = true ]] } if [[ "$mode" = disable || "$mode" = --disable ]]; then set +e compose_cmd stop crowdsec rc=$? set -e [[ $rc -eq 0 ]] || printf 'CrowdSec engine was not running or could not be stopped.\n' env_set CROWDSEC_LOCAL_ENABLED false printf 'CrowdSec local stack disabled. The engine container is stopped; volumes and .env key were kept.\n' exit 0 fi if [[ "$mode" = status || "$mode" = --status ]]; then enabled=no [[ "$(env_get CROWDSEC_LOCAL_ENABLED 2>/dev/null || true)" = true ]] && enabled=yes port="$(env_get CROWDSEC_LAPI_PORT 2>/dev/null || true)" [[ -z "$port" ]] && port="$DEFAULT_PORT" printf 'CROWDSEC_LOCAL_ENABLED=%s\n' "$enabled" if container_running; then printf 'Engine: running\n' if health_probe "http://127.0.0.1:$port/health"; then printf 'LAPI health: OK (127.0.0.1:%s)\n' "$port" else printf 'LAPI health: UNREACHABLE (127.0.0.1:%s)\n' "$port" fi else printf 'Engine: not running\n' printf 'Start with: bash cms security\n' fi exit 0 fi port="$(env_get CROWDSEC_LAPI_PORT 2>/dev/null || true)" [[ -n "$port" ]] || port="$DEFAULT_PORT" [[ "$port" =~ ^[0-9]{1,5}$ ]] || fail "CROWDSEC_LAPI_PORT must be a port number." if (( port < 1024 || port > 65535 )); then fail "CROWDSEC_LAPI_PORT must be within 1024-65535." fi key="$(env_get CROWDSEC_LAPI_API_KEY 2>/dev/null || true)" [[ -n "$key" ]] || key="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')" url="$(env_get CROWDSEC_LAPI_URL 2>/dev/null || true)" [[ -n "$url" ]] || url="http://127.0.0.1:$port" log_dir="${CROWDSEC_NGINX_LOG_DIR:-$(env_get CROWDSEC_NGINX_LOG_DIR 2>/dev/null || true)}" [[ -n "$log_dir" ]] || log_dir="/var/log/nginx" if ! container_running && command -v ss >/dev/null 2>&1; then if ss -ltn "( sport = :$port )" 2>/dev/null | grep -q LISTEN; then fail "Port $port is already in use. Set CROWDSEC_LAPI_PORT (and CROWDSEC_LAPI_URL) in .env to a free port and re-run." fi fi if [[ ! -r "$log_dir/access.log" ]]; then printf 'Warning: %s/access.log is not readable. The engine will run but has no detections until an access log is available.\n' "$log_dir" fi env_set CROWDSEC_LOCAL_ENABLED true env_set CROWDSEC_LAPI_URL "$url" env_set CROWDSEC_LAPI_PORT "$port" env_set CROWDSEC_LAPI_API_KEY "$key" env_set CROWDSEC_NGINX_LOG_DIR "$log_dir" compose_cmd config --quiet || fail "CrowdSec Compose configuration is invalid; fix CROWDSEC_* settings in .env." set +e compose_cmd up -d --wait crowdsec rc=$? set -e if [[ $rc -ne 0 ]]; then compose_cmd up -d crowdsec fi attempt=0 while ! health_probe "http://127.0.0.1:$port/health"; do attempt=$((attempt + 1)) [[ $attempt -lt 30 ]] || fail "CrowdSec LAPI did not become healthy on port $port." sleep 2 done printf 'CrowdSec engine running in LAPI-only mode on 127.0.0.1:%s (container %s).\n' "$port" "$CONTAINER_NAME" compose_cmd exec -T crowdsec cscli bouncers list >/dev/null 2>&1 \ && printf 'Bouncer "cms" was registered against the local LAPI.\n' \ || printf 'Warning: could not list bouncers. Diagnose with: docker compose exec -T %s cscli bouncers list\n' "$CONTAINER_NAME" printf 'Restart the CMS container (or run your next deployment) so it loads the new bouncer env. For a clone: bash cms update --skip-pull\n'