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.
Installation
Section titled “Installation”The required baseline — detection only, blocks nothing yet.
-
From
core/crowdsec/:Terminal window cp .env.example .env -
Find the GID that owns Traefik’s log files, and set it as
CROWDSEC_LOG_GIDin.env:Terminal window stat -c '%g' ../traefik/volumes/logs/access.log -
Start:
Terminal window docker compose up -d
Verify
Section titled “Verify”docker exec crowdsec cscli lapi statusdocker exec crowdsec cscli metrics show acquisitionExpect “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.
What it can and cannot see
Section titled “What it can and cannot see”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.
Going further
Section titled “Going further”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.
-
Generate a key:
Terminal window docker exec crowdsec cscli bouncers add traefik-bouncer -
In
core/traefik/.env, switch the integration on and add the key:CROWDSEC_BOUNCER_ENABLED=trueCROWDSEC_BOUNCER_PLUGIN_VERSION=v1.7.1CROWDSEC_BOUNCER_KEY=<key> -
Render, validate and recreate Traefik.
render.shemits the plugin block and the entrypoint list intoconfig/traefik.ymland thethreat-crowdsec/threat-crowdsec-appsecmiddlewares intoconfig/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.shbash ops/scripts/validate.shdocker compose up -d --force-recreate traefik -
Confirm the entrypoint carries it. Nothing is attached by hand: the recreate placed
threat-crowdsec@fileon thewebsecureentrypoint, in front of every router — the VPN-only ones included.Terminal window grep -A2 ' middlewares:' config/traefik.yml # threat-crowdsec@file, compress@fileA router cannot opt out; an address that must never be refused is a CrowdSec allowlist (
CROWDSEC_ALLOWLIST_ADMINincore/crowdsec), never a label.CROWDSEC_APPSEC_ENABLED=trueputsthreat-crowdsec-appsec@filethere instead — request inspection for every application at once; readcore/crowdsec/docs/appsec.mdfirst.
Verify:
docker exec crowdsec cscli bouncers list# Expect an IP address and a recent "Last API pull"Drops packets from banned IPs at the host firewall, before Docker and Traefik see them. Runs as a host package, not a container.
Host enforcement is experimental. Building the firewall state, rebuilding it on restart and tearing it down again work as described on a running host; the scoped blocking rule is applied by hand from the repository reference, with a timed rollback armed first. So this tab describes the model rather than offering a switch to flip.
The bouncer reaches the engine on the host’s loopback. The base stack publishes no host port. The firewall-bouncer.yml overlay adds the local API as 127.0.0.1:8081: set COMPOSE_FILE=docker-compose.yml:firewall-bouncer.yml in core/crowdsec/.env, run docker compose up -d, and point the bouncer at it in crowdsec-firewall-bouncer.yaml.local with api_url: http://127.0.0.1:8081/ — the package’s own default is 8080. The port is 8081 so that apps/unifi, which publishes 8080 for its devices, can run on the same host; CROWDSEC_LAPI_PORT changes it. Traefik does not use this port: it reaches the engine at crowdsec:8080 over the Docker network.
What it will cover: individual banned addresses, IPv4 and IPv6. CrowdSec can also decide against a whole address range, and those are not enforced correctly at the host firewall by the packaged bouncer — a range ends up protecting only its first address. It blocks less than you might expect, never more, and it cannot affect your management access. Treat host enforcement as covering single addresses, and leave range decisions to the proxy layer.
How the safe model works
Section titled “How the safe model works”Two ownership changes make the difference.
The bouncer runs in set-only mode. It keeps the two lists of banned addresses — one IPv4, one IPv6 — up to date, and installs no blocking rule of its own. The Blueprint owns everything else: the lists themselves, the containers they live in, and the blocking rule, because the rule is where the scope lives. The Blueprint also rebuilds that state from empty every time the bouncer starts, so a stale entry from a previous run can never end up being enforced.
The rule matches the public interface explicitly. Traffic arriving on the VPN interface cannot match it. That is a property of the rule rather than of a list, so it holds even if an allowlist is wrong, missing, or ignored.
Internet │ ▼Provider firewall which ports may enter at all │ ▼public interface │ ├── CrowdSec host enforcement banned source → dropped here │ (optional, experimental) │ ▼Docker → Traefik → applications
VPN management │ ▼management interface │ └── never enters CrowdSec host enforcementTwo things that are easy to get wrong
Section titled “Two things that are easy to get wrong”Published container ports are not host traffic. Docker rewrites the destination and forwards the packet, so it never passes the host’s inbound path. A blocking rule attached there filters no application traffic at all — while still being able to drop administrative traffic, which does take that path. The rule belongs on the forwarding path.
A VPN range is not a private range. CrowdSec’s built-in exemptions cover the classic private networks and loopback. Tailscale’s IPv4 range is carrier-grade NAT space used by real ISP customers, and its IPv6 range is not named either — so neither is exempt by default, and CGNAT addresses do appear in community blocklists.
The engine-level fix is a native allowlist, and it is worth having on its own even without host enforcement, because it also stops the proxy layer refusing your own browser:
docker exec crowdsec cscli allowlists create tailnet \ -d "Trusted VPN management plane"docker exec crowdsec cscli allowlists add tailnet 100.64.0.0/10 fd7a:115c:a1e0::/48docker exec crowdsec cscli allowlists check <an-address-in-that-range>Substitute your own VPN ranges. Adding the allowlist also clears decisions it already covers.
Changing a host firewall safely
Section titled “Changing a host firewall safely”Two rules, because a firewall mistake locks out the session that made it.
No firewall change may depend on the session applying it. Arm an independent timed rollback first, prove it is armed, then apply. Test from a second machine, and open new connections — an existing one survives on connection state and tells you nothing.
Test both address families. IPv6 is not a spare key. A ban lands on one address, so the other family keeps working until it does not; the same machine can be banned on both within hours.
A reboot needs its own guard, because a timer does not survive one.
Setup, the exact rules, the service lifecycle, the rollback and the current verification status are in the repository reference — core/crowdsec/docs/firewall-bouncer.md.
Backup
Section titled “Backup”| 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.
Updates
Section titled “Updates”# Read the release notes first: https://github.com/crowdsecurity/crowdsec/releases# Bump APP_TAG in .env, then:docker compose pulldocker compose up -ddocker exec crowdsec cscli lapi statusdocker exec crowdsec cscli hub update && docker exec crowdsec cscli hub upgradeThe hub commands refresh the collections, parsers and scenarios; run them monthly as well, independent of an image update.
Troubleshooting
Section titled “Troubleshooting”“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.
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:
docker exec crowdsec cscli decisions list --ip <ip> # what exists, and from which origindocker exec crowdsec cscli decisions delete --ip <ip>docker exec crowdsec cscli decisions list --ip <ip> # expect nothingIf the decision comes back after you delete it, deleting harder is not the fix. Find out where it comes from:
docker exec crowdsec cscli decisions list --ip <ip> -o json # origin, scenario, durationdocker exec crowdsec cscli alerts list --ip <ip> # what triggered itAn 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.
Repository files
Section titled “Repository files”core/crowdsec/README.md— full setup for all three phasescore/crowdsec/docs/runbook.md— day-to-day operationscore/crowdsec/docs/firewall-bouncer.md— host-firewall remediation in depth, including SSH detection
© 2026 SecDockBlueCode Apache-2.0Site content CC BY-NC 4.0