CrowdSec: modern, collaborative intrusion prevention
Set up CrowdSec with the Traefik bouncer: detect attacks from the access logs, block attackers with a 403 and benefit from the community blocklist.
Table of contents
Fail2ban blocks brute force on your SSH access – but what about the scanners that probe your web apps for holes all day long? CrowdSec closes exactly this gap: it detects attacks in Traefik’s access logs and bans the attackers before they even reach your applications.
What are we building?
By the end, a CrowdSec agent runs alongside your Traefik, reading along with the access logs, detecting suspicious behavior based on scenarios (port scans, path probing, known CVE exploitation, login brute force) and putting attacker IPs on a block list. A bouncer – here as a Traefik plugin – enforces these decisions: a banned IP only gets 403 Forbidden at every one of your apps. Tested with CrowdSec v1.7.8, the Traefik bouncer plugin v1.6.0 and Traefik v3.7.
This is how the parts interlock:
- Agent + Local API (LAPI): reads the logs, evaluates them against the scenarios and manages the active bans (“decisions”). That’s the container we set up.
- Bouncer: asks the LAPI on every request “is this IP banned?” and blocks if yes. The Traefik bouncer runs as a plugin directly in the reverse proxy – no additional container.
- Community blocklist: CrowdSec reports (anonymized) detected attacks to the central network and gets a blocklist fed by all users back in return. IPs that were already flagged as malicious elsewhere are then banned at your place from the start. That’s the “collaborative” in the title.
CrowdSec instead of fail2ban – or in addition?
Prerequisites
- A running reverse proxy with Traefik (the
proxynetwork and the resolverlecome from there) with at least one app behind it. - Fail2ban for SSH protection – CrowdSec handles the HTTP layer here, not SSH.
- Basic knowledge of Docker Compose and a hardened server with an active firewall.
VPS 2000 G12
8 vCores · 16 GB RAM · 512 GB NVMe
from €19.24/month
CrowdSec runs with a few hundred MB alongside your existing app stack – the VPS 2000 has enough reserve for that.
💶 5 € voucher for new netcup customers:
36nc17844976032
(new customers only, no domains)
Step by step
Step 1: Enable Traefik’s access logs
CrowdSec can only detect what it sees – and it sees attacks via Traefik’s access logs. They’re off by default. Open the compose.yaml of your Traefik (from the Traefik tutorial, folder ~/traefik) and add these three lines to the command block:
- "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--accesslog.format=json"The JSON format is important – the CrowdSec parsers expect it that way. So that both Traefik (writing) and CrowdSec (reading) can reach the same file, you put it on a fixed host path. Add to Traefik’s volumes block:
- /var/log/traefik:/var/log/traefikCreate the directory on the host and restart Traefik:
sudo mkdir -p /var/log/traefik
cd ~/traefik && docker compose up -dCall one of your apps once and check that lines arrive:
tail -n 2 /var/log/traefik/access.logYou should see JSON lines with "ClientHost", "RequestPath" and "DownstreamStatus". If the file stays empty, the path in the volume is wrong, or simply no request has come in yet.
Step 2: Set up the CrowdSec container
CrowdSec gets its own folder. First generate a bouncer key – the Traefik bouncer later authenticates with it at the LAPI:
mkdir -p ~/crowdsec && cd ~/crowdsec
openssl rand -hex 32Copy the output; it’s used in two places shortly. Create the acquisition – it tells CrowdSec which file with which log type to read:
# ~/crowdsec/acquis.yaml
filenames:
- /var/log/traefik/access.log
labels:
type: traefikNow the compose.yaml. Replace YOUR_BOUNCER_KEY with the generated key:
services:
crowdsec:
image: crowdsecurity/crowdsec:v1.7.8
container_name: crowdsec
environment:
COLLECTIONS: "crowdsecurity/traefik"
BOUNCER_KEY_TRAEFIK: "YOUR_BOUNCER_KEY"
GID: "1000"
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.d/traefik.yaml:ro
- /var/log/traefik:/var/log/traefik:ro
- crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/
networks: [proxy]
restart: unless-stopped
volumes:
crowdsec-db:
crowdsec-config:
networks:
proxy:
external: trueWhat matters here:
COLLECTIONS: crowdsecurity/traefikinstalls the matching collection of parsers and scenarios for Traefik logs on the first start (including the HTTP base scenarios).BOUNCER_KEY_TRAEFIKregisters a bouncer namedTRAEFIKwith your key right at startup – that saves the detour viacscli bouncers add.GID: "1000"runs the CrowdSec process under this group.1000is the usual default for the first user; if Traefik runs as root, theaccess.logis usually world-readable and the GID doesn’t matter anyway. If it’s set more tightly, check withls -l /var/log/traefik/access.logand enter the GID of the file’s group.- The log is mounted read-only (
:ro). CrowdSec is on theproxynetwork so the Traefik bouncer can reach the LAPI atcrowdsec:8080later.
Start the container and check that everything takes effect:
docker compose up -d
docker exec crowdsec cscli collections listThe Traefik collection and the HTTP scenarios should appear as enabled:
crowdsecurity/base-http-scenarios ✔️ enabled
crowdsecurity/http-cve ✔️ enabled
crowdsecurity/traefik ✔️ enabledCheck that the community blocklist is active – it costs nothing and is half the benefit of CrowdSec:
docker exec crowdsec cscli capi statusYou can successfully interact with Central API (CAPI)
Sharing signals is enabled
Pulling community blocklist is enabled
Pulling blocklists from the console is enabledPulling community blocklist is enabled confirms that you receive the collective block list of the CrowdSec community – attackers that stood out elsewhere are already banned at your place before they even try.
Step 3: Add the Traefik bouncer
Now the part that enforces the bans. The bouncer is a Traefik plugin and consists of two additions to your Traefik: load the plugin and define a middleware that acts on all apps.
Load the plugin by adding to the command block of ~/traefik/compose.yaml:
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.6.0"
- "--entrypoints.websecure.http.middlewares=crowdsec@file"The last line attaches the bouncer middleware globally to the websecure entrypoint – so every HTTPS request to every app runs through the bouncer, without you touching each app individually. You define the middleware itself via the file provider.
Don't overwrite existing entrypoint middlewares
--entrypoints.websecure.http.middlewares is single-valued: if you’ve already set middlewares on this entrypoint in the Traefik setup (e.g. security headers), a second flag overwrites the existing value – then your previous middlewares silently drop out. So don’t set a second flag, but extend the existing value comma-separated, e.g. --entrypoints.websecure.http.middlewares=header@file,crowdsec@file. Only if no middleware is attached to this entrypoint so far do you add the flag anew as above.No second file-provider flag
--providers.file.filename=… or --providers.file.directory=…). Traefik does not allow filename and directory at the same time – so don’t add a second flag blindly. Put the following middleware file either into your existing dynamic directory, or – if your provider points to a single filename file – write the http.middlewares block right in there. The paths below (~/traefik/dynamic or /etc/traefik/dynamic) only apply if you already use a directory.Create the middleware (path adapted to your existing file provider):
# ~/traefik/dynamic/crowdsec.yaml
http:
middlewares:
crowdsec:
plugin:
bouncer:
enabled: true
crowdsecMode: stream
crowdsecLapiScheme: http
crowdsecLapiHost: crowdsec:8080
crowdsecLapiKey: "YOUR_BOUNCER_KEY"crowdsecLapiKey is the same key as in step 2 – the bouncer identifies itself with it at the LAPI. With crowdsecMode: stream the bouncer pulls the block list completely in advance at a fixed interval and answers every request from the local cache – the mode recommended by the plugin, because it doesn’t load the LAPI on every request.
Make sure Traefik also sees the directory. If your existing file provider already uses a mounted dynamic directory, the new file is in it anyway – then there’s nothing to do here. If you create the directory anew, add it to Traefik’s volumes block:
- ./dynamic:/etc/traefik/dynamic:roRestart Traefik so it downloads the plugin and loads the middleware:
cd ~/traefik && docker compose up -d
docker compose logs -f traefikThe log must show no ERR about the plugin or the middleware (Traefik v3 logs only errors at the default level – a quiet output is the good sign). Check from the CrowdSec side that the bouncer has connected:
docker exec crowdsec cscli bouncers list Name IP Address Valid Last API pull Type
TRAEFIK 172.19.0.2 ✔️ 2026-07-23T10:49:58Z Crowdsec-Bouncer-Traefik-PluginA set Last API pull proves: the plugin talks to the LAPI.
Step 4: Arm it and test the ban
Call one of your apps – it should respond perfectly normally:
curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_APP.YOUR_DOMAIN/200Now you test-ban an IP by hand and see whether the bouncer locks it out. Take your own public IP (curl -s https://ifconfig.me):
docker exec crowdsec cscli decisions add --ip YOUR_TEST_IP --duration 5m --reason "test"New bans don't take effect immediately
updateIntervalSeconds, default 60 seconds), in live mode from the per-IP cache of each decision (defaultDecisionSeconds, default 60 seconds). In real operation this is irrelevant; when testing you have to wait briefly. Whoever wants to reduce the reaction time lowers the respective value – i.e. updateIntervalSeconds (stream) or defaultDecisionSeconds (live) – in the middleware, instead of switching modes.After the wait, the banned IP only gets:
curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_APP.YOUR_DOMAIN/403Lift the test ban again:
docker exec crowdsec cscli decisions delete --ip YOUR_TEST_IPStep 5: Let it detect real attacks
The actual point: CrowdSec bans on its own, without you writing rules. As soon as a scanner probes your domain for paths (many 404 in a short time), the scenario crowdsecurity/http-probing kicks in. This is what a real, automatically detected ban looks like:
docker exec crowdsec cscli alerts list ID value reason country as decisions
4 Ip:109.250.21.197 crowdsecurity/http-probing DE 8881 1&1 Versatel GmbH ban:1docker exec crowdsec cscli decisions list Source Scope:Value Reason Action Country AS
crowdsec Ip:109.250.21.197 crowdsecurity/http-probing ban DE 8881 1&1 Versatel GmbHCrowdSec automatically enriches the ban with country and provider (AS). Besides the self-detected attacks, bans with the source lists:crowdsecurity/... appear here over time – that’s the community blocklist: IPs that already stood out negatively worldwide, you block before they hit you the first time.
Whoever prefers to see the bans graphically instead of in cscli tables enrolls the instance for free in the CrowdSec console (the enroll command is in “Maintenance & backups”). There the same decisions appear presented nicely:

