diff --git a/docs/architecture/security.md b/docs/architecture/security.md index 1dd7397a..e12347d2 100644 --- a/docs/architecture/security.md +++ b/docs/architecture/security.md @@ -83,7 +83,17 @@ CrowdSec operates in a hub-and-agent model: **Traefik Bouncer Plugin** (`crowdsec-bouncer-traefik-plugin`, `stacks/traefik/modules/traefik/middleware.tf`): - Integrated as Traefik middleware (in the default ingress chain) - Queries LAPI for IP reputation on each request +- **Registered with LAPI** via `BOUNCER_KEY_traefik` env on the LAPI container + (`stacks/crowdsec/modules/crowdsec/values.yaml`), seeded from the same Vault key + the middleware presents (`ingress_crowdsec_api_key`). **Before 2026-06-19 the + bouncer was never registered → LAPI returned 403 → the plugin failed open and + enforced nothing (no bans, no captcha).** The seed re-registers automatically on + every LAPI start, so a DB wipe (e.g. the MySQL→PostgreSQL migration that lost the + original registration) can't silently disable enforcement again. - **Fail-open mode**: If LAPI unreachable, allows traffic (graceful degradation) +- **Only sees non-proxied (direct) apps' real client IPs** (ETP=Local). Proxied + apps arrive from cloudflared's pod IP (in `clientTrustedIPs`) and are bypassed — + extending enforcement to proxied apps needs `forwardedHeadersTrustedIPs` (future). - Honours two LAPI remediation types (profiles in `stacks/crowdsec/modules/crowdsec/values.yaml`): - **`ban`** → HTTP 403 (serious attacks: CVE exploits, scanners, brute force) - **`captcha`** → **Cloudflare Turnstile challenge** so the flagged user can diff --git a/stacks/crowdsec/main.tf b/stacks/crowdsec/main.tf index 70f01eea..a4c0013f 100644 --- a/stacks/crowdsec/main.tf +++ b/stacks/crowdsec/main.tf @@ -29,4 +29,7 @@ module "crowdsec" { crowdsec_dash_machine_id = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_machine_id"] crowdsec_dash_machine_password = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_machine_password"] slack_webhook_url = data.vault_kv_secret_v2.secrets.data["alertmanager_slack_api_url"] + # Same key the traefik-stack bouncer middleware uses — seeded into LAPI so the + # bouncer authenticates and pulls decisions (was unregistered → 403 → fail-open). + ingress_bouncer_key = data.vault_kv_secret_v2.secrets.data["ingress_crowdsec_api_key"] } diff --git a/stacks/crowdsec/modules/crowdsec/main.tf b/stacks/crowdsec/modules/crowdsec/main.tf index 3022e518..a900e8c4 100644 --- a/stacks/crowdsec/modules/crowdsec/main.tf +++ b/stacks/crowdsec/modules/crowdsec/main.tf @@ -16,6 +16,11 @@ variable "tier" { type = string } variable "slack_webhook_url" { type = string } variable "mysql_host" { type = string } variable "postgresql_host" { type = string } +variable "ingress_bouncer_key" { + type = string + sensitive = true + description = "API key for the Traefik CrowdSec bouncer plugin. Seeded into LAPI via BOUNCER_KEY_traefik so the bouncer authenticates and pulls decisions — the same key the traefik-stack middleware presents." +} module "tls_secret" { source = "../../../../modules/kubernetes/setup_tls_secret" @@ -148,7 +153,7 @@ resource "helm_release" "crowdsec" { repository = "https://crowdsecurity.github.io/helm-charts" chart = "crowdsec" - values = [templatefile("${path.module}/values.yaml", { homepage_username = var.homepage_username, homepage_password = var.homepage_password, DB_PASSWORD = var.db_password, ENROLL_KEY = var.enroll_key, SLACK_WEBHOOK_URL = var.slack_webhook_url, mysql_host = var.mysql_host, postgresql_host = var.postgresql_host })] + values = [templatefile("${path.module}/values.yaml", { homepage_username = var.homepage_username, homepage_password = var.homepage_password, DB_PASSWORD = var.db_password, ENROLL_KEY = var.enroll_key, SLACK_WEBHOOK_URL = var.slack_webhook_url, mysql_host = var.mysql_host, postgresql_host = var.postgresql_host, INGRESS_CROWDSEC_API_KEY = var.ingress_bouncer_key })] timeout = 1200 wait = true wait_for_jobs = true diff --git a/stacks/crowdsec/modules/crowdsec/values.yaml b/stacks/crowdsec/modules/crowdsec/values.yaml index 8407ac37..040b44d8 100644 --- a/stacks/crowdsec/modules/crowdsec/values.yaml +++ b/stacks/crowdsec/modules/crowdsec/values.yaml @@ -126,6 +126,15 @@ lapi: secretKeyRef: name: crowdsec-lapi-secrets key: dbPassword + # Register the Traefik bouncer at LAPI startup with the SAME API key the + # traefik-stack middleware uses (Vault `ingress_crowdsec_api_key`). Before + # this the bouncer was never registered → LAPI returned 403 → the plugin + # failed open (updateMaxFailure=-1) and enforced NOTHING (no bans, no + # captcha). Idempotent across the 3 LAPI replicas / restarts, and + # re-registers automatically if the LAPI DB is ever wiped (the root cause: + # the prior manual registration was lost in the MySQL→PostgreSQL migration). + - name: BOUNCER_KEY_traefik + value: "${INGRESS_CROWDSEC_API_KEY}" dashboard: enabled: true env: