Explanation

Architecture overview

CrowdSec sits in kup6s as the application-layer (L7) defense-in-depth tier. The Hetzner edge handles L3/L4 volumetric filtering. CrowdSec adds IP reputation enforcement and, optionally in Phase 3, rule-based WAF inspection.

Component layout

The request path through the cluster:

Internet
   │  Hetzner DDoS edge filters L3/L4 volumetrics
Hetzner Cloud LB  ─── L4 routing + PROXY-Protocol-v2 forwarding
Traefik (websecure entrypoint)
   │  Default middleware: crowdsec-crowdsec-bouncer@kubernetescrd
   │  Plugin extracts source IP from the PROXY header
   │  Plugin polls LAPI every 60 s in stream mode
Backend pods (ArgoCD, Mailu, Forgejo, Alpinecity, ...)

The engine runs in parallel:

CrowdSec engine (pod: crowdsec-lapi)
   ├── LAPI HTTP API on port 8080
   ├── AppSec component on port 7422 (not yet attached to routes)
   └── Postgres backend (CNPG cluster crowdsec-db, 2 instances)

Decisions in LAPI:
   ├── CAPI (default, ~2-30k IPs: ssh:exploit, http:scan, http:exploit, ...)
   └── Premium blocklists (after Console enrollment)

Plugin flow per request

The plugin processes each incoming request in this order:

  1. The client sends an HTTPS request which reaches Hetzner LB and then a Traefik pod.

  2. Traefik decodes the PROXY-Protocol header and recovers the real client source IP.

  3. The plugin extracts the source IP.

  4. The plugin checks the source IP against ClientTrustedIPs (RFC1918 ranges). On match, the request passes immediately.

  5. The plugin checks the decision cache. On match with action: "ban", the plugin returns HTTP 403 and never contacts the backend.

  6. Otherwise the request continues to the backend service.

Fail-modes

The plugin is configured fail-open at multiple levels. This trades a brief security gap for service availability during component outages.

Component

Status “down”

Plugin behavior

LAPI unreachable

Pod crash, Postgres outage

Cache stays valid for TTL, then fail-open

AppSec unreachable

Not relevant in Phase 2 (no routes attached)

CrowdsecAppsecUnreachableBlock=false → pass

Postgres outage

CNPG failover window

LAPI memory cache serves decisions, no new persistence

See also

How-to: LAPI or Postgres outage describes recovery steps for each scenario.

Where the data flows

The plugin maintains a local cache and pulls deltas from LAPI:

  • The plugin sends GET /v1/decisions/stream to LAPI every 60 seconds.

  • LAPI responds with a delta since the last poll: new: [...], deleted: [...].

  • LAPI itself draws decisions from multiple sources. CAPI provides the standard community feed via an outbound connection (enabled by chart default). Premium blocklists arrive after Console enrollment via app.crowdsec.net. Local scenarios from agent log parsing remain disabled in kup6s (agent.enabled: false).

Why “agent disabled”

Phase 1 disabled the CrowdSec agent (agent.enabled: false in engine.ts). The reason: local log parsing requires choosing and tuning detection scenarios, which warrants its own iteration. A future iteration could enable the agent to parse Mailu pod logs for SMTP brute-force detection, but Phase 2 does not include this.