Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Security Difficulty: Advanced

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.

· 13 min read ·Duration: approx. 45 minutes
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?

CrowdSec doesn’t replace fail2ban, it complements it. fail2ban is unbeatably simple for a single service on the host (SSH). CrowdSec plays to its strength with HTTP apps behind the reverse proxy: one bouncer protects all apps at once, the scenarios are maintained and current, and you get the community blocklist for free. The clean split: fail2ban stays on SSH, CrowdSec takes over the web layer. Both may run in parallel.

Prerequisites

  • A running reverse proxy with Traefik (the proxy network and the resolver le come 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.
🍳 Recommendation Ad

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.

Go to netcup →

💶 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:

YAML
      - "--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:

YAML
      - /var/log/traefik:/var/log/traefik

Create the directory on the host and restart Traefik:

Terminal
sudo mkdir -p /var/log/traefik
cd ~/traefik && docker compose up -d

Call one of your apps once and check that lines arrive:

Terminal
tail -n 2 /var/log/traefik/access.log

You 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:

Terminal
mkdir -p ~/crowdsec && cd ~/crowdsec
openssl rand -hex 32

Copy the output; it’s used in two places shortly. Create the acquisition – it tells CrowdSec which file with which log type to read:

YAML
# ~/crowdsec/acquis.yaml
filenames:
  - /var/log/traefik/access.log
labels:
  type: traefik

Now the compose.yaml. Replace YOUR_BOUNCER_KEY with the generated key:

YAML
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: true

What matters here:

  • COLLECTIONS: crowdsecurity/traefik installs the matching collection of parsers and scenarios for Traefik logs on the first start (including the HTTP base scenarios).
  • BOUNCER_KEY_TRAEFIK registers a bouncer named TRAEFIK with your key right at startup – that saves the detour via cscli bouncers add.
  • GID: "1000" runs the CrowdSec process under this group. 1000 is the usual default for the first user; if Traefik runs as root, the access.log is usually world-readable and the GID doesn’t matter anyway. If it’s set more tightly, check with ls -l /var/log/traefik/access.log and enter the GID of the file’s group.
  • The log is mounted read-only (:ro). CrowdSec is on the proxy network so the Traefik bouncer can reach the LAPI at crowdsec:8080 later.

Start the container and check that everything takes effect:

Terminal
docker compose up -d
docker exec crowdsec cscli collections list

The Traefik collection and the HTTP scenarios should appear as enabled:

Ausgabe
 crowdsecurity/base-http-scenarios    ✔️  enabled
 crowdsecurity/http-cve               ✔️  enabled
 crowdsecurity/traefik                ✔️  enabled

Check that the community blocklist is active – it costs nothing and is half the benefit of CrowdSec:

Terminal
docker exec crowdsec cscli capi status
Ausgabe
You can successfully interact with Central API (CAPI)
Sharing signals is enabled
Pulling community blocklist is enabled
Pulling blocklists from the console is enabled

Pulling 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:

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

The Traefik setup usually already has the file provider active (e.g. as --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):

YAML
# ~/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:

YAML
      - ./dynamic:/etc/traefik/dynamic:ro

Restart Traefik so it downloads the plugin and loads the middleware:

Terminal
cd ~/traefik && docker compose up -d
docker compose logs -f traefik

The 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:

Terminal
docker exec crowdsec cscli bouncers list
Ausgabe
 Name     IP Address  Valid  Last API pull         Type
 TRAEFIK  172.19.0.2  ✔️      2026-07-23T10:49:58Z  Crowdsec-Bouncer-Traefik-Plugin

A 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:

Terminal
curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_APP.YOUR_DOMAIN/
Ausgabe
200

Now 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):

Terminal
docker exec crowdsec cscli decisions add --ip YOUR_TEST_IP --duration 5m --reason "test"

New bans don't take effect immediately

The bouncer fetches the decisions in the background and caches them – it can take up to ~60 seconds for a fresh ban to take effect. That’s no bug but intended (less load on the LAPI). In stream mode the delay comes from the pull interval (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:

Terminal
curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_APP.YOUR_DOMAIN/
Ausgabe
403

Lift the test ban again:

Terminal
docker exec crowdsec cscli decisions delete --ip YOUR_TEST_IP

Step 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:

Terminal
docker exec crowdsec cscli alerts list
Ausgabe
 ID  value              reason                       country  as                      decisions
 4   Ip:109.250.21.197  crowdsecurity/http-probing   DE       8881 1&1 Versatel GmbH  ban:1
Terminal
docker exec crowdsec cscli decisions list
Ausgabe
 Source    Scope:Value        Reason                       Action  Country  AS
 crowdsec  Ip:109.250.21.197  crowdsecurity/http-probing   ban     DE       8881 1&1 Versatel GmbH

CrowdSec 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 CrowdSec console lists the active bans as a table: several IP addresses with the action “ban”, an expiry time and the triggering scenario http-probing, each “via crowdsec”
The CrowdSec console (optional, free): the same decisions as in cscli, just graphical

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)

Beyond pure IP blocking, CrowdSec can work as a web application firewall and detect malicious requests directly by their content (virtual patching). The Traefik plugin supports this AppSec mode (from plugin 1.2.0 and CrowdSec 1.6.0). For getting started, the scenario-based blocking shown here is enough; AppSec is the worthwhile next expansion step when a particularly exposed app needs additional protection.

Whether CrowdSec reads your logs at all is shown by the metrics overview:

Terminal
docker exec crowdsec cscli metrics
Ausgabe
| 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::

YAML
          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:

Terminal
docker exec crowdsec cscli decisions delete --ip YOUR_IP   # one IP
docker exec crowdsec cscli decisions delete --all           # all bans

When 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.log otherwise grows unbounded. Set up a logrotate rule – mandatorily with copytruncate: 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.yaml with 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 metrics gives you numbers for the terminal; for continuous observation, CrowdSec provides a Prometheus endpoint (enable it in the config under prometheus:) that you can hang on your Grafana stack. Then you see bans by origin and scenario at a glance:

    A Grafana dashboard with CrowdSec metrics: around 15,000 active bans total, broken down by origin (community blocklist vs. locally detected) and by scenario like ssh:bruteforce, generic:scan and http-probing
    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 upgrade and a look at cscli metrics and the container version once a month.

You might also like