mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-07-20 11:09:00 +02:00
✨ tests: revert to docker provider + bump versions + add 6 scenarios
Reverts the stream-mode scenario to the Traefik docker provider (the file provider was a workaround for a local docker daemon API version mismatch, irrelevant in CI). Bumps Traefik to v3.7.1 and Crowdsec to v1.7.8 across all scenarios. Adds six new E2E scenarios: - live-mode: short defaultDecisionSeconds, verifies cache-then-recheck - none-mode: verifies LAPI is queried per request, no caching - trusted-ips: clientTrustedIPs bypass even when the trusted IP is banned - custom-ban-page: BanHTMLFilePath body + Content-Type validation - captcha: captcha decision serves the captcha page (HTTP 200) - appsec: SQLi probe blocked by appsec-virtual-patching Each scenario uses an isolated compose project, mounts the repo as a local plugin, and asserts behavior via curl. Workflow matrix and Makefile E2E_SCENARIOS updated accordingly. Refs #328
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
listen_addr: 0.0.0.0:7422
|
||||
appsec_config: crowdsecurity/virtual-patching
|
||||
name: e2eAppSec
|
||||
source: appsec
|
||||
labels:
|
||||
type: appsec
|
||||
@@ -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
|
||||
Executable
+40
@@ -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"
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>E2E captcha marker</title></head>
|
||||
<body>
|
||||
<h1 id="e2e-captcha-marker">E2E_CAPTCHA_PAGE_MARKER</h1>
|
||||
<script src="{{ .FrontendJS }}"></script>
|
||||
<div class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.7.1
|
||||
container_name: e2e-captcha-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
|
||||
- ./captcha.html:/captcha.html:ro
|
||||
ports:
|
||||
- "8000:80"
|
||||
depends_on:
|
||||
crowdsec:
|
||||
condition: service_healthy
|
||||
|
||||
whoami:
|
||||
image: traefik/whoami
|
||||
container_name: e2e-captcha-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.captchaprovider=turnstile"
|
||||
# Cloudflare Turnstile public test keys (always pass / always fail variants exist).
|
||||
# These render a valid widget without contacting any real API key.
|
||||
- "traefik.http.middlewares.bouncer.plugin.bouncer.captchasitekey=1x00000000000000000000AA"
|
||||
- "traefik.http.middlewares.bouncer.plugin.bouncer.captchasecretkey=1x0000000000000000000000000000000AA"
|
||||
- "traefik.http.middlewares.bouncer.plugin.bouncer.captchahtmlfilepath=/captcha.html"
|
||||
- "traefik.http.middlewares.bouncer.plugin.bouncer.captchagraceperiodseconds=10"
|
||||
|
||||
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
|
||||
Executable
+53
@@ -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=captcha
|
||||
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 captcha decision for 1.2.3.4"
|
||||
docker exec crowdsec cscli decisions add --ip 1.2.3.4 --type captcha --duration 5m
|
||||
|
||||
echo "[$SCENARIO] waiting one stream tick + buffer..."
|
||||
sleep 6
|
||||
|
||||
echo "[$SCENARIO] captcha response must be HTTP 200 (the captcha page itself, not a 403)"
|
||||
assert_status http://localhost:8000/foo 200 -H "X-Forwarded-For: 1.2.3.4"
|
||||
|
||||
echo "[$SCENARIO] captcha response body must contain the captcha template marker"
|
||||
body=$(curl -s http://localhost:8000/foo -H "X-Forwarded-For: 1.2.3.4")
|
||||
if ! grep -q "E2E_CAPTCHA_PAGE_MARKER" <<<"$body"; then
|
||||
echo "Expected response body to contain E2E_CAPTCHA_PAGE_MARKER, got:" >&2
|
||||
echo "$body" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$SCENARIO] non-flagged IP must still pass through to the backend"
|
||||
assert_status http://localhost:8000/foo 200 -H "X-Forwarded-For: 5.6.7.8"
|
||||
|
||||
echo "[$SCENARIO] OK"
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>E2E ban marker</title></head>
|
||||
<body>
|
||||
<h1 id="e2e-ban-marker">E2E_CUSTOM_BAN_PAGE_MARKER</h1>
|
||||
<p>IP: {{ .ClientIP }} reason: {{ .RemediationReason }}</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
Executable
+53
@@ -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"
|
||||
@@ -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
|
||||
Executable
+48
@@ -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"
|
||||
@@ -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
|
||||
Executable
+48
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Executable
+46
@@ -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"
|
||||
Reference in New Issue
Block a user