Skip to content

CrowdSec

CrowdSec watches Traefik’s access logs and decides which IPs to ban for attack patterns (brute force, CVE probes, path traversal). On its own it detects but doesn’t block — blocking is added separately, in layers you choose.

Last run on a host 9 Nov 2026 · data restored from a backup

Pinned at v1.8.2 (released 26 Oct 2026) · the newest upstream release

In the upstream images: 0 CRITICAL · 10 HIGH with a published fix — per image

Needs: Traefik running.

The required baseline — detection only, blocks nothing yet.

  1. From core/crowdsec/:

    Terminal window
    cp .env.example .env
  2. Find the GID that owns Traefik’s log files, and set it as CROWDSEC_LOG_GID in .env:

    Terminal window
    stat -c '%g' ../traefik/volumes/logs/access.log
  3. Start:

    Terminal window
    docker compose up -d
Terminal window
docker exec crowdsec cscli lapi status
docker exec crowdsec cscli metrics show acquisition

Expect “You can successfully interact with Local API” and a row for file:/var/log/traefik/access.log with lines_read > 0.

The three layers, and what each one covers

Section titled “The three layers, and what each one covers”

They are separate components with one thing in common: the engine decides, nothing else does. It writes decisions to a list; each enforcement layer polls that list and applies it in its own place. A decision with no enforcer attached does nothing at all.

Where it acts Catches
Engine reads the proxy’s access log decides — blocks nothing itself
Proxy plugin inside Traefik, per request HTTP attacks; answers 403 and sees the decrypted request
Firewall bouncer nftables on the host, scoped to the public interface any port arriving from outside — the packet is dropped before a connection exists

The proxy plugin has the better view: it knows the URL and can say why it refused. The firewall bouncer has the wider one — port scans and anything not behind the proxy never reach Traefik at all.

Neither replaces the provider firewall in front of the machine. That decides which ports may enter at all; CrowdSec decides which sources may continue through the ports already open. And neither applies to the VPN you administer the host over — that interface is deliberately outside the enforcement path, for reasons the firewall section explains.

Running both is not redundancy: a decision reaches the kernel’s address set in about 15 seconds and clears again in about 10, while the proxy plugin acts on its own polling cycle in between.

What the firewall layer inherits the moment you enable it

Section titled “What the firewall layer inherits the moment you enable it”

The engine subscribes to CrowdSec’s community blocklist from the start. On a running host that list holds roughly 15,000 addresses — known-bad, with nothing enforcing them, because the engine only decides.

Bans the engine finds itself, such as an address probing for common exploit paths, are in the same position: without host enforcement they apply only to HTTP through the proxy.

The two protective layers in this Blueprint do not add up — they take turns, and that determines when it earns its keep.

Behind access-vpn or access-trusted-vpn the engine sees close to nothing: 120 requests against a VPN-only host move its counter by about 3. That is not a problem, because nothing got through either. The engine becomes worth its keep the moment an application is public.

As shipped, the engine reads only the proxy’s access log. SSH detection needs the host’s authentication log mounted into the container; Debian 13 keeps no /var/log/auth.log and writes to the journal instead, so there the proxy is the engine’s only source. The SSH setup is in core/crowdsec/docs/firewall-bouncer.md.

The WAF layer is narrower than the name suggests

Section titled “The WAF layer is narrower than the name suggests”

The optional AppSec engine inspects each request against rule sets. Against a public router, the shipped rules give:

Request Result
?id=1' OR '1'='1 passes
?q=<script>alert(1)</script> passes
?f=../../../../etc/passwd passes
/vendor/phpunit/…/eval-stdin.php blocked

The shipped rules are virtual patches for named CVEs plus a small generic set — not a general injection filter. The engine that evaluates them is Coraza, built into CrowdSec’s AppSec component rather than a separate service; the OWASP Core Rule Set could run in it but is not installed here. The shipped middleware is fail-open: with the engine stopped, requests pass. It stops the mass scanning that finds any public host within minutes of a certificate being issued, which is most of what arrives uninvited. It will not stop an attack crafted against your application’s own parameters, and it is no reason to relax anything about input handling.

The upside of that narrowness: far fewer false positives than a full WAF. That matters, because inspection is one decision for the deployment — CROWDSEC_APPSEC_ENABLED=true in core/traefik/.env puts it in front of every application at once, and one application with known false positives holds the whole deployment at decisions only.

The engine above is detection only. Two enforcement layers add blocking. They are independent of each other — pick by what you expose, not in order.

Rejects requests from banned IPs with a 403, inside Traefik.

  1. Generate a key:

    Terminal window
    docker exec crowdsec cscli bouncers add traefik-bouncer
  2. In core/traefik/.env, switch the integration on and add the key:

    CROWDSEC_BOUNCER_ENABLED=true
    CROWDSEC_BOUNCER_PLUGIN_VERSION=v1.7.1
    CROWDSEC_BOUNCER_KEY=<key>
  3. Render, validate and recreate Traefik. render.sh emits the plugin block and the entrypoint list into config/traefik.yml and the threat-crowdsec / threat-crowdsec-appsec middlewares into config/dynamic/crowdsec.yml — no template is edited. The plugin block and the entrypoint list are static configuration and need the recreate; the middlewares are hot-reloaded.

    Terminal window
    bash ops/scripts/render.sh
    bash ops/scripts/validate.sh
    docker compose up -d --force-recreate traefik
  4. Confirm the entrypoint carries it. Nothing is attached by hand: the recreate placed threat-crowdsec@file on the websecure entrypoint, in front of every router — the VPN-only ones included.

    Terminal window
    grep -A2 ' middlewares:' config/traefik.yml # threat-crowdsec@file, compress@file

    A router cannot opt out; an address that must never be refused is a CrowdSec allowlist (CROWDSEC_ALLOWLIST_ADMIN in core/crowdsec), never a label. CROWDSEC_APPSEC_ENABLED=true puts threat-crowdsec-appsec@file there instead — request inspection for every application at once; read core/crowdsec/docs/appsec.md first.

