From c23b03864eaad2f887009fda1b384e7504ab5dc8 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 21 Jun 2026 13:35:13 +0000 Subject: [PATCH] traefik/crowdsec: delete dead Yaegi plugin + middleware CRD + captcha (PR2/2) Zero live ingresses reference traefik-crowdsec@kubernetescrd (PR1 + a cluster-wide targeted ingress re-apply confirmed 0), so the crowdsec Middleware CRD and the broken Yaegi bouncer plugin can be removed without orphaning any router. Removes: the `crowdsec` Middleware, the crowdsec-bouncer plugin (static config + initContainer download + state.json entry), the captcha template ConfigMap + volume + captcha.html, the Turnstile widget + data.cloudflare_accounts, and the 3 now-unused module vars. Also drops the `crowdsec` middleware from the catch-all error-pages IngressRoute chain (the one remaining CRD-level reference, which an Ingress-annotation grep does not surface) so that router is not orphaned when the Middleware is deleted; it keeps rate-limit. Enforcement is fully handled out-of-band now: cs-firewall-bouncer (in-kernel nftables, direct hosts) + Cloudflare IP-List/WAF (proxied hosts). The api-token-middleware plugin is deliberately preserved (still used by paperless-mcp). Co-Authored-By: Claude Opus 4.8 --- stacks/traefik/main.tf | 26 -- stacks/traefik/modules/traefik/captcha.html | 338 ------------------ stacks/traefik/modules/traefik/error-pages.tf | 1 - stacks/traefik/modules/traefik/main.tf | 54 +-- stacks/traefik/modules/traefik/middleware.tf | 53 --- 5 files changed, 4 insertions(+), 468 deletions(-) delete mode 100644 stacks/traefik/modules/traefik/captcha.html diff --git a/stacks/traefik/main.tf b/stacks/traefik/main.tf index d824a1c4..0210b030 100644 --- a/stacks/traefik/main.tf +++ b/stacks/traefik/main.tf @@ -13,35 +13,9 @@ data "vault_kv_secret_v2" "viktor" { name = "viktor" } -# Cloudflare Turnstile widget backing the CrowdSec captcha remediation. When -# LAPI issues a `captcha` decision (rate-limit / 403 / crawl / sensitive-file -# abuse — the captcha_remediation profile in stacks/crowdsec .../values.yaml), -# the Traefik bouncer plugin serves this widget so flagged users can -# self-unblock instead of getting a hard 403 (which is what happened before: -# the plugin had no captcha provider, so captcha decisions fell through to ban). -# Scoped to the registrable domain — a Turnstile hostname covers its subdomains, -# so one widget works on every *.viktorbarzin.me app the bouncer fronts. -# Same IaC pattern as stacks/forgejo/turnstile.tf; the CF Global API Key -# (cloudflare_provider.tf) has account-wide Turnstile access. The widget secret -# is sensitive and lands in TF state (Tier-1 PG, encrypted) — same trust level -# as the CrowdSec LAPI key already passed into the bouncer middleware. -data "cloudflare_accounts" "main" {} - -resource "cloudflare_turnstile_widget" "crowdsec_captcha" { - account_id = data.cloudflare_accounts.main.accounts[0].id - name = "crowdsec-captcha" - domains = ["viktorbarzin.me"] - # "managed" = Cloudflare adaptively decides whether to show an interactive - # challenge; lowest friction for real users, strong against bots. - mode = "managed" -} - module "traefik" { source = "./modules/traefik" tier = local.tiers.core - crowdsec_api_key = data.vault_kv_secret_v2.secrets.data["ingress_crowdsec_api_key"] - captcha_site_key = cloudflare_turnstile_widget.crowdsec_captcha.id - captcha_secret_key = cloudflare_turnstile_widget.crowdsec_captcha.secret redis_host = var.redis_host tls_secret_name = var.tls_secret_name auth_fallback_htpasswd = data.vault_kv_secret_v2.secrets.data["auth_fallback_htpasswd"] diff --git a/stacks/traefik/modules/traefik/captcha.html b/stacks/traefik/modules/traefik/captcha.html deleted file mode 100644 index bf7a8afd..00000000 --- a/stacks/traefik/modules/traefik/captcha.html +++ /dev/null @@ -1,338 +0,0 @@ - - - - - CrowdSec Captcha - - - - - - - -
-
-
- -

CrowdSec Captcha

-
-
-
-
-
-
-

This security check has been powered by

- - - - - - - - - - - - - - - - - - - - - CrowdSec - -
-
-
- - - diff --git a/stacks/traefik/modules/traefik/error-pages.tf b/stacks/traefik/modules/traefik/error-pages.tf index 4913bc99..7f2e614c 100644 --- a/stacks/traefik/modules/traefik/error-pages.tf +++ b/stacks/traefik/modules/traefik/error-pages.tf @@ -201,7 +201,6 @@ resource "kubernetes_manifest" "ingressroute_catchall" { priority = 1 middlewares = [ { name = "rate-limit", namespace = kubernetes_namespace.traefik.metadata[0].name }, - { name = "crowdsec", namespace = kubernetes_namespace.traefik.metadata[0].name }, ] services = [{ name = "error-pages" diff --git a/stacks/traefik/modules/traefik/main.tf b/stacks/traefik/modules/traefik/main.tf index 1de3fc41..1d72be22 100644 --- a/stacks/traefik/modules/traefik/main.tf +++ b/stacks/traefik/modules/traefik/main.tf @@ -1,8 +1,4 @@ variable "tier" { type = string } -variable "crowdsec_api_key" { - type = string - sensitive = true -} variable "redis_host" { type = string } variable "tls_secret_name" {} variable "auth_fallback_htpasswd" { @@ -21,16 +17,6 @@ variable "x402_notify_webhook_url" { description = "Slack-compatible incoming-webhook URL the gateway POSTs to on every successful payment. Empty = no notifications." sensitive = true } -variable "captcha_site_key" { - type = string - sensitive = true - description = "Cloudflare Turnstile site key (public) for the CrowdSec captcha remediation. Sourced from cloudflare_turnstile_widget in the stack root." -} -variable "captcha_secret_key" { - type = string - sensitive = true - description = "Cloudflare Turnstile secret key for the CrowdSec captcha remediation — validated server-side by the bouncer plugin against Cloudflare siteverify." -} resource "kubernetes_namespace" "traefik" { metadata { @@ -48,22 +34,6 @@ resource "kubernetes_namespace" "traefik" { } } -# captcha.html template served by the CrowdSec bouncer plugin for Turnstile -# challenges. The pulled Yaegi plugin does NOT expose its bundled template to -# Traefik, so we vendor it (captcha.html in this module) and mount it into the -# Traefik container at /captcha via the `volumes` Helm value below. The template -# is provider-agnostic: the plugin fills {{ .FrontendJS }}/{{ .FrontendKey }}/ -# {{ .SiteKey }} with Turnstile's JS URL + `cf-turnstile` class + the site key. -resource "kubernetes_config_map" "captcha_template" { - metadata { - name = "crowdsec-captcha-template" - namespace = kubernetes_namespace.traefik.metadata[0].name - } - data = { - "captcha.html" = file("${path.module}/captcha.html") - } -} - resource "helm_release" "traefik" { namespace = kubernetes_namespace.traefik.metadata[0].name create_namespace = false @@ -75,9 +45,9 @@ resource "helm_release" "traefik" { # chart 41.0.0 rejects this values block's `logs` key ("Additional property # logs is not allowed"). Bump deliberately (with values migration), never # implicitly. Deployed since 2026-05-30 (release rev 57). - version = "40.2.0" - atomic = true - timeout = 600 + version = "40.2.0" + atomic = true + timeout = 600 values = [yamlencode({ deployment = { @@ -100,13 +70,10 @@ resource "helm_release" "traefik" { command = ["sh", "-c", join("", [ "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.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.6.0\",\"github.com/Aetherinox/traefik-api-token-middleware\":\"v0.1.4\"}' ", + "printf '{\"github.com/Aetherinox/traefik-api-token-middleware\":\"v0.1.4\"}' ", "> \"$STORAGE/archives/state.json\"; ", "echo \"Plugins pre-downloaded successfully\"", ])] @@ -125,15 +92,6 @@ resource "helm_release" "traefik" { } } - # Mount the CrowdSec captcha template into the Traefik container at - # /captcha/captcha.html (chart `volumes` creates the volume + container - # mount from one entry). Referenced by captchaHTMLFilePath in middleware.tf. - volumes = [{ - name = kubernetes_config_map.captcha_template.metadata[0].name - mountPath = "/captcha" - type = "configMap" - }] - ingressClass = { enabled = true isDefaultClass = true @@ -230,10 +188,6 @@ resource "helm_release" "traefik" { # Plugins experimental = { plugins = { - crowdsec-bouncer = { - moduleName = "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - 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 # (e.g. paperless-mcp, which has no native auth). Plugin key diff --git a/stacks/traefik/modules/traefik/middleware.tf b/stacks/traefik/modules/traefik/middleware.tf index 9b9709c7..befeb6bf 100644 --- a/stacks/traefik/modules/traefik/middleware.tf +++ b/stacks/traefik/modules/traefik/middleware.tf @@ -183,59 +183,6 @@ resource "kubernetes_manifest" "middleware_security_headers" { depends_on = [helm_release.traefik] } -# CrowdSec bouncer plugin middleware -resource "kubernetes_manifest" "middleware_crowdsec" { - manifest = { - apiVersion = "traefik.io/v1alpha1" - kind = "Middleware" - metadata = { - name = "crowdsec" - namespace = kubernetes_namespace.traefik.metadata[0].name - } - spec = { - plugin = { - crowdsec-bouncer = { - crowdsecLapiKey = var.crowdsec_api_key - crowdsecLapiHost = "crowdsec-service.crowdsec.svc.cluster.local:8080" - # 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 - # 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 - # (the pre-2026-06 behaviour — no provider configured → handleBan). - # captcha.html is mounted at /captcha in the Traefik pod - # (kubernetes_config_map.captcha_template + the helm `volumes` value); - # keys come from the Turnstile widget in the stack root (main.tf). - captchaProvider = "turnstile" - captchaSiteKey = var.captcha_site_key - captchaSecretKey = var.captcha_secret_key - captchaGracePeriodSeconds = 1800 # how long a solved challenge is honoured - captchaHTMLFilePath = "/captcha/captcha.html" - } - } - } - } - - depends_on = [helm_release.traefik, kubernetes_config_map.captcha_template] -} - # TLS option for mTLS (client certificate auth) resource "kubernetes_manifest" "tls_option_mtls" { manifest = { -- 2.49.1