From 7392b843adab4329df074bd9bd358cbb18c1b966 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 24 Sep 2026 18:59:13 +0200 Subject: [PATCH] fix(security): LAPI-only engine boot, import via stdin, correct compose service --- README.md | 24 +++++++++++++++++------- deployment/crowdsec/compose.crowdsec.yml | 4 ++-- docs/operations/docker-installation.md | 4 ++-- scripts/blocklists-sync.sh | 4 ++-- scripts/crowdsec-setup.sh | 2 +- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e6b74731..999240ed 100644 --- a/README.md +++ b/README.md @@ -778,12 +778,21 @@ Open **DevOps → Anti-DDoS protection** ## Local CrowdSec Engine (opt-in) The repository ships a self-contained CrowdSec engine that runs on the same -Docker host. It reads the host Nginx access log, runs -`crowdsecurity/crowdsec:1.8.1` in its own Compose project and exposes LAPI only -on `127.0.0.1:18080`. When enabled, the app-layer anti-DDoS gate -(`src/lib/crowdsec-local.ts`) asks the local LAPI per client IP (short-cached) -and blocks `ban` / `captcha` decisions before its own rate buckets run. -No reverse-proxy, Traefik, Cloudflare or firewall configuration is changed. +Docker host. It runs `crowdsecurity/crowdsec:v1.8.1` in its own Compose project +and exposes **LAPI only** on `127.0.0.1:18080`. When enabled, the app-layer +anti-DDoS gate (`src/lib/crowdsec-local.ts`) asks the local LAPI per client IP +(short-cached) and blocks `ban` / `captcha` decisions before its own rate +buckets run. No reverse-proxy, Traefik, Cloudflare or firewall configuration is +changed. + +The engine boots in **LAPI-only mode** (`DISABLE_AGENT=true`): it does not +consume the host Nginx access log and needs no outbound access to +`crowdsec.net`, which is often blocked on hardened hosts. Combined with +`DISABLE_ONLINE_API=true` (no CrowdSec Central API) the engine needs no account +and no inbound internet — blocking comes from the imported blocklists +(Step 4) and the app's own rate buckets. Re-enable the agent only on a host +with outbound internet by removing `DISABLE_AGENT: "true"` from +`deployment/crowdsec/compose.crowdsec.yml`. ### Step 1 — Enable the engine and register the bouncer @@ -794,7 +803,8 @@ bash cms security This generates `CROWDSEC_LAPI_API_KEY` (random 64 hex chars), writes the CrowdSec flags into `.env`, starts the engine and registers the `cms` bouncer against the local LAPI. The engine does **not** enroll into the CrowdSec -Central API (`DISABLE_ONLINE_API=true`): detection stays local. +Central API (`DISABLE_ONLINE_API=true`) and runs without the agent +(`DISABLE_AGENT=true`), so it never phones home. ### Step 2 — Restart the CMS so it loads the bouncer credentials diff --git a/deployment/crowdsec/compose.crowdsec.yml b/deployment/crowdsec/compose.crowdsec.yml index 2b2bb67e..6093c1cb 100644 --- a/deployment/crowdsec/compose.crowdsec.yml +++ b/deployment/crowdsec/compose.crowdsec.yml @@ -1,11 +1,11 @@ services: crowdsec: - image: crowdsecurity/crowdsec:${CROWDSEC_VERSION:-1.8.1} + image: crowdsecurity/crowdsec:${CROWDSEC_VERSION:-v1.8.1} container_name: epicnext-crowdsec restart: unless-stopped profiles: ["security"] environment: - COLLECTIONS: ${CROWDSEC_COLLECTIONS:-crowdsecurity/nginx} + DISABLE_AGENT: "true" BOUNCER_KEY_cms: ${CROWDSEC_LAPI_API_KEY:?CROWDSEC_LAPI_API_KEY must be set} DISABLE_ONLINE_API: "true" GID: "${CROWDSEC_GID:-0}" diff --git a/docs/operations/docker-installation.md b/docs/operations/docker-installation.md index 5d837f48..2c3e1cd4 100644 --- a/docs/operations/docker-installation.md +++ b/docs/operations/docker-installation.md @@ -87,7 +87,7 @@ The direct template intentionally records the CDN/edge socket address when place ## Opt-in: CrowdSec on the same Docker host -A self-contained CrowdSec engine ships in `deployment/crowdsec`. It reads the host Nginx access log, runs `crowdsecurity/crowdsec:1.8.1` in its own Compose project and exposes LAPI only on `127.0.0.1:18080`. No reverse-proxy, Traefik, Cloudflare or firewall configuration is changed. +A self-contained CrowdSec engine ships in `deployment/crowdsec`. It runs `crowdsecurity/crowdsec:v1.8.1` in its own Compose project in **LAPI-only mode** (`DISABLE_AGENT=true`) and exposes LAPI only on `127.0.0.1:18080`. No reverse-proxy, Traefik, Cloudflare or firewall configuration is changed. ```sh bash cms security @@ -95,7 +95,7 @@ bash cms security The command generates `CROWDSEC_LAPI_API_KEY`, writes the CrowdSec flags into `.env`, starts the engine and registers the `cms` bouncer. The anti-DDoS gate then consults the local LAPI per client IP (short-cached) and blocks `ban`/`captcha` decisions before its own rate buckets. `bash cms security status` reports engine state and `bash cms security disable` stops the engine and flips the toggle off. -The engine does not enroll into the CrowdSec Central API (`DISABLE_ONLINE_API=true`): detection stays local. The app still has its separate opt-in traffic-sharing channel via `CROWDSEC_REPORT_ENABLED`. Change `CROWDSEC_LAPI_PORT` and `CROWDSEC_LAPI_URL` together when `18080` is already in use. `CROWDSEC_NGINX_LOG_DIR` overrides the log directory the engine acquires. +The engine does not enroll into the CrowdSec Central API (`DISABLE_ONLINE_API=true`) and runs without the agent (`DISABLE_AGENT=true`), so it needs no account and no outbound access to `crowdsec.net` (often blocked on hardened hosts). Blocking comes from the imported blocklists plus the app's own rate buckets; it does not parse the host Nginx log. The app still has its separate opt-in traffic-sharing channel via `CROWDSEC_REPORT_ENABLED`. Change `CROWDSEC_LAPI_PORT` and `CROWDSEC_LAPI_URL` together when `18080` is already in use. `CROWDSEC_NGINX_LOG_DIR` is honored for when the agent is re-enabled. This bouncer is application-layer: it sheds known-bad IPs at the CMS process and only for traffic that reaches the Next.js proxy. It does not drop traffic before the origin, does not protect other host ports/services, and depends on the client IP being trustworthy at the ingress. Keep the upstream protections (Cloudflare IP rules, proxy rate limits) for defense before the origin. diff --git a/scripts/blocklists-sync.sh b/scripts/blocklists-sync.sh index 94bcc7d3..ae861780 100644 --- a/scripts/blocklists-sync.sh +++ b/scripts/blocklists-sync.sh @@ -79,7 +79,7 @@ container_running() { } cscli_exec() { - compose_cmd exec -T "$CONTAINER_NAME" cscli "$@" + compose_cmd exec -T crowdsec cscli "$@" } fetch_sources() { @@ -205,6 +205,6 @@ cscli_exec decisions delete --origin cscli-import >/dev/null 2>&1 || true } > "$work/import.csv" printf 'Importing %s decisions into the local LAPI (duration %s)...\n' "$count_total" "$duration" -cscli_exec decisions import -i "$work/import.csv" --format csv --batch 1000 +cscli_exec decisions import -i - --format csv --batch 1000 < "$work/import.csv" printf 'Done. The app bouncer picks these up within a few seconds.\n' \ No newline at end of file diff --git a/scripts/crowdsec-setup.sh b/scripts/crowdsec-setup.sh index 8c9bbc30..a9a39ba6 100644 --- a/scripts/crowdsec-setup.sh +++ b/scripts/crowdsec-setup.sh @@ -147,7 +147,7 @@ while ! health_probe "http://127.0.0.1:$port/health"; do sleep 2 done -printf 'CrowdSec engine running on 127.0.0.1:%s (container %s), reading %s/access.log.\n' "$port" "$CONTAINER_NAME" "$log_dir" +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"