diff --git a/.env.example b/.env.example index 6788249..7f99113 100644 --- a/.env.example +++ b/.env.example @@ -84,6 +84,13 @@ AUTHENTIK_EMAIL__FROM=authentik@localhost COMPOSE_PORT_HTTP=80 COMPOSE_PORT_HTTPS=443 +# CrowdSec (Swarm) +# Generate a value with: openssl rand -base64 32 +CROWDSEC_LAPI_KEY=change-me +# Must match a bouncer key registered in CrowdSec, for example: +# cscli bouncers add traefik-bouncer --key +CROWDSEC_BOUNCER_KEY=change-me + #=====================================================================# # LibreChat Configuration # #=====================================================================# diff --git a/README.md b/README.md index b3551cf..fc73118 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,56 @@ make deploy-core docker service logs core_traefik --details ``` +## CrowdSec Runbook (Swarm + Traefik) + +### 1) Set keys in swarm env +In `swarm/.env`, set: + +```env +CROWDSEC_LAPI_KEY= +CROWDSEC_BOUNCER_KEY= +``` + +### 2) Deploy CrowdSec stack +```bash +make deploy-crowdsec +``` + +### 3) Register the Traefik bouncer +```bash +make register-crowdsec-bouncer +``` + +### 4) (Re)deploy Traefik core +This ensures the global HTTPS forwardAuth middleware is active. + +```bash +make deploy-core +``` + +### 5) Verify health and wiring +```bash +docker service ls | grep crowdsec +docker service logs --since 10m crowdsec_traefik-bouncer +docker service logs --since 10m core_traefik +docker ps --filter label=com.docker.swarm.service.name=crowdsec_crowdsec +``` + +### 6) Verify decisions and blocking +```bash +# Show parser/scenario metrics +docker exec -it $(docker ps --filter label=com.docker.swarm.service.name=crowdsec_crowdsec -q | head -n1) cscli metrics + +# Add a temporary test decision for your client IP and verify HTTPS returns 403 +docker exec -it $(docker ps --filter label=com.docker.swarm.service.name=crowdsec_crowdsec -q | head -n1) cscli decisions add --ip +``` + +Cleanup test decision when done: + +```bash +docker exec -it $(docker ps --filter label=com.docker.swarm.service.name=crowdsec_crowdsec -q | head -n1) cscli decisions delete --ip +``` + ### Notes: ```yaml # Declaring the user list diff --git a/makefile b/makefile index 471a612..e602b10 100644 --- a/makefile +++ b/makefile @@ -33,6 +33,37 @@ deploy-core: pull remove-core: docker stack rm core +setup-crowdsec: + { \ + echo "Setting up crowdsec config on host..." ;\ + mkdir -p /var/data/crowdsec ;\ + cmp -s ./swarm/crowdsec/acquis.yaml /var/data/crowdsec/acquis.yaml 2>/dev/null || cp ./swarm/crowdsec/acquis.yaml /var/data/crowdsec/acquis.yaml ;\ + } + +deploy-crowdsec: pull setup-crowdsec + { \ + echo "Deploying the crowdsec stack..." ;\ + set -a ;\ + . ./swarm/.env ;\ + set +a ;\ + docker stack deploy --compose-file ./swarm/crowdsec.yml crowdsec ;\ + } + +register-crowdsec-bouncer: + { \ + echo "Registering crowdsec traefik bouncer..." ;\ + set -a ;\ + . ./swarm/.env ;\ + set +a ;\ + bash ./scripts/register-crowdsec-bouncer.sh ;\ + } + +deploy-crowdsec-all: deploy-crowdsec register-crowdsec-bouncer deploy-core + @echo "CrowdSec + bouncer registration + core deploy completed." + +remove-crowdsec: + docker stack rm crowdsec + deploy-portainer: pull { \ echo "Deploying the portainer stack..." ;\ diff --git a/scripts/register-crowdsec-bouncer.sh b/scripts/register-crowdsec-bouncer.sh new file mode 100755 index 0000000..150c319 --- /dev/null +++ b/scripts/register-crowdsec-bouncer.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +BOUNCER_NAME="${CROWDSEC_BOUNCER_NAME:-traefik-bouncer}" +BOUNCER_KEY="${CROWDSEC_BOUNCER_KEY:-}" +SERVICE_NAME="crowdsec_crowdsec" + +if [[ -z "$BOUNCER_KEY" ]]; then + echo "CROWDSEC_BOUNCER_KEY is not set. Export it or source swarm/.env first." + exit 1 +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "docker command not found in PATH." + exit 1 +fi + +container_id="$(docker ps --filter "label=com.docker.swarm.service.name=${SERVICE_NAME}" --format '{{.ID}}' | head -n1)" + +if [[ -z "$container_id" ]]; then + echo "No running CrowdSec LAPI task found for service ${SERVICE_NAME}." + echo "Deploy CrowdSec first: make deploy-crowdsec" + exit 1 +fi + +if docker exec "$container_id" cscli bouncers list 2>/dev/null | grep -q "$BOUNCER_NAME"; then + echo "Bouncer '${BOUNCER_NAME}' is already registered." + exit 0 +fi + +echo "Registering bouncer '${BOUNCER_NAME}' on ${SERVICE_NAME}..." +docker exec "$container_id" cscli bouncers add "$BOUNCER_NAME" --key "$BOUNCER_KEY" +echo "Bouncer registration complete." diff --git a/swarm/core.yml b/swarm/core.yml index 313ca64..aaa1391 100644 --- a/swarm/core.yml +++ b/swarm/core.yml @@ -91,6 +91,8 @@ services: - traefik.http.middlewares.xbot.headers.stsIncludeSubdomains=true - traefik.http.middlewares.xbot.headers.permissionsPolicy="accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), layout-animations=(), legacy-image-formats=(), magnetometer=(), microphone=(), midi=(), oversized-images=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), wake-lock=(), xr-spatial-tracking=()" - traefik.http.middlewares.xbot.headers.referrerPolicy="no-referrer" + - traefik.http.middlewares.crowdsec-bouncer.forwardauth.address=http://traefik-bouncer:8080/api/v1/forwardAuth + - traefik.http.middlewares.crowdsec-bouncer.forwardauth.trustForwardHeader=true - treafik.http.middlewares.neo4j_strip.stripprefix.prefixes=/neo4j # traefik-https the actual router using HTTPS - traefik.http.routers.traefik-public-https.rule=Host(`dashboard.${DOMAIN?Variable not set}`) @@ -106,8 +108,9 @@ services: - traefik.http.services.traefik-public.loadbalancer.server.port=8080 # Pass the original Host header to the backend # - "traefik.http.services.dashboard.loadbalancer.passhostheader=true" - # - crowdsec.enable=true - # - crowdsec.labels.type=nginx + # CrowdSec log acquisition for Traefik is label-based via docker source. + - crowdsec.enable=true + - crowdsec.labels.type=traefik volumes: # Add Docker as a mounted volume, so that Traefik can read the labels of other services - /var/run/docker.sock:/var/run/docker.sock:ro @@ -132,6 +135,8 @@ services: - --entrypoints.http.address=:80 # Create an entrypoint "https" listening on port 443 - --entrypoints.https.address=:443 + # Apply CrowdSec bouncer globally to all HTTPS routers discovered by Traefik. + - --entrypoints.https.http.middlewares=crowdsec-bouncer@swarm # Redirect HTTP to HTTPS - --entrypoints.http.http.redirections.entrypoint.to=https - --entrypoints.http.http.redirections.entrypoint.scheme=https diff --git a/swarm/crowdsec.yml b/swarm/crowdsec.yml index cff3cde..a19bf75 100644 --- a/swarm/crowdsec.yml +++ b/swarm/crowdsec.yml @@ -1,27 +1,38 @@ services: crowdsec: image: crowdsecurity/crowdsec:slim - restart: always networks: - crowdsec: + - crowdsec-internal environment: DOCKER_HOST: tcp://socket-proxy:2375 - COLLECTIONS: "crowdsecurity/nginx" - ports: - - "127.0.0.1:8080:8080" - depends_on: - - 'socket-proxy' + COLLECTIONS: "crowdsecurity/traefik" + CUSTOM_CS_LAPI_KEY: ${CROWDSEC_LAPI_KEY:?Variable not set} + deploy: + restart_policy: + condition: any volumes: - - ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml + - /var/data/crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro - crowdsec-db:/var/lib/crowdsec/data/ - crowdsec-config:/etc/crowdsec/ - socket-proxy: + traefik-bouncer: + image: crowdsecurity/traefik-bouncer:latest + environment: + CROWDSEC_LAPI_URL: http://crowdsec:8080 + CROWDSEC_LAPI_KEY: ${CROWDSEC_BOUNCER_KEY:?Variable not set} + expose: + - "8080" networks: - crowdsec: - restart: always + - crowdsec-internal + - traefik-public + deploy: + restart_policy: + condition: any + + socket-proxy: image: lscr.io/linuxserver/socket-proxy:latest - container_name: socket-proxy + networks: + - crowdsec-internal environment: INFO: 1 CONTAINERS: 1 @@ -38,10 +49,13 @@ services: PLUGINS: 0 SERVICES: 0 SESSION: 0 - SWARM: 0 + SWARM: 1 SYSTEM: 0 TASKS: 0 VOLUMES: 0 + deploy: + restart_policy: + condition: any volumes: - '/var/run/docker.sock:/var/run/docker.sock:ro' read_only: true @@ -53,5 +67,8 @@ volumes: crowdsec-config: networks: - crowdsec: - driver: bridge \ No newline at end of file + crowdsec-internal: + driver: overlay + attachable: true + traefik-public: + external: true \ No newline at end of file diff --git a/swarm/crowdsec/acquis.yaml b/swarm/crowdsec/acquis.yaml index f670b36..b3c1c9e 100644 --- a/swarm/crowdsec/acquis.yaml +++ b/swarm/crowdsec/acquis.yaml @@ -1,3 +1,6 @@ source: docker use_container_labels: true ## https://docs.crowdsec.net/docs/next/data_sources/docker#use_container_labels +# Traefik service must set these docker labels so CrowdSec can classify logs correctly: +# - crowdsec.enable=true +# - crowdsec.labels.type=traefik check_interval: 10s ## How often to check for new containers https://docs.crowdsec.net/docs/next/appsec/quickstart/traefik \ No newline at end of file