The HTTP scenarios installed with the Traefik collection cover what real scanners try all day: probing non-existent paths (http-probing), searching for sensitive files and backdoors, path traversal and SQL injection/XSS probes, suspicious user agents of known scanners, as well as the exploitation of known CVEs (from the collection crowdsecurity/http-cve). Which scenarios are active is shown by docker exec crowdsec cscli scenarios list.
One notch sharper: AppSec (WAF)
Whether CrowdSec reads your logs at all is shown by the metrics overview:
docker exec crowdsec cscli metrics| Source | Lines read | Lines parsed |
| file:/var/log/traefik/access.log | 54 | 54 |If it says Lines read: 0, no log is arriving – then back to step 1.
Step 6: Don’t lock yourself out
A false alarm against your own maintenance IP is annoying. Enter your fixed IP as trusted in the middleware – the bouncer then doesn’t even check it. Add to ~/traefik/dynamic/crowdsec.yaml under bouncer::
clientTrustedIPs:
- "YOUR_FIXED_IP/32"If you did lock yourself out, one command lifts the ban immediately – and in an emergency you can always reach the server via the VNC console in the SCP:
docker exec crowdsec cscli decisions delete --ip YOUR_IP # one IP
docker exec crowdsec cscli decisions delete --all # all bansWhen things go wrong
A freshly set ban doesn’t take effect, the IP keeps getting 200. The bouncer caches the
decisions and, in stream mode, pulls new ones only after the pull interval (up to ~60 seconds). Wait
briefly. Whoever wants to reduce the reaction time lowers updateIntervalSeconds in the middleware
(step 3).
cscli metrics shows Lines read: 0 for the log file. CrowdSec isn’t reading the logs. Check
that Traefik really writes to /var/log/traefik/access.log (step 1), that both containers mount
the same host path, and that the acquis.yaml names exactly this path.
After restarting Traefik, all apps get an error or don’t load. In stream mode (set above),
the bouncer can lock down after several failed LAPI pulls – controlled via updateMaxFailure. If
the CrowdSec LAPI is unreachable, the bouncer then blocks in doubt instead of letting through
unprotected. That’s safe, but makes CrowdSec a critical component: if the crowdsec container
isn’t running, your site is affected. Check with docker ps that it’s Up; restart: unless-stopped (set above) brings it back automatically after a restart.
CrowdSec bans lots of harmless or no real visitors – the detected IP is always your CDN’s. If a
CDN like Cloudflare sits in front of Traefik, Traefik only sees its IP. Then have the real
client IP read from the X-Forwarded-For header: fill forwardedHeadersTrustedIPs in the
middleware with the CDN networks – otherwise you end up banning the CDN. Directly at netcup (without
a CDN) there’s nothing to do here.
docker compose up -d on Traefik reports a plugin error. Traefik downloads the plugin from the
net on startup. Check that the server may go out (the netcup
firewall blocks nothing outbound, UFW likewise) and that
modulename and version are exactly right.
Maintenance & backups
Keep scenarios current. CrowdSec’s strength is maintained detection rules – pull them regularly:
docker exec crowdsec cscli hub update && docker exec crowdsec cscli hub upgrade. You bump the image (crowdsecurity/crowdsec) and the plugin version (v1.6.0) deliberately; read the release notes on major versions. CrowdSec develops briskly – a good candidate for regular version checks.Rotate the access log. Traefik’s
/var/log/traefik/access.logotherwise grows unbounded. Set up a logrotate rule – mandatorily withcopytruncate: this variant copies the file away and empties the original in place, instead of renaming it and creating a new one. Only this way does CrowdSec’s tail keep the file access (with rename/create it would lose the handle and read nothing anymore). Create/etc/logrotate.d/traefik:Ausgabe/var/log/traefik/access.log { daily rotate 7 compress missingok notifempty copytruncate }After the truncate, CrowdSec reads on perfectly normally from the file that’s growing again – a restart of the container isn’t needed.
fail2ban stays. Don’t remove fail2ban – it continues to protect your SSH access at the host level, while CrowdSec covers the web apps. Two tools, two responsibilities.
Backups. What you must back up is above all your configuration: the folder
~/crowdsec(compose.yaml,acquis.yaml) and~/traefik/dynamic/crowdsec.yamlwith the bouncer key. Include them in your Restic backup. The block lists themselves are ephemeral and rebuild on their own – no backup needed.Optional: the CrowdSec dashboard. Whoever wants to see the attacks graphically enrolls the instance for free in the CrowdSec console (
docker exec crowdsec cscli console enroll YOUR_ENROLL_KEY). The community blocklist runs without enrollment too.Integrate it into your monitoring.
cscli metricsgives you numbers for the terminal; for continuous observation, CrowdSec provides a Prometheus endpoint (enable it in the config underprometheus:) that you can hang on your Grafana stack. Then you see bans by origin and scenario at a glance:
CrowdSec in Grafana: active bans by origin and scenario, fed from the Prometheus endpoint Honest about the effort: After setup, CrowdSec runs largely on its own. Plan a
hub upgradeand a look atcscli metricsand the container version once a month.
Send feedback: feedback@serverkueche.de
You might also like

Monitoring with Grafana & Prometheus: your server in live dashboards
A complete monitoring stack of Prometheus, node-exporter, cAdvisor and Grafana behind Traefik – with live metrics for …

Self-hosting Jellyfin: your own media server behind Traefik
Set up Jellyfin with Docker behind Traefik: stream your movies, series and music – with HTTPS and the library on …

Self-hosting Immich: your private photo backup behind Traefik
Set up Immich with Docker behind Traefik: the self-hosted alternative to Google Photos – with automatic phone backup, …