Back to Article List

CrowdSec firewall bouncer setup: iptables and nftables

CrowdSec firewall bouncer setup: iptables and nftables - CrowdSec firewall bouncer setup: iptables and nftables

Updated 22 October 2026

There's a specific moment that brings people to this article. cscli decisions list shows the attacker banned, the access log shows the same IP still cheerfully probing, and the realization lands that a decision in CrowdSec's database is just a row until something enforces it. The firewall bouncer is that something for Linux servers: a small Go daemon that streams decisions from the local API into kernel packet filters.

Here's how I configure and verify it, and how to keep Docker from quietly undermining the whole thing. I'm assuming a running Security Engine; if you don't have one yet, start with my CrowdSec install guide for Debian.

Package choice: iptables mode or nftables mode

Two packages, one binary, different modes. crowdsec-firewall-bouncer-iptables drives the classic toolchain with ipset-backed match sets, while crowdsec-firewall-bouncer-nftables talks to nftables natively. Debian 12, Debian 13 and Ubuntu 24.04 all use nftables as the real packet filter, with the iptables command surviving as a compatibility shim. The firewall bouncer docs have a quick test: run iptables -V, and if the output mentions nf_tables you're on the shim. On any modern Debian or Ubuntu I install the nftables package and skip the legacy layer; iptables mode still works fine through the shim, it's just one indirection more than necessary.

iptables -V on Ubuntu 24.04 printing iptables v1.8.10 (nf_tables), the compatibility shim

nf_tables in brackets: iptables is the shim on Ubuntu 24.04.

sudo apt update
sudo apt install crowdsec-firewall-bouncer-nftables

Run the update first on a freshly deployed server. LumaDock's nginx + CrowdSec template already ships this package, and with the package lists from the image build apt calls 0.0.34 the newest version even though a later release exists.

When the engine runs on the same host, the package registers itself during install (it runs cscli bouncers add on your behalf) and starts the service. sudo cscli bouncers list should immediately show it with a fresh pull timestamp, under a name like cs-firewall-bouncer-1792659514 (the number is a Unix timestamp).

Switching an existing install from one package to the other takes two extra steps. The packages conflict, so installing one removes the other, and dpkg then stops on crowdsec-firewall-bouncer.yaml because the file was modified since installation. Answer Y. The default N keeps your current file, mode: line included, so the iptables package would carry on in nftables mode. The new package also registers itself under a new cs-firewall-bouncer- name and leaves the old registration in cscli bouncers list, still marked valid, so delete the old one with sudo cscli bouncers delete. And leave the removed package in its removed state rather than purging it: its purge script runs cscli bouncers delete on the name stored in crowdsec-firewall-bouncer.yaml.id, a file both packages share, which would take out the registration your running bouncer depends on.

crowdsec-firewall-bouncer.yaml walkthrough

The config lives at /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml, and the commented default on GitHub is worth reading once in full. These are the keys I actually touch:

mode: nftables
update_frequency: 10s
api_url: http://127.0.0.1:8080/
api_key: YOUR_GENERATED_KEY
deny_action: DROP
disable_ipv6: false

update_frequency is how often the bouncer polls the LAPI for new and expired decisions; 10 seconds is the shipped default and I leave it alone. deny_action takes DROP or REJECT. DROP makes scanners sit through connection timeouts, REJECT fails fast and is kinder to the rare false positive; on internet-facing boxes I stay with DROP and let attackers waste their sockets. api_key gets filled in automatically for a local engine. For a remote one, generate a key with cscli bouncers add on the engine host and point api_url at it across the network.

Mode-specific settings live below those. In iptables mode, iptables_chains lists which chains receive the match-set rule (just INPUT by default), and the sets are capped at 131072 entries; ipset_size changes that, though the packaged file doesn't carry the key, so you add it yourself. In nftables mode there's an nftables: block naming the table and chain, crowdsec and crowdsec-chain for IPv4 out of the box, plus a set-only switch for people who'd rather own their ruleset and have the bouncer manage set contents alone. I like set-only mode on boxes with hand-built nftables configs; everywhere else, managed mode is fine. Below that block, nftables_hooks lists the netfilter hooks the bouncer attaches to, and the packaged config already enables both input and forward. In the ruleset they show up as chains named crowdsec-chain-input and crowdsec-chain-forward.

nftables block of the bouncer yaml: table crowdsec, chain crowdsec-chain, nftables_hooks input and forward

The packaged config: table and chain names, and both hooks already enabled.

How decisions become dropped packets

The bouncer mirrors decision state into sets, one per decision origin. In nftables mode they carry the origin in the name: crowdsec-blacklists-CAPI, crowdsec-blacklists-crowdsec and crowdsec-blacklists-cscli for IPv4, and crowdsec6-blacklists-CAPI and its siblings for IPv6. In iptables mode they're ipsets numbered crowdsec-blacklists-0, -1, -2 and crowdsec6-blacklists-0, and sudo ipset list -n shows which exist. A plain crowdsec-blacklists set no longer exists in either mode (bouncer 0.0.34), so older commands that name it fail. Each entry carries a timeout matching the decision's remaining duration, so bans expire in the kernel without the bouncer lifting a finger.

One quirk worth knowing: ipset can't store timeouts beyond 2147483 seconds, about 596 hours, so anything longer gets capped in iptables mode. Community blocklist entries and your own scenario bans land in separate sets, and each set gets one rule in the bouncer's chain; in iptables mode sudo iptables -S CROWDSEC_CHAIN shows them with comments such as CrowdSec: CAPI. That's why iptables -L looks nearly empty on a box that's blocking tens of thousands of IPs; the lists live in the sets, and the rules are a few lines.

sudo ipset list -n in iptables mode listing crowdsec-blacklists-0, -1, -2 and crowdsec6-blacklists-0

In iptables mode the sets are numbered, one per decision origin.

Verify with a test decision

sudo cscli decisions add --ip 203.0.113.42 --duration 5m --reason "bouncer smoke test"
sudo nft list ruleset | grep 203.0.113.42                  # nftables mode
sudo iptables -S CROWDSEC_CHAIN                            # iptables mode: which set is which
sudo ipset list crowdsec-blacklists-2 | grep 203.0.113.42  # iptables mode
sudo cscli decisions delete --ip 203.0.113.42

Within one update_frequency tick the test IP appears in the set for manual decisions: crowdsec-blacklists-cscli in nftables mode, and in iptables mode whichever numbered set the CrowdSec: cscli comment points to (it was -2 on the server this update was tested on). Grepping the whole ruleset for crowdsec works too, but once the community list has loaded it scrolls for thousands of lines.

nft ruleset with test IP 203.0.113.42 in crowdsec-blacklists-cscli and the input chain rule that drops that set

The full-ruleset view: the test IP sits in crowdsec-blacklists-cscli, and the input chain drops each origin's set.

If it doesn't, read /var/log/crowdsec-firewall-bouncer.log; authentication failures against the LAPI are the usual culprit and I've written up the API access forbidden fixes separately. For a true end-to-end test, ban a spare external IP you control (a phone hotspot works) and confirm new connections from it die. And if this is your first bouncer on a production machine, skim my safe rollout guide first; the confidence to test aggressively comes from knowing you can't lock yourself out.

Docker published ports and the DOCKER-USER chain

Now the trap. When Docker publishes a port it DNATs traffic in PREROUTING, and from there packets flow through FORWARD into Docker's own chains. Your bouncer's rule sits in INPUT. INPUT never sees that traffic. Containers stay reachable from banned IPs while bare-metal services are protected, which is precisely the kind of half-working state that goes unnoticed for months. Docker also rewrites its chains whenever the daemon feels like it, so anything you insert into the DOCKER chain itself gets bulldozed on restart. DOCKER-USER exists as the one chain Docker promises never to touch, evaluated before its own rules, and that's where enforcement belongs on a container host.

In iptables mode the fix is two lines of config, and the docs call it out explicitly:

iptables_chains:
  - INPUT
  - DOCKER-USER

Restart the bouncer and DOCKER-USER gets the same jump to CROWDSEC_CHAIN that INPUT has, covering forwarded container traffic.

iptables -L DOCKER-USER showing a jump to CROWDSEC_CHAIN after adding DOCKER-USER to iptables_chains

DOCKER-USER now jumps to CROWDSEC_CHAIN, so container traffic is checked too.

The manual equivalent you'll find in community threads, like the r/CrowdSec writeup on securing Docker services, inserts the rule by hand: iptables -I FORWARD -m set --match-set crowdsec-blacklists src -j DROP. With current bouncers that set name no longer exists, so you'd need one rule per numbered set, and none of them survive rule flushes. I'd rather the bouncer own it. In nftables mode the packaged config already hooks forward through nftables_hooks, so there is nothing to add; a ban against a published container port wasn't part of this guide's test run in nftables mode, though, so verify it on your own host with a spare IP. The acquisition half of the container story is in my CrowdSec Docker Compose guide.

UFW interactions

UFW and the bouncer program the same kernel firewall and mostly ignore each other politely. The bouncer inserts its rule at the top of the chain, ahead of UFW's allow rules, which is the ordering you want: a banned IP gets dropped before ufw allow 22 ever applies. Don't replicate CrowdSec bans as UFW rules, and after a ufw reload or disable/enable cycle, restart the bouncer so its rules land back in the right position. One documented annoyance: in ipset-only mode, UFW's managed chains can confuse the bouncer's metrics parsing, so treat those counters as approximate on UFW boxes.

Your idea deserves better hosting

24/7 support 30-day money-back guaranteeRefund covers the VPS plan on your first order. Cancel anytime
Billing Cycle

VPS.S1

28.41 RON Save  17 %
23.67 RON Monthly
  • 2 vCPU AMD EPYC
  • 2 GB RAMMEMORY
  • 30 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included

VPS.S3

71.10 RON Save  33 %
47.38 RON Monthly
  • 4 vCPU AMD EPYC
  • 6 GB RAMMEMORY
  • 70 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included

VPS.S4

94.81 RON Save  20 %
75.84 RON Monthly
  • 6 vCPU AMD EPYC
  • 8 GB RAMMEMORY
  • 80 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included

EPYC VPS.P1

42.64 RON Save  22 %
33.15 RON Monthly
  • 2 vCPU AMD EPYC
  • 4 GB RAMMEMORY
  • 40 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

EPYC VPS.P2

80.58 RON Save  24 %
61.61 RON Monthly
  • 2 vCPU AMD EPYC
  • 8 GB RAMMEMORY
  • 80 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

EPYC VPS.P4

142.24 RON Save  23 %
109.04 RON Monthly
  • 4 vCPU AMD EPYC
  • 16 GB RAMMEMORY
  • 160 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

EPYC VPS.P5

189.67 RON Save  25 %
142.24 RON Monthly
  • 8 vCPU AMD EPYC
  • 16 GB RAMMEMORY
  • 180 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

EPYC VPS.P6

284.54 RON Save  25 %
213.39 RON Monthly
  • 8 vCPU AMD EPYC
  • 32 GB RAMMEMORY
  • 200 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

EPYC VPS.P7

331.97 RON Save  29 %
237.11 RON Monthly
  • 16 vCPU AMD EPYC
  • 32 GB RAMMEMORY
  • 240 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

Genoa VPS.G2

118.53 RON Save  20 %
94.81 RON Monthly
  • 2 vCPUAMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture. AMD EPYC G4
  • 4 GB DDR5MEMORY
  • 50 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

Genoa VPS.G4

213.39 RON Save  22 %
165.96 RON Monthly
  • 4 vCPUAMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture. AMD EPYC G4
  • 8 GB DDR5MEMORY
  • 100 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

Genoa VPS.G6

426.83 RON Save  22 %
331.97 RON Monthly
  • 8 vCPUAMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture. AMD EPYC G4
  • 16 GB DDR5MEMORY
  • 200 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

Genoa VPS.G7

758.84 RON Save  22 %
592.83 RON Monthly
  • 8 vCPUAMD EPYC Genoa 4th generation 9xx4 with 3.25 GHz or similar, on Zen 4 architecture. AMD EPYC G4
  • 32 GB DDR5MEMORY
  • 250 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6IPv6 is currently unavailable in France, Finland or the Netherlands. included
  • Free auto backupsIncludes one backup slot you can set to run daily, weekly or monthly.

AMD Ryzen VPS.R1

80.58 RON Save  18 %
66.36 RON Monthly
  • 1 dedicated CPU AMD Ryzen 9 7950X with 4.5 GHz or similar, on Zen 4 architecture. vCPU
  • 4 GB DDR5MEMORY
  • 50 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • Auto backup included

AMD Ryzen VPS.R2

142.24 RON Save  17 %
118.53 RON Monthly
  • 2 dedicated CPUs AMD Ryzen 9 7950X with 4.5 GHz or similar, on Zen 4 architecture. vCPU
  • 8 GB DDR5MEMORY
  • 100 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • Auto backup included

AMD Ryzen VPS.R4

521.69 RON Save  18 %
426.83 RON Monthly
  • 8 dedicated CPUs AMD Ryzen 9 7950X with 4.5 GHz or similar, on Zen 4 architecture. vCPU
  • 32 GB DDR5MEMORY
  • 400 GB NVMeSTORAGE
  • Unmetered bandwidth
  • IPv4 & IPv6 included IPv6 support is currently unavailable in France, Finland or the Netherlands.
  • Auto backup included

*VAT excluded.

INCLUDED WITH EVERY PLAN

✓ No setup fees ✓1 Gbps network
✓ Full root access on KVM ✓ Free server monitoring ✓ Firewall management ✓24/7 support

IPv6, set capacity and performance

Don't set disable_ipv6: true just because your v6 traffic graph is flat. If the server has an AAAA record it gets scanned over v6 too, and an attacker who finds the v4 door locked will try the other one; the IPv6 sets cost nothing to keep active. On capacity: stacking third-party blocklists on top of the community feed can blow past the default set size, at which point the log fills with "IPSET Hash is full, cannot add more elements" and new bans silently fail. Add ipset_size with a higher value to the yaml (the packaged file doesn't include it) and restart the bouncer. My CrowdSec blocklists guide goes into how big those subscriptions actually get.

Performance questions come up every time someone sees tens of thousands of entries in a set. Both ipset and nftables sets are hash structures, constant-time lookup per packet regardless of size, so a huge blacklist costs the same as a tiny one. I've run sets north of 50,000 entries on 1 vCPU machines without a visible line on the CPU graph. The expensive part of CrowdSec is parsing logs. The blocking is nearly free, which is exactly why there's no good excuse to run the engine without it.

Questions?

What happens to my active bans when the bouncer restarts?

Nothing worth worrying about. On startup the bouncer pulls the full decision list from the local API and rebuilds its sets, so state converges within seconds. You're only exposed during the brief window between the set being recreated and repopulated, and I've never seen that matter in practice.

Ellie Grace Hayes
About the author

Ellie Grace Hayes

I grew up in Minnesota and started working remotely before I decided that travelling was going to become a permanent part of my life. And it kinda did, more or less... I tend to stay somewhere for a month or two, sometimes longer if I like the place (so "digital nomad" is technically accurate, even if most days are much less exciting than the term suggests). Usually I'm at a desk in a rented room trying to work out why a command behaved differently on two supposedly identical machines :)

I came into technical writing through support work & community tutorials. Linux was the first subject I wrote about, but since then I've wandered into containers, hosting, networking plus a lot of self-hosted software. I've played Minecraft since I was a kid and still go through phases where I get completely absorbed in it for a few weeks. I don't have a five-year plan. I've tried - it keeps becoming a six-month plan instead.

Deploy your next project in seconds

Bring your winning ideas online faster, with modern hardware and unmetered bandwidth. Join a European cloud trusted by thousands of developers and businesses worldwide.