Verify:

Terminal window
docker exec crowdsec cscli bouncers list
# Expect an IP address and a recent "Last API pull"
Database SQLite inside volumes/data/ — no database server and no dump hook
State volumes/data/ (decisions, alerts, machine and bouncer credentials) · volumes/config/ (installed collections, parsers, local API credentials)
Reproducible the acquisition config in config/ — versioned in git
Quiescing Required. A file copy of a live SQLite database can capture a torn state that restores as a corrupt file. Stop the container, or snapshot the filesystem, before copying volumes/data/.

For borgmatic, back the directory up as a source directory and quiesce it: borgmatic’s SQLite support addresses a single file, and this one lives inside the data directory.

The bouncer credentials are the part that matters. The Traefik bouncer’s key lives in core/traefik/.env (CROWDSEC_BOUNCER_KEY) and is registered against the local API in volumes/data/; the engine’s own machine credentials are in volumes/config/. Restore them together: one without the other leaves bouncers authenticating against an API that does not know them. Losing the decision list is survivable, because bans rebuild from live traffic; losing the credentials means enrolling every bouncer again by hand.

A restored copy brings back the decisions, alerts, collections, machine and bouncer, and the bouncer key from core/traefik/.env authenticates against it. To check a copy without touching the live one, start it on its own network with DISABLE_ONLINE_API=true, so it does not speak to CrowdSec’s central API as the same machine.

Terminal window
# Read the release notes first: https://github.com/crowdsecurity/crowdsec/releases
# Bump APP_TAG in .env, then:
docker compose pull
docker compose up -d
docker exec crowdsec cscli lapi status
docker exec crowdsec cscli hub update && docker exec crowdsec cscli hub upgrade

The hub commands refresh the collections, parsers and scenarios; run them monthly as well, independent of an image update.

“The site does not load for me” — check this first, not last. Once the firewall layer is running, the most likely cause of one person being unable to reach a service is that their address is on a blocklist. The packet is dropped in the kernel, so there is no log line anywhere to tell you — not in the proxy, not in the application.

Terminal window
docker exec crowdsec cscli decisions list --ip <their address>
docker exec crowdsec cscli decisions delete --ip <their address>

Most of those decisions did not come from your server. The engine subscribes to CrowdSec’s community blocklist, which on a running host means around 15,000 addresses other installations reported. That is what the subscription delivers, and it also means a shared CGNAT address, a VPN exit or a compromised neighbour can take a legitimate visitor with it.

The bouncer never polls — cscli bouncers list shows no Last API pull. Loading the plugin is not enough: the polling loop starts only once the middleware is in a request path, which the entrypoint list is after Traefik has been recreated. Recreate the container (docker compose up -d --force-recreate traefik in core/traefik/), then check again. There is no error message for this.

A ban you set does not seem to work when you test it from the server itself — it is the test that is wrong. A request made on the host arrives through the Docker bridge, so the proxy records the gateway as the client and the address you banned never appears as a sender. Read the address out of the access log, ban that one, and have the real client retry.

AppSec refuses every request — the engine is unreachable and the middleware was set to fail closed by hand; the shipped threat-crowdsec-appsec is fail-open. Check that something answers on 7422 from the proxy container; a 401 is correct there, connection refused is not.

A legitimate IP got banned. Start narrow, and check the result rather than assuming it:

Terminal window
docker exec crowdsec cscli decisions list --ip <ip> # what exists, and from which origin
docker exec crowdsec cscli decisions delete --ip <ip>
docker exec crowdsec cscli decisions list --ip <ip> # expect nothing

If the decision comes back after you delete it, deleting harder is not the fix. Find out where it comes from:

Terminal window
docker exec crowdsec cscli decisions list --ip <ip> -o json # origin, scenario, duration
docker exec crowdsec cscli alerts list --ip <ip> # what triggered it

An origin of crowdsec means your own engine decided it and will decide it again from the same traffic. lists or CAPI means it arrived with the community blocklist. A local re-detection needs a whitelist entry, not a deletion.

A whitelist parser only covers what your own engine derives from its logs — community-blocklist decisions arrive ready-made and never pass it. To exempt an address whatever the source, use an allowlist (cscli allowlists), which applies to every decision the engine serves.

A ban that keeps coming back is a different problem — the engine is still seeing the behaviour that caused it. A whitelist entry stops decisions being created at all, which is the right tool for your own monitoring, office address or a known crawler.

Full operational reference — health checks, whitelisting, false positives, SSH detection, geo-blocking, emergency procedures: core/crowdsec/docs/runbook.md.