Exposing self-hosted services to the internet is a balancing act between convenience and risk. Traefik makes routing easy, but it does not detect hostile traffic on its own. CrowdSec fills that gap by parsing logs, detecting attack patterns, and sharing threat intelligence across its user community.
This post shows the stack I run in my homelab: Traefik v3.3 behind Cloudflare, with CrowdSec acting as both a WAF and an IP-reputation firewall.
What is CrowdSec?#
CrowdSec is an open-source intrusion prevention system that analyzes logs, detects attack patterns, and turns those detections into decisions such as bans or alerts. Its main strength is the combination of local detection and shared threat intelligence: when one deployment sees malicious behavior, that signal helps improve protection for the wider community. In practice, that makes it a strong fit for exposed homelab services, reverse proxies, SSH access, and other small infrastructure that still needs real security controls.
CrowdSec has a modular architecture with a central engine, a REST API (LAPI), and multiple bouncers that enforce decisions at different layers. For this Traefik deployment, I use the CrowdSec bouncer plugin for HTTP-layer blocking and the crowdsec-firewall-bouncer-iptables for network-layer blocking on the host.
Why this setup?#
The goal is to block bad traffic as early as possible while keeping the stack simple to operate.
- Traefik handles routing, TLS, headers, and rate limiting.
- CrowdSec reads Traefik and host logs, then turns detections into decisions.
- The Traefik bouncer blocks malicious HTTP requests.
- The firewall bouncer drops banned IPs at the network layer.
Deployment model#
Two enforcement points work together:
| Layer | Component | What it blocks |
|---|---|---|
| HTTP | crowdsec-bouncer-traefik-plugin | Banned IPs and malicious requests through AppSec |
| Network | crowdsec-firewall-bouncer-iptables | Banned IPs before they reach the Docker network |
The important design choice is the shared log volume: Traefik writes access logs into a named Docker volume, and CrowdSec reads them back without a host bind mount.
Compose stack#
Before the first deployment, generate CROWDSEC_FIREWALL_BOUNCER_API_KEY and CROWDSEC_TRAEFIK_BOUNCER_API_KEY and store them in your secret manager (for example Ansible Vault or a .env file that is never committed). These keys are long-lived shared secrets used by each bouncer to authenticate to CrowdSec LAPI, so create one key per bouncer and rotate them with your regular credential lifecycle.
# Option 1: generate strong random keys yourself (recommended before first compose up)
openssl rand -hex 32 # use as CROWDSEC_FIREWALL_BOUNCER_API_KEY
openssl rand -hex 32 # use as CROWDSEC_TRAEFIK_BOUNCER_API_KEY
# Option 2: let CrowdSec generate and print keys (after crowdsec container is running)
docker exec crowdsec cscli bouncers add firewall-bouncer -o raw
docker exec crowdsec cscli bouncers add traefik-bouncer -o rawIf you use the BOUNCER_KEY_* environment variables shown above, CrowdSec pre-registers those exact values on startup, which is ideal for immutable and repeatable deployments.
The full stack lives in one compose.yaml file:
services:
crowdsec:
container_name: crowdsec
image: crowdsecurity/crowdsec:latest-debian
restart: unless-stopped
environment:
- BOUNCER_KEY_firewall-bouncer=${CROWDSEC_FIREWALL_BOUNCER_API_KEY}
- BOUNCER_KEY_traefik-bouncer=${CROWDSEC_TRAEFIK_BOUNCER_API_KEY}
- COLLECTIONS=crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules crowdsecurity/appsec-crs crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/base-http-scenarios crowdsecurity/sshd crowdsecurity/linux
ports:
- 7422:7422 # AppSec (WAF)
- 8080:8080 # LAPI (bouncers)
volumes:
- ./crowdsec:/etc/crowdsec
- /var/log/auth.log:/var/log/auth.log:ro
- /var/log/journal:/var/log/host:ro
- /var/log/syslog:/var/log/syslog:ro
- crowdsec-db:/var/lib/crowdsec/data
- traefik_logs:/var/log/traefik:ro
security_opt:
- no-new-privileges:true
traefik:
container_name: traefik
image: traefik:v3.3
restart: unless-stopped
ports:
- 80:80
- 443:443
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
- CROWDSEC_TRAEFIK_BOUNCER_API_KEY=${CROWDSEC_TRAEFIK_BOUNCER_API_KEY}
depends_on:
- crowdsec
volumes:
- ./traefik:/etc/traefik
- traefik_logs:/var/log/traefik
volumes:
crowdsec-db:
traefik_logs:Traefik configuration#
Static configuration#
Traefik keeps HTTP-to-HTTPS redirection, dashboard exposure, certificate management, and the CrowdSec plugin in one place.
global:
checkNewVersion: false
sendAnonymousUsage: false
log:
level: INFO
filePath: /var/log/traefik/traefik.log
accessLog:
filePath: /var/log/traefik/access.log
bufferingSize: 0
api:
dashboard: false
insecure: false
entryPoints:
web:
address: :80
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: :443
providers:
file:
directory: /etc/traefik/dynamic
watch: true
experimental:
plugins:
bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.4.1
certificatesResolvers:
production:
acme:
email: your-email@example.com
storage: /etc/traefik/certs/acme.json
caServer: https://acme-v02.api.letsencrypt.org/directory
dnsChallenge:
provider: cloudflare
resolvers:
- 1.1.1.1:53
- 8.8.8.8:53Dynamic middleware#
The dynamic configuration defines the CrowdSec bouncer middleware, security headers, rate limiting, and IP allowlist. The wan-exposed chain applies all protections to WAN traffic, while the default chain allows internal traffic without interference:
http:
middlewares:
crowdsec-bouncer:
plugin:
bouncer:
enabled: true
updateIntervalSeconds: 10
crowdsecMode: stream
crowdsecAppsecEnabled: true
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true
crowdsecLapiHost: crowdsec:8080
crowdsecLapiKey: '{{ env "CROWDSEC_TRAEFIK_BOUNCER_API_KEY" }}'
default-headers:
headers:
frameDeny: true
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsSeconds: 15552000
stsIncludeSubdomains: true
stsPreload: true
customFrameOptionsValue: SAMEORIGIN
ratelimit:
rateLimit:
average: 100
burst: 200
period: 1s
default-allowlist:
ipAllowList:
sourceRange:
- 127.0.0.1/32 # localhost
- 172.16.0.0/12 # internal Docker network
- 192.168.0.0/16 # internal network
wan-exposed:
chain:
middlewares:
- crowdsec-bouncer
- default-headers
- ratelimitRoute all WAN traffic through the wan-exposed chain, while keeping internal services on the default chain with just the allowlist. That way, only traffic that reaches the WAN entry point is subject to CrowdSec detection, security headers, and rate limiting, while internal traffic is unaffected.
CrowdSec configuration#
Log acquisition#
CrowdSec reads both Traefik access logs and host logs for a comprehensive view of traffic and system events. The Traefik logs are shared via a Docker volume, while host logs are bind-mounted read-only:
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
---
filenames:
- /var/log/auth.log
- /var/log/syslog
labels:
type: syslog
---
source: journalctl
journalctl_filter:
- "--directory=/var/log/host/"
labels:
type: syslog
---
appsec_config: crowdsecurity/appsec-default
labels:
type: appsec
listen_addr: 0.0.0.0:7422
source: appsecRemediation profiles#
The default remediation profiles are simple IP bans for both individual IPs and CIDR ranges. You can customize the duration and scope of these bans by editing the profiles in /etc/crowdsec/profiles/ or creating new ones:
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
on_success: break
---
name: default_range_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
- type: ban
duration: 4h
on_success: breakFirewall bouncer#
The Traefik plugin blocks HTTP requests, but I still install the kernel-level bouncer on the host.
sudo apt update
sudo apt install -y crowdsec-firewall-bouncer-iptables rsyslogEnable rsyslog so host logs are available to CrowdSec:
sudo systemctl enable --now rsyslogThen configure the firewall bouncer to use your local LAPI endpoint and API key in /etc/crowdsec/bouncers/crowdsec-firewall-bouncer-iptables.yaml by updating api_url and api_key:
...
api_url: http://localhost:8080/
api_key: <CROWDSEC_FIREWALL_BOUNCER_API_KEY>
...Then restart the bouncer service:
sudo systemctl restart crowdsec-firewall-bouncer-iptablesThat gives me two independent enforcement points: one in Traefik and one in iptables.
Hub collections#
The COLLECTIONS variable auto-installs the parsers and scenarios I want on startup.
| Collection | What it detects |
|---|---|
crowdsecurity/traefik | Traefik access log parsers and HTTP attack scenarios |
crowdsecurity/http-cve | Exploitation attempts for published HTTP CVEs |
crowdsecurity/base-http-scenarios | Generic web scanners and bots |
crowdsecurity/appsec-crs | OWASP Core Rule Set for AppSec |
crowdsecurity/appsec-virtual-patching | Virtual patches for known vulnerabilities |
crowdsecurity/appsec-generic-rules | SQLi, XSS, and path traversal rules |
crowdsecurity/sshd | SSH brute-force and invalid-user attempts |
crowdsecurity/linux | General Linux parsers for sudo, PAM, and related events |
Validation checklist#
After deployment, I usually check the stack in this order:
docker exec crowdsec cscli decisions list
docker exec crowdsec cscli bouncers list
docker exec crowdsec cscli hub update && docker exec crowdsec cscli hub upgrade
docker exec crowdsec cscli alerts list --limit 10The main things I want to confirm are:
- Traefik is issuing certificates and serving only HTTPS.
- CrowdSec can read Traefik logs and host logs.
- The bouncers can talk to the LAPI.
- Bans show up in both the Traefik plugin and the firewall bouncer.
