crowdsec: register the Traefik bouncer with LAPI (fix fail-open)
All checks were successful
ci/woodpecker/push/default Pipeline was successful
All checks were successful
ci/woodpecker/push/default Pipeline was successful
The Traefik bouncer plugin's API key was never registered with LAPI — the crowdsec stack reads many keys from Vault but not ingress_crowdsec_api_key, and the chart registers no bouncer. So LAPI returned 403 to the plugin, which with updateMaxFailure=-1 failed open and enforced NOTHING: no community-blocklist bans, and the (now-Turnstile-wired) captcha never fired. cscli bouncers list was empty; the registration was likely lost in the MySQL->PostgreSQL DB migration with no IaC to recreate it. Seed the bouncer at LAPI startup via BOUNCER_KEY_traefik, valued from the same Vault key the middleware presents — so they match by construction, and the bouncer re-registers automatically on every LAPI start (survives DB wipes). - stacks/crowdsec/main.tf: read ingress_crowdsec_api_key, pass to module. - module main.tf: new sensitive var + thread into the values templatefile. - values.yaml: BOUNCER_KEY_traefik on lapi.env. - docs/architecture/security.md: document registration + fail-open history and the proxied-app coverage caveat. Activates enforcement (community blocklist bans + captcha) on non-proxied apps; internal IPs stay bypassed (clientTrustedIPs), fail-open-on-LAPI-down preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
56dadda453
commit
a5bb4db9c5
4 changed files with 28 additions and 1 deletions
|
|
@ -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`):
|
**Traefik Bouncer Plugin** (`crowdsec-bouncer-traefik-plugin`, `stacks/traefik/modules/traefik/middleware.tf`):
|
||||||
- Integrated as Traefik middleware (in the default ingress chain)
|
- Integrated as Traefik middleware (in the default ingress chain)
|
||||||
- Queries LAPI for IP reputation on each request
|
- 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)
|
- **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`):
|
- Honours two LAPI remediation types (profiles in `stacks/crowdsec/modules/crowdsec/values.yaml`):
|
||||||
- **`ban`** → HTTP 403 (serious attacks: CVE exploits, scanners, brute force)
|
- **`ban`** → HTTP 403 (serious attacks: CVE exploits, scanners, brute force)
|
||||||
- **`captcha`** → **Cloudflare Turnstile challenge** so the flagged user can
|
- **`captcha`** → **Cloudflare Turnstile challenge** so the flagged user can
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,7 @@ module "crowdsec" {
|
||||||
crowdsec_dash_machine_id = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_machine_id"]
|
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"]
|
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"]
|
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"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,11 @@ variable "tier" { type = string }
|
||||||
variable "slack_webhook_url" { type = string }
|
variable "slack_webhook_url" { type = string }
|
||||||
variable "mysql_host" { type = string }
|
variable "mysql_host" { type = string }
|
||||||
variable "postgresql_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" {
|
module "tls_secret" {
|
||||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||||
|
|
@ -148,7 +153,7 @@ resource "helm_release" "crowdsec" {
|
||||||
repository = "https://crowdsecurity.github.io/helm-charts"
|
repository = "https://crowdsecurity.github.io/helm-charts"
|
||||||
chart = "crowdsec"
|
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
|
timeout = 1200
|
||||||
wait = true
|
wait = true
|
||||||
wait_for_jobs = true
|
wait_for_jobs = true
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,15 @@ lapi:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: crowdsec-lapi-secrets
|
name: crowdsec-lapi-secrets
|
||||||
key: dbPassword
|
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:
|
dashboard:
|
||||||
enabled: true
|
enabled: true
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue