Cs-routeros-bouncer: CrowdSec bans enforced on RouterOS through the API (address lists + raw rules, open source, MIT)

CrowdSec reads logs, spots attacks and bans the source. On its own, a ban only takes effect where a bouncer applies it, usually on the same server. cs-routeros-bouncer takes the bans to the MikroTik: every decision becomes a dynamic entry in a RouterOS address list, and a rule in raw drops it on the way in, before connection tracking, NAT or anything behind the router sees it.

It has been running on my RB5009 since February. It came up in a couple of threads here, but I never presented it properly, so here it is.

GitHub: GitHub - jmrplens/cs-routeros-bouncer: CrowdSec bouncer for MikroTik RouterOS: manages address lists and firewall rules via the RouterOS API · GitHub
Docs (English and Spanish): https://jmrp.io/docs/cs-routeros-bouncer/
Also listed on the CrowdSec Hub: https://app.crowdsec.net/hub/author/jmrplens/remediation-components/cs-routeros-bouncer

What it writes on the router

With the default configuration, eight rules, the same four for IPv4 and IPv6:

/ip firewall raw
add chain=prerouting action=passthrough comment="crowdsec-bouncer:raw-prerouting-counting-v4 @cs-routeros-bouncer"
add chain=prerouting action=drop src-address-list=crowdsec-banned comment="crowdsec-bouncer:raw-prerouting-input-v4 @cs-routeros-bouncer"
/ip firewall filter
add chain=input action=passthrough comment="crowdsec-bouncer:filter-input-counting-v4 @cs-routeros-bouncer"
add chain=input action=drop src-address-list=crowdsec-banned comment="crowdsec-bouncer:filter-input-input-v4 @cs-routeros-bouncer"

The passthrough rules only count traffic for the metrics and can be turned off. The rules go to the top of their chains (or before/after a rule you name), and they are removed when the bouncer stops. A ban looks like this:

/ip firewall address-list
add list=crowdsec-banned address=203.0.113.7 timeout=3h59m50s comment="crowdsec-bouncer|crowdsec|crowdsecurity/ssh-bf|2026-10-05T10:10:12Z @cs-routeros-bouncer"

The entries are dynamic and carry the ban's remaining time as their timeout, so they expire on the router even if the bouncer is down, and they never end up in the saved configuration.

How it gets there:

  • New bans and unbans arrive from the CrowdSec LAPI every 10 s, and each one is a single API call. With the in-memory cache an add takes 1–3 ms on the RB5009, and an unban removes the entry by its id when the bouncer already knows it, instead of searching the list.
  • At start, and then every 15 min by default, it compares the router's lists with CrowdSec and fixes any difference: missing entries are added, stale ones removed. Entries in the same lists that it did not create are left alone.
  • Missing entries are added in chunks of 100 per RouterOS script. On 7.8rc1 and later that is a single /execute call, so nothing is stored and nothing reaches the router log. Before 7.8 it uses a temporary /system/script.
  • The API user needs only read,write,api.

Everything is optional around that: IPv4 and IPv6 separately, the filter and raw tables separately, more chains (forward), reject instead of drop, connection-state, in-interface/interface-list, a whitelist list, blocking outgoing traffic to banned addresses, and rule logging with a prefix. The firewall rules page lists every rule each setting produces.

What it has stopped on mine

The drop rules have log=yes on my router, and the log goes to Loki. From 8 September to 4 October, with only my own CrowdSec's decisions (no community blocklist):

  • 777,547 packets dropped from 7,088 banned addresses. That's 13,600 a day at the median, with days up to 93,000.
  • 84% were TCP SYNs, mostly to random ports: the same scanners still knocking after the ban.
  • 233,075 of them went to 80 and 443, which this router forwards to a reverse proxy. The raw drop happens before the dst-nat, so none of those packets reached it.


Packets dropped per day by the bouncer's rules on my RB5009, 2026-09-08 to 10-04, own CrowdSec decisions only. Counted from the router's own firewall log in Loki.

Most of those bans come from the router itself. CrowdSec can read the RouterOS log too: the a1ad/mikrotik collection on the Hub parses it and detects brute force and port scans, and I run a couple of my own scenarios on top. About 90% of the 2,590 active decisions on mine came from that log (scans of the router's management ports and hits on a few honeypot rules), the rest from the web server. So the router reports the attack, CrowdSec decides, and the bouncer blocks it on that same router.

What it costs the router

The heaviest moment is the first sync, when the lists are empty. Measured on the same RB5009 (RouterOS 7.24.4) with CrowdSec's community blocklist on, 17,361 entries:


First sync with the defaults on an RB5009, RouterOS 7.24.4: 17,361 entries written in 13.7 s at about 31% CPU (four cores), with a 45% peak over 100 ms. RAM steps up about 18 MiB when the bouncer connects and ends 16 MiB above where it started. CPU and RAM sampled at 10 Hz from inside the router with mikroscope.

Bulk-add method Writing 17,361 entries Mean CPU CPU time above idle
/execute (default, RouterOS 7.8+) 13.7 s 31% 12.8 core·s
stored /system/script (RouterOS < 7.8) 24.1 s 33% 24.3 core·s
bulk_add_method: api, one call per entry 22.8 s 35% 26.5 core·s

After that, a ban or unban is one API call. A periodic check that finds nothing to fix still reads both lists back: about 1.5 s of router work at this size, every 15 minutes by default. The bouncer itself uses about 30 MB of RAM and almost no CPU on the host it runs on, with 2,600 decisions.

Setting it up

On the router, a user with only read,write,api:

/user group add name=crowdsec policy=read,write,api,!sensitive,!ftp,!local,!ssh,!reboot,!policy,!test,!password,!sniff,!romon,!rest-api
/user add name=crowdsec group=crowdsec password=CHANGE_ME

On the CrowdSec host, cscli bouncers add cs-routeros-bouncer gives you the API key. Then the bouncer runs anywhere that reaches both (Docker, a systemd service, or a plain binary for Linux, FreeBSD, macOS or Windows), with this as its whole configuration:

crowdsec:
  api_url: "http://localhost:8080/"
  api_key: "the key from cscli"
mikrotik:
  address: "192.168.88.1:8728"   # 8729 with tls: true
  username: "crowdsec"
  password: "CHANGE_ME"

Or as environment variables in Docker:

docker run -d --name cs-routeros-bouncer --restart unless-stopped \
  -e CROWDSEC_URL=http://crowdsec:8080/ -e CROWDSEC_BOUNCER_API_KEY=... \
  -e MIKROTIK_HOST=192.168.88.1:8728 -e MIKROTIK_USER=crowdsec -e MIKROTIK_PASS=... \
  ghcr.io/jmrplens/cs-routeros-bouncer:latest

If you only want bans from your own machines and not the community blocklist, set crowdsec.origins: ["crowdsec", "cscli"].

Watching it

/metrics (Prometheus) and /health, plus a Grafana dashboard in the repository with the active decisions, what the rules dropped, and the router's own CPU, memory and temperature as read through the API. It also reports usage metrics back to CrowdSec, so the console shows what the router blocked.


The top of the bundled Grafana dashboard on my RB5009, last hour.

Where it has run, and what I'd like to hear

Only on my RB5009UG+S+, from RouterOS 7.21 to 7.24.4, with CrowdSec on a separate Linux host. Nothing in it is specific to that board, but I have no numbers from anything smaller. If you run it on a hAP ax², a hEX or a CHR, especially with the community blocklist on (15,000–25,000 entries), I'd like to know how long the first sync took and how the CPU looked. Bug reports and "this doesn't fit how I configure my firewall" are welcome too.

Perhaps I missed it, but can this be run in a container on the router?

Hello all ,,

jmrplens :

This is a great topic. However, back when these systems—especially industrial ones—were built, that extra processing power, time, and RAM weren't taken into account. Or at least, such a high level of communication channel security wasn't required. Few people anticipated the need for extra capacity "just in case." Still, I’m glad there’s a solution from to handle the "fail-to-ban" aspect. Everyone has to make their own call based on their hardware :slight_smile: For me, the best part is the visualization. A comprehensive, clear graphical representation of processes is a priority for me when deciding on hardware. I don't like working via CLI; it limits my overall view of the processes. "Donkeys" are a peculiar breed ...

@tadawson

Not covered in the docs because I hadn't tried it, but nothing in the bouncer prevents it. The image on ghcr.io is built for arm64, arm/v7 and amd64, and a normal setup is configured entirely with environment variables, so an envlist is enough.

It still talks to RouterOS through the API, now over the container's veth, so the input chain has to accept that, and it still needs a CrowdSec LAPI somewhere. CrowdSec itself is the heavy part; I wouldn't run that on the router. The router's own work, writing and reading the address lists, is the same wherever the bouncer runs. What moves onto the router is the bouncer process: about 30 MB of RAM with 2,600 decisions on my x86 host, up to ~56 MB with the 17k community list. I don't have ARM CPU figures yet.

MIPS boards have no container package, and the hEX refresh only runs arm32v5 images, which I don't build. It also needs device-mode container=yes, so I'd only do it on a router that already runs containers. Where it makes most sense is CrowdSec on a VPS with no always-on machine at home.

@valsirob

Thanks! You're right that a lot of hardware out there was never sized for this, which is why the bouncer keeps the router's share small: CrowdSec reads the logs and decides on another machine, and the router only holds an address list and drops it in raw, before connection tracking. A ban is one API call, a millisecond or two on the RB5009; the only heavy moment is loading a big list from empty, and even the 17k community list took about 14 s at ~31% CPU there. If a router can't spare that, leaving the community blocklist out (crowdsec.origins: ["crowdsec", "cscli"]) keeps the list to your own bans, a few thousand entries on mine.

And no CLI is needed to see what it does. In WinBox, IP > Firewall > Address Lists shows every banned address with the scenario that banned it and how long it has left, and the bouncer's rules show their packet counters. With Prometheus there's also the Grafana dashboard from the first post (decisions, dropped traffic, the router's CPU, memory and temperature), and the CrowdSec console gets the bouncer's usage metrics as well.

Hello all ,,,

jmrplens :

Can this tool be started later with manual settings, so as to minimize the load on the processor and RAM? Let me approach it a little differently ... Prioritization helps with the lack of resources! Therefore, can we Manage -> we can do everything!
and to start only with its individual parts, in order to regulate the available power or to do this automatically according to the resource? For example, to track only the login channel at start and not to bother with the unnecessary for the start itself. Such automation smells like barbecue..

if we want security, then security starts first .. then, the services that make us exist start ..

Resource allocation is a policy and not an exception :slight_smile:

My strategy burning started with floppy disks and "CoyĐľteRouter"( Pentium / Pentium1 ), that's why the results are like this :slight_smile:

exception --/-> there are still working devices.. Coyote & ClarkConnect Routers !

Regards // Welcome to Bulgaria :heart_eyes:

@valsirob

Most of that is already possible, it just lives in two places.

The bouncer's share of the router is small to begin with. CrowdSec and the bouncer run on another machine; the router only holds the address list and the drop rules. A ban or an unban is one API call, and a periodic pass reads the list and fixes the difference: about 0.25 s here with 2,500 entries, 1.9 s with 20,000 on the RB5009. The one heavy moment is the first load of a big list from empty: the 17k community blocklist took about 14 s at ~31% CPU.

What it applies is up to you, so you can start with the part that matters most and add the rest later:

crowdsec:
  origins: ["crowdsec", "cscli"]   # your own bans, without the community blocklist
  scenarios_containing: ["ssh"]    # only bans from scenarios with "ssh" in their name
  reconciliation_interval: 1h      # fewer periodic passes (0 turns them off)
mikrotik:
  pool_size: 2                     # fewer parallel API sessions

scenarios_containing is the closest to "only the login channel": with ["ssh"] it applies only the SSH brute-force bans and ignores everything else. Widen it later, restart the bouncer, and its first pass adds what was left out.

"Security first" at boot belongs to RouterOS itself, though. Restricting who may reach the router (the allowed addresses in /ip service, input rules for the management ports) is static configuration, there from the first second, before any bouncer. The bouncer's own rules are configuration too and survive a reboot; their address list is dynamic, so it comes back empty, and since 1.8.0 the bouncer notices the reboot within a minute and fills it again.

You also gave me a good idea: letting the bouncer slow down by itself when the router's CPU is already busy. It reads the router's CPU anyway for its metrics, so during a big load it can space out its adds and wait while the CPU is above a threshold. I'll add that in the next version.

And respect for Coyote Linux on floppies. That's where a lot of us learned how little a router needs, as long as nothing wastes it.

@valsirob

The idea from your last post is in 1.9.0, out today: the bouncer now slows itself down when the router's CPU is already busy.

It's one setting, on by default:

mikrotik:
  cpu_limit: 80   # % of all cores; 0 turns it off

While it loads or reconciles a list, it reads RouterOS' own cpu-load, the average of all cores over the last second, at most once a second. After two busy readings in a row it starts pausing before each block of work, 100 entries at a time. The pause starts at 200 ms, doubles up to 5 s while the load stays high, and halves back to full speed once the load drops 10 points under the limit. A single busy second, like the router's own periodic jobs, slows nothing down. A periodic check also waits up to a minute for the load to drop before it starts. Even at the slowest pace it adds about 20 entries a second, so a load always finishes.

Bans and unbans are never slowed down. In 1.9.0 a ban no longer waits for a running sync either: it reaches the router as it arrives, even in the middle of a slowed-down load.

To test it I held a virtual router at 50% CPU and loaded 30,000 entries with cpu_limit at 55:

  • Without the limit (1.8.0), the router sat at or above 55% for 73–88% of the load. With it, 3–4% of the load, all in the first couple of seconds before the slowdown kicks in.
  • The load took a minute or so instead of 6–12 s.
  • A ban that arrived halfway through still landed in the same second.

On my RB5009 the default 80 never triggers: a 20,000-entry load runs at about 30% and takes as long as before. Forcing cpu_limit to 20 there, the same load took 48 s at 14%.

When it slows down it says so in the log. Two Prometheus metrics show it as well: crowdsec_bouncer_routeros_throttled (1 while slowing down) and crowdsec_bouncer_routeros_throttle_seconds_total. Details: https://jmrp.io/docs/cs-routeros-bouncer/configuration/mikrotik/#router-cpu

Thanks for the push. It made the bouncer better on exactly the kind of hardware you were talking about.

@tadawson, an update to my earlier answer: since 1.8.0 there is an arm/v5 image too, so the hEX refresh and the hEX S (2025) can run it. Thanks to sqonde on GitHub, who contributed it. The docs now also have a page on running the bouncer as a container on the router it protects: https://jmrp.io/docs/cs-routeros-bouncer/getting-started/routeros-container/