From 531efb218d41570cbaaf56c83cb7b963670db3e2 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 19 Jun 2026 17:25:51 +0000 Subject: [PATCH 1/3] traefik: bump crowdsec-bouncer plugin v1.4.2 -> v1.6.0 (fix stream not pulling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crowdsec-bouncer Yaegi plugin pinned at v1.4.2 loads on Traefik 3.7.5 but its decision-stream goroutine never runs — no Traefik pod ever calls the LAPI stream (verified: no traefik-pod bouncer entry / no @pod-ip auto-registration), and it logs nothing. All deps are healthy (LAPI 200 + full ban list reachable from the traefik ns, key valid, redis PONG, config correct, no NetworkPolicies), so CrowdSec enforced nothing despite the bouncer now being registered. This is the Traefik-v3 / Yaegi plugin-incompat class that already killed rewrite-body here. v1.4.2 predates Nov 2025; latest is v1.6.0. Bump to v1.6.0 (initContainer download URL + state.json + experimental.plugins version). Config-verified compatible: every key we use survives (crowdsecMode, crowdsecLapiKey/Host, updateMaxFailure, redisCache*, clientTrustedIPs, all captcha* incl. turnstile); v1.6.0 also moves logging to slog/trace for future diagnosis. Pinned, not auto-updated (Keel can't manage a Yaegi plugin, and plugin bumps must be tested against the running Traefik/Yaegi). Co-Authored-By: Claude Opus 4.8 --- stacks/traefik/modules/traefik/main.tf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stacks/traefik/modules/traefik/main.tf b/stacks/traefik/modules/traefik/main.tf index f0b235e5..1de3fc41 100644 --- a/stacks/traefik/modules/traefik/main.tf +++ b/stacks/traefik/modules/traefik/main.tf @@ -101,12 +101,12 @@ resource "helm_release" "traefik" { "set -e; ", "STORAGE=/plugins-storage; ", "mkdir -p \"$STORAGE/archives/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin\"; ", - "wget -q -T 30 -O \"$STORAGE/archives/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.4.2.zip\" ", - "\"https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/archive/refs/tags/v1.4.2.zip\"; ", + "wget -q -T 30 -O \"$STORAGE/archives/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.6.0.zip\" ", + "\"https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/archive/refs/tags/v1.6.0.zip\"; ", "mkdir -p \"$STORAGE/archives/github.com/Aetherinox/traefik-api-token-middleware\"; ", "wget -q -T 30 -O \"$STORAGE/archives/github.com/Aetherinox/traefik-api-token-middleware/v0.1.4.zip\" ", "\"https://github.com/Aetherinox/traefik-api-token-middleware/archive/refs/tags/v0.1.4.zip\"; ", - "printf '{\"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin\":\"v1.4.2\",\"github.com/Aetherinox/traefik-api-token-middleware\":\"v0.1.4\"}' ", + "printf '{\"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin\":\"v1.6.0\",\"github.com/Aetherinox/traefik-api-token-middleware\":\"v0.1.4\"}' ", "> \"$STORAGE/archives/state.json\"; ", "echo \"Plugins pre-downloaded successfully\"", ])] @@ -232,7 +232,7 @@ resource "helm_release" "traefik" { plugins = { crowdsec-bouncer = { moduleName = "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - version = "v1.4.2" + version = "v1.6.0" } # Static-token bearer/header auth middleware. Used by services that # need gateway-level API-key/bearer enforcement without app-layer auth From 0cc48d83ac63539d0344d80ac3a5ae2e6d281d67 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 19 Jun 2026 17:34:38 +0000 Subject: [PATCH 2/3] =?UTF-8?q?traefik/crowdsec:=20disable=20bouncer=20red?= =?UTF-8?q?is=20cache=20(broken=20under=20Yaegi=20=E2=86=92=20in-memory)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the plugin on v1.6.0 the stream goroutine finally runs, and its slog output revealed the real blocker: `handleStreamTicker ... isCrowdsecStreamHealthy:true cache:unreachable`. The LAPI stream is healthy, but the plugin's redis client cannot reach the cache under Traefik's Yaegi interpreter — even though redis-master.redis.svc is reachable AND writable from the traefik namespace (SET/GET verified via busybox; no NetworkPolicies; no auth). Same interpreter -incompat class as the stream goroutine itself. With redisCacheUnreachableBlock =false the bouncer then failed open and enforced nothing. Disable the redis cache so the plugin uses its in-memory decision store (works under Yaegi). Removes redisCacheHost/redisCacheUnreachableBlock. Trade-off: captcha already-solved grace is per-pod across the 3 Traefik replicas (at worst an occasional re-solve) — acceptable; bans/captcha decisions enforce correctly. Co-Authored-By: Claude Opus 4.8 --- stacks/traefik/modules/traefik/middleware.tf | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/stacks/traefik/modules/traefik/middleware.tf b/stacks/traefik/modules/traefik/middleware.tf index 5f758be4..1837a794 100644 --- a/stacks/traefik/modules/traefik/middleware.tf +++ b/stacks/traefik/modules/traefik/middleware.tf @@ -199,9 +199,15 @@ resource "kubernetes_manifest" "middleware_crowdsec" { crowdsecLapiHost = "crowdsec-service.crowdsec.svc.cluster.local:8080" crowdsecMode = "stream" updateMaxFailure = -1 # fail-open: serve from cache when LAPI is unreachable - redisCacheEnabled = true - redisCacheHost = var.redis_host - redisCacheUnreachableBlock = false # don't block traffic if Redis is also unreachable + # Redis cache DISABLED: the plugin's redis client does not work under + # Traefik's Yaegi interpreter — it logs `cache:unreachable` even though + # redis-master is reachable+writable from the traefik ns (verified). With + # the redis cache enabled + redisCacheUnreachableBlock=false the bouncer + # therefore failed open and enforced nothing. In-memory cache (the + # default when disabled) holds the streamed decision set per-pod and + # works under Yaegi. Trade-off: captcha "already-solved" grace is + # per-pod across the 3 Traefik replicas (at worst an occasional re-solve). + redisCacheEnabled = false clientTrustedIPs = ["10.0.20.0/24", "10.10.0.0/16"] # node + pod CIDRs bypass CrowdSec # Captcha remediation: serve a Cloudflare Turnstile challenge for # `captcha`-type LAPI decisions instead of falling through to a 403 From dd029ca7fbcd6973236b5dbdea1a7f237bfe0a3c Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 19 Jun 2026 17:43:30 +0000 Subject: [PATCH 3/3] traefik/crowdsec: switch bouncer to live mode (stream cache doesn't enforce under Yaegi) After bumping to v1.6.0 (stream goroutine runs) and disabling redis (in-memory cache), the plugin logs `handleStreamCache:updated` but still does NOT enforce: a ban present in the LAPI stream AND pulled by the plugin still let the banned IP through. Stream-mode decision matching is unreliable under Traefik's Yaegi interpreter here. Switch crowdsecMode stream->live: the plugin queries LAPI synchronously per request (result cached per-IP for defaultDecisionSeconds), which enforces reliably and picks up new decisions immediately. LAPI is 3-replica + in-cluster so per-request latency is small; fail-open preserved (updateMaxFailure=-1). Co-Authored-By: Claude Opus 4.8 --- stacks/traefik/modules/traefik/middleware.tf | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/stacks/traefik/modules/traefik/middleware.tf b/stacks/traefik/modules/traefik/middleware.tf index 1837a794..9b9709c7 100644 --- a/stacks/traefik/modules/traefik/middleware.tf +++ b/stacks/traefik/modules/traefik/middleware.tf @@ -197,8 +197,16 @@ resource "kubernetes_manifest" "middleware_crowdsec" { crowdsec-bouncer = { crowdsecLapiKey = var.crowdsec_api_key crowdsecLapiHost = "crowdsec-service.crowdsec.svc.cluster.local:8080" - crowdsecMode = "stream" - updateMaxFailure = -1 # fail-open: serve from cache when LAPI is unreachable + # LIVE mode (synchronous per-request LAPI query), not stream: under + # Traefik's Yaegi interpreter the plugin's stream cache updates (it logs + # `handleStreamCache:updated`) but does NOT enforce the cached decisions + # — verified by a ban that was present in the LAPI stream AND pulled by + # the plugin yet still let the banned IP through. Live mode queries LAPI + # per request (result cached per-IP for defaultDecisionSeconds), enforces + # reliably, and picks up new decisions immediately. LAPI is 3-replica + + # in-cluster; fail-open preserved via updateMaxFailure=-1. + crowdsecMode = "live" + updateMaxFailure = -1 # fail-open if LAPI is unreachable # Redis cache DISABLED: the plugin's redis client does not work under # Traefik's Yaegi interpreter — it logs `cache:unreachable` even though # redis-master is reachable+writable from the traefik ns (verified). With