diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index caaf82d..2e15a59 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,6 +21,12 @@ jobs: matrix: scenario: - stream-mode + - live-mode + - none-mode + - trusted-ips + - custom-ban-page + - captcha + - appsec steps: - uses: actions/checkout@v6 - name: Run scenario diff --git a/Makefile b/Makefile index eab2bb5..e02f58b 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ export GO111MODULE=on -E2E_SCENARIOS := stream-mode +E2E_SCENARIOS := stream-mode live-mode none-mode trusted-ips custom-ban-page captcha appsec default: lint test diff --git a/tests/e2e/scenarios/appsec/acquis.yaml b/tests/e2e/scenarios/appsec/acquis.yaml new file mode 100644 index 0000000..cfc4d36 --- /dev/null +++ b/tests/e2e/scenarios/appsec/acquis.yaml @@ -0,0 +1,6 @@ +listen_addr: 0.0.0.0:7422 +appsec_config: crowdsecurity/virtual-patching +name: e2eAppSec +source: appsec +labels: + type: appsec diff --git a/tests/e2e/scenarios/appsec/docker-compose.yml b/tests/e2e/scenarios/appsec/docker-compose.yml new file mode 100644 index 0000000..5465751 --- /dev/null +++ b/tests/e2e/scenarios/appsec/docker-compose.yml @@ -0,0 +1,54 @@ +services: + traefik: + image: traefik:v3.7.1 + container_name: e2e-appsec-traefik + command: + - "--log.level=INFO" + - "--accesslog" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" + - "--entryPoints.web.forwardedHeaders.insecure=true" + - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ../../../..:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin + ports: + - "8000:80" + depends_on: + crowdsec: + condition: service_healthy + + whoami: + image: traefik/whoami + container_name: e2e-appsec-whoami + labels: + - "traefik.enable=true" + - "traefik.http.routers.r.rule=PathPrefix(`/foo`)" + - "traefik.http.routers.r.entrypoints=web" + - "traefik.http.routers.r.middlewares=bouncer@docker" + - "traefik.http.services.s.loadbalancer.server.port=80" + - "traefik.http.middlewares.bouncer.plugin.bouncer.enabled=true" + # IP bouncing disabled — we only test AppSec body inspection here. + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecmode=none" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecappsecenabled=true" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecappsechost=crowdsec:7422" + - "traefik.http.middlewares.bouncer.plugin.bouncer.forwardedheaderstrustedips=172.16.0.0/12" + + crowdsec: + image: crowdsecurity/crowdsec:v1.7.8 + container_name: crowdsec + environment: + COLLECTIONS: "crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules" + BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5 + CUSTOM_HOSTNAME: crowdsec + volumes: + - ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro + healthcheck: + test: ["CMD", "cscli", "lapi", "status"] + interval: 2s + timeout: 3s + retries: 60 + start_period: 15s diff --git a/tests/e2e/scenarios/appsec/run.sh b/tests/e2e/scenarios/appsec/run.sh new file mode 100755 index 0000000..ab0172e --- /dev/null +++ b/tests/e2e/scenarios/appsec/run.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" + +SCENARIO=appsec +PROJECT="e2e-${SCENARIO}" +COMPOSE_FILE="$HERE/docker-compose.yml" +LOG_FILE="/tmp/e2e-${SCENARIO}.log" + +cleanup() { + local rc=$? + if (( rc != 0 )); then + dump_diagnostics "$PROJECT" "$COMPOSE_FILE" > "$LOG_FILE" 2>&1 || true + echo "Scenario failed. Logs written to $LOG_FILE" + fi + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true + exit $rc +} +trap cleanup EXIT + +echo "[$SCENARIO] starting stack (AppSec collections download — may take ~30s on first boot)..." +docker compose -p "$PROJECT" -f "$COMPOSE_FILE" up -d --wait + +echo "[$SCENARIO] waiting for crowdsec readiness..." +wait_crowdsec_ready crowdsec 180 + +echo "[$SCENARIO] waiting for traefik readiness..." +wait_for_status http://localhost:8000/foo 200 30 + +echo "[$SCENARIO] benign request must pass" +assert_status http://localhost:8000/foo 200 + +echo "[$SCENARIO] SQL-injection-like query string must be blocked by AppSec virtual patching" +# CRS-style SQLi probe — caught by crowdsecurity/appsec-generic-rules. +assert_status "http://localhost:8000/foo?id=1%27%20UNION%20SELECT%201%2C2%2C3--" 403 + +echo "[$SCENARIO] OK" diff --git a/tests/e2e/scenarios/captcha/captcha.html b/tests/e2e/scenarios/captcha/captcha.html new file mode 100644 index 0000000..94736cc --- /dev/null +++ b/tests/e2e/scenarios/captcha/captcha.html @@ -0,0 +1,9 @@ + + +
IP: {{ .ClientIP }} reason: {{ .RemediationReason }}
+ + diff --git a/tests/e2e/scenarios/custom-ban-page/docker-compose.yml b/tests/e2e/scenarios/custom-ban-page/docker-compose.yml new file mode 100644 index 0000000..4a8b7ac --- /dev/null +++ b/tests/e2e/scenarios/custom-ban-page/docker-compose.yml @@ -0,0 +1,52 @@ +services: + traefik: + image: traefik:v3.7.1 + container_name: e2e-banpage-traefik + command: + - "--log.level=INFO" + - "--accesslog" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" + - "--entryPoints.web.forwardedHeaders.insecure=true" + - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ../../../..:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin + - ./ban.html:/ban.html:ro + ports: + - "8000:80" + depends_on: + crowdsec: + condition: service_healthy + + whoami: + image: traefik/whoami + container_name: e2e-banpage-whoami + labels: + - "traefik.enable=true" + - "traefik.http.routers.r.rule=PathPrefix(`/foo`)" + - "traefik.http.routers.r.entrypoints=web" + - "traefik.http.routers.r.middlewares=bouncer@docker" + - "traefik.http.services.s.loadbalancer.server.port=80" + - "traefik.http.middlewares.bouncer.plugin.bouncer.enabled=true" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecmode=stream" + - "traefik.http.middlewares.bouncer.plugin.bouncer.updateintervalseconds=3" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" + - "traefik.http.middlewares.bouncer.plugin.bouncer.forwardedheaderstrustedips=172.16.0.0/12" + - "traefik.http.middlewares.bouncer.plugin.bouncer.banhtmlfilepath=/ban.html" + + crowdsec: + image: crowdsecurity/crowdsec:v1.7.8 + container_name: crowdsec + environment: + DISABLE_ONLINE_API: "true" + BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5 + CUSTOM_HOSTNAME: crowdsec + healthcheck: + test: ["CMD", "cscli", "lapi", "status"] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/tests/e2e/scenarios/custom-ban-page/run.sh b/tests/e2e/scenarios/custom-ban-page/run.sh new file mode 100755 index 0000000..02ca143 --- /dev/null +++ b/tests/e2e/scenarios/custom-ban-page/run.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" + +SCENARIO=custom-ban-page +PROJECT="e2e-${SCENARIO}" +COMPOSE_FILE="$HERE/docker-compose.yml" +LOG_FILE="/tmp/e2e-${SCENARIO}.log" + +cleanup() { + local rc=$? + if (( rc != 0 )); then + dump_diagnostics "$PROJECT" "$COMPOSE_FILE" > "$LOG_FILE" 2>&1 || true + echo "Scenario failed. Logs written to $LOG_FILE" + fi + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true + exit $rc +} +trap cleanup EXIT + +echo "[$SCENARIO] starting stack..." +docker compose -p "$PROJECT" -f "$COMPOSE_FILE" up -d --wait + +echo "[$SCENARIO] waiting for crowdsec readiness..." +wait_crowdsec_ready + +echo "[$SCENARIO] waiting for traefik readiness..." +wait_for_status http://localhost:8000/foo 200 30 + +echo "[$SCENARIO] adding ban decision" +docker exec crowdsec cscli decisions add --ip 1.2.3.4 --type ban --duration 5m + +echo "[$SCENARIO] waiting one stream tick + buffer..." +sleep 6 + +echo "[$SCENARIO] banned response status is 403" +assert_status http://localhost:8000/foo 403 -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] banned response Content-Type is HTML" +assert_header http://localhost:8000/foo Content-Type "text/html; charset=utf-8" -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] banned response body contains the custom marker" +body=$(curl -s http://localhost:8000/foo -H "X-Forwarded-For: 1.2.3.4") +if ! grep -q "E2E_CUSTOM_BAN_PAGE_MARKER" <<<"$body"; then + echo "Expected response body to contain E2E_CUSTOM_BAN_PAGE_MARKER, got:" >&2 + echo "$body" >&2 + exit 1 +fi + +echo "[$SCENARIO] OK" diff --git a/tests/e2e/scenarios/live-mode/docker-compose.yml b/tests/e2e/scenarios/live-mode/docker-compose.yml new file mode 100644 index 0000000..1c657c7 --- /dev/null +++ b/tests/e2e/scenarios/live-mode/docker-compose.yml @@ -0,0 +1,50 @@ +services: + traefik: + image: traefik:v3.7.1 + container_name: e2e-live-traefik + command: + - "--log.level=INFO" + - "--accesslog" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" + - "--entryPoints.web.forwardedHeaders.insecure=true" + - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ../../../..:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin + ports: + - "8000:80" + depends_on: + crowdsec: + condition: service_healthy + + whoami: + image: traefik/whoami + container_name: e2e-live-whoami + labels: + - "traefik.enable=true" + - "traefik.http.routers.r.rule=PathPrefix(`/foo`)" + - "traefik.http.routers.r.entrypoints=web" + - "traefik.http.routers.r.middlewares=bouncer@docker" + - "traefik.http.services.s.loadbalancer.server.port=80" + - "traefik.http.middlewares.bouncer.plugin.bouncer.enabled=true" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecmode=live" + - "traefik.http.middlewares.bouncer.plugin.bouncer.defaultdecisionseconds=2" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" + - "traefik.http.middlewares.bouncer.plugin.bouncer.forwardedheaderstrustedips=172.16.0.0/12" + + crowdsec: + image: crowdsecurity/crowdsec:v1.7.8 + container_name: crowdsec + environment: + DISABLE_ONLINE_API: "true" + BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5 + CUSTOM_HOSTNAME: crowdsec + healthcheck: + test: ["CMD", "cscli", "lapi", "status"] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/tests/e2e/scenarios/live-mode/run.sh b/tests/e2e/scenarios/live-mode/run.sh new file mode 100755 index 0000000..2324105 --- /dev/null +++ b/tests/e2e/scenarios/live-mode/run.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" + +SCENARIO=live-mode +PROJECT="e2e-${SCENARIO}" +COMPOSE_FILE="$HERE/docker-compose.yml" +LOG_FILE="/tmp/e2e-${SCENARIO}.log" + +cleanup() { + local rc=$? + if (( rc != 0 )); then + dump_diagnostics "$PROJECT" "$COMPOSE_FILE" > "$LOG_FILE" 2>&1 || true + echo "Scenario failed. Logs written to $LOG_FILE" + fi + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true + exit $rc +} +trap cleanup EXIT + +echo "[$SCENARIO] starting stack..." +docker compose -p "$PROJECT" -f "$COMPOSE_FILE" up -d --wait + +echo "[$SCENARIO] waiting for crowdsec readiness..." +wait_crowdsec_ready + +echo "[$SCENARIO] waiting for traefik readiness..." +wait_for_status http://localhost:8000/foo 200 30 + +echo "[$SCENARIO] no decision -> first hit queries LAPI, returns 200, caches 'allowed' for 2s" +assert_status http://localhost:8000/foo 200 -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] adding ban decision for 1.2.3.4" +docker exec crowdsec cscli decisions add --ip 1.2.3.4 --type ban --duration 5m + +echo "[$SCENARIO] waiting for defaultDecisionSeconds cache to expire..." +sleep 3 + +echo "[$SCENARIO] next hit must re-query LAPI and now see the ban" +assert_status http://localhost:8000/foo 403 -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] another non-banned IP must still pass" +assert_status http://localhost:8000/foo 200 -H "X-Forwarded-For: 5.6.7.8" + +echo "[$SCENARIO] OK" diff --git a/tests/e2e/scenarios/none-mode/docker-compose.yml b/tests/e2e/scenarios/none-mode/docker-compose.yml new file mode 100644 index 0000000..e571183 --- /dev/null +++ b/tests/e2e/scenarios/none-mode/docker-compose.yml @@ -0,0 +1,49 @@ +services: + traefik: + image: traefik:v3.7.1 + container_name: e2e-none-traefik + command: + - "--log.level=INFO" + - "--accesslog" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" + - "--entryPoints.web.forwardedHeaders.insecure=true" + - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ../../../..:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin + ports: + - "8000:80" + depends_on: + crowdsec: + condition: service_healthy + + whoami: + image: traefik/whoami + container_name: e2e-none-whoami + labels: + - "traefik.enable=true" + - "traefik.http.routers.r.rule=PathPrefix(`/foo`)" + - "traefik.http.routers.r.entrypoints=web" + - "traefik.http.routers.r.middlewares=bouncer@docker" + - "traefik.http.services.s.loadbalancer.server.port=80" + - "traefik.http.middlewares.bouncer.plugin.bouncer.enabled=true" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecmode=none" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" + - "traefik.http.middlewares.bouncer.plugin.bouncer.forwardedheaderstrustedips=172.16.0.0/12" + + crowdsec: + image: crowdsecurity/crowdsec:v1.7.8 + container_name: crowdsec + environment: + DISABLE_ONLINE_API: "true" + BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5 + CUSTOM_HOSTNAME: crowdsec + healthcheck: + test: ["CMD", "cscli", "lapi", "status"] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/tests/e2e/scenarios/none-mode/run.sh b/tests/e2e/scenarios/none-mode/run.sh new file mode 100755 index 0000000..328ce71 --- /dev/null +++ b/tests/e2e/scenarios/none-mode/run.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" + +SCENARIO=none-mode +PROJECT="e2e-${SCENARIO}" +COMPOSE_FILE="$HERE/docker-compose.yml" +LOG_FILE="/tmp/e2e-${SCENARIO}.log" + +cleanup() { + local rc=$? + if (( rc != 0 )); then + dump_diagnostics "$PROJECT" "$COMPOSE_FILE" > "$LOG_FILE" 2>&1 || true + echo "Scenario failed. Logs written to $LOG_FILE" + fi + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true + exit $rc +} +trap cleanup EXIT + +echo "[$SCENARIO] starting stack..." +docker compose -p "$PROJECT" -f "$COMPOSE_FILE" up -d --wait + +echo "[$SCENARIO] waiting for crowdsec readiness..." +wait_crowdsec_ready + +echo "[$SCENARIO] waiting for traefik readiness..." +wait_for_status http://localhost:8000/foo 200 30 + +echo "[$SCENARIO] no decision -> request passes (LAPI queried per request)" +assert_status http://localhost:8000/foo 200 -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] adding ban decision" +docker exec crowdsec cscli decisions add --ip 1.2.3.4 --type ban --duration 5m + +echo "[$SCENARIO] none mode has no cache -> next request must be blocked immediately" +assert_status http://localhost:8000/foo 403 -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] deleting decision" +docker exec crowdsec cscli decisions delete --ip 1.2.3.4 + +echo "[$SCENARIO] previously banned IP must pass again immediately" +assert_status http://localhost:8000/foo 200 -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] OK" diff --git a/tests/e2e/scenarios/stream-mode/docker-compose.yml b/tests/e2e/scenarios/stream-mode/docker-compose.yml index c850647..4fdabbf 100644 --- a/tests/e2e/scenarios/stream-mode/docker-compose.yml +++ b/tests/e2e/scenarios/stream-mode/docker-compose.yml @@ -1,18 +1,18 @@ services: traefik: - image: traefik:v3.5.0 + image: traefik:v3.7.1 container_name: e2e-stream-traefik command: - "--log.level=INFO" - "--accesslog" - "--api.insecure=true" - - "--providers.file.filename=/etc/traefik/dynamic.yml" - - "--providers.file.watch=false" - - "--entrypoints.web.address=:80" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" - "--entryPoints.web.forwardedHeaders.insecure=true" - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" volumes: - - ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro + - /var/run/docker.sock:/var/run/docker.sock:ro - ../../../..:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin ports: - "8000:80" @@ -23,9 +23,20 @@ services: whoami: image: traefik/whoami container_name: e2e-stream-whoami + labels: + - "traefik.enable=true" + - "traefik.http.routers.r.rule=PathPrefix(`/foo`)" + - "traefik.http.routers.r.entrypoints=web" + - "traefik.http.routers.r.middlewares=bouncer@docker" + - "traefik.http.services.s.loadbalancer.server.port=80" + - "traefik.http.middlewares.bouncer.plugin.bouncer.enabled=true" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecmode=stream" + - "traefik.http.middlewares.bouncer.plugin.bouncer.updateintervalseconds=3" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" + - "traefik.http.middlewares.bouncer.plugin.bouncer.forwardedheaderstrustedips=172.16.0.0/12" crowdsec: - image: crowdsecurity/crowdsec:v1.6.8 + image: crowdsecurity/crowdsec:v1.7.8 container_name: crowdsec environment: DISABLE_ONLINE_API: "true" diff --git a/tests/e2e/scenarios/stream-mode/traefik-dynamic.yml b/tests/e2e/scenarios/stream-mode/traefik-dynamic.yml deleted file mode 100644 index 9cbe9cf..0000000 --- a/tests/e2e/scenarios/stream-mode/traefik-dynamic.yml +++ /dev/null @@ -1,22 +0,0 @@ -http: - routers: - whoami: - rule: "PathPrefix(`/foo`)" - entryPoints: [web] - service: whoami - middlewares: [bouncer] - services: - whoami: - loadBalancer: - servers: - - url: "http://whoami:80" - middlewares: - bouncer: - plugin: - bouncer: - enabled: true - crowdsecMode: stream - updateIntervalSeconds: 3 - crowdsecLapiKey: "40796d93c2958f9e58345514e67740e5" - forwardedHeadersTrustedIPs: - - "172.16.0.0/12" diff --git a/tests/e2e/scenarios/trusted-ips/docker-compose.yml b/tests/e2e/scenarios/trusted-ips/docker-compose.yml new file mode 100644 index 0000000..3fb0348 --- /dev/null +++ b/tests/e2e/scenarios/trusted-ips/docker-compose.yml @@ -0,0 +1,51 @@ +services: + traefik: + image: traefik:v3.7.1 + container_name: e2e-trusted-traefik + command: + - "--log.level=INFO" + - "--accesslog" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:80" + - "--entryPoints.web.forwardedHeaders.insecure=true" + - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ../../../..:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin + ports: + - "8000:80" + depends_on: + crowdsec: + condition: service_healthy + + whoami: + image: traefik/whoami + container_name: e2e-trusted-whoami + labels: + - "traefik.enable=true" + - "traefik.http.routers.r.rule=PathPrefix(`/foo`)" + - "traefik.http.routers.r.entrypoints=web" + - "traefik.http.routers.r.middlewares=bouncer@docker" + - "traefik.http.services.s.loadbalancer.server.port=80" + - "traefik.http.middlewares.bouncer.plugin.bouncer.enabled=true" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdsecmode=stream" + - "traefik.http.middlewares.bouncer.plugin.bouncer.updateintervalseconds=3" + - "traefik.http.middlewares.bouncer.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" + - "traefik.http.middlewares.bouncer.plugin.bouncer.forwardedheaderstrustedips=172.16.0.0/12" + - "traefik.http.middlewares.bouncer.plugin.bouncer.clienttrustedips=1.2.3.4/32" + + crowdsec: + image: crowdsecurity/crowdsec:v1.7.8 + container_name: crowdsec + environment: + DISABLE_ONLINE_API: "true" + BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5 + CUSTOM_HOSTNAME: crowdsec + healthcheck: + test: ["CMD", "cscli", "lapi", "status"] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/tests/e2e/scenarios/trusted-ips/run.sh b/tests/e2e/scenarios/trusted-ips/run.sh new file mode 100755 index 0000000..479bc5e --- /dev/null +++ b/tests/e2e/scenarios/trusted-ips/run.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" + +SCENARIO=trusted-ips +PROJECT="e2e-${SCENARIO}" +COMPOSE_FILE="$HERE/docker-compose.yml" +LOG_FILE="/tmp/e2e-${SCENARIO}.log" + +cleanup() { + local rc=$? + if (( rc != 0 )); then + dump_diagnostics "$PROJECT" "$COMPOSE_FILE" > "$LOG_FILE" 2>&1 || true + echo "Scenario failed. Logs written to $LOG_FILE" + fi + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true + exit $rc +} +trap cleanup EXIT + +echo "[$SCENARIO] starting stack..." +docker compose -p "$PROJECT" -f "$COMPOSE_FILE" up -d --wait + +echo "[$SCENARIO] waiting for crowdsec readiness..." +wait_crowdsec_ready + +echo "[$SCENARIO] waiting for traefik readiness..." +wait_for_status http://localhost:8000/foo 200 30 + +echo "[$SCENARIO] banning the trusted IP 1.2.3.4 and an untrusted IP 5.6.7.8" +docker exec crowdsec cscli decisions add --ip 1.2.3.4 --type ban --duration 5m +docker exec crowdsec cscli decisions add --ip 5.6.7.8 --type ban --duration 5m + +echo "[$SCENARIO] waiting one stream tick + buffer..." +sleep 6 + +echo "[$SCENARIO] trusted IP must bypass the bouncer even though it is banned" +assert_status http://localhost:8000/foo 200 -H "X-Forwarded-For: 1.2.3.4" + +echo "[$SCENARIO] untrusted banned IP must be blocked (control: proves the bouncer is active)" +assert_status http://localhost:8000/foo 403 -H "X-Forwarded-For: 5.6.7.8" + +echo "[$SCENARIO] OK"