A CrowdSec remediation component (bouncer) for MikroTik RouterOS that automatically manages firewall rules and address lists via the RouterOS API. Listed on the CrowdSec Hub.
📖 Full documentation — available in English and Spanish.
This README covers installation and the settings most deployments need. The documentation site carries the rest, and carries it more reliably: its configuration reference is generated from internal/config/config.go and regenerated in CI, which fails on any diff. The firewall rule listing is not generated but is single-sourced: one module, read field by field out of internal/manager, rendered on every page that shows the rules — so the three copies that used to disagree are now one. A hand-written copy here would be a fourth, and the last one drifted.
| If you want to | Go to |
|---|---|
| Every setting, its default and env var | Configuration reference |
| Worked config examples | Examples |
| Exactly what gets written to the router | Firewall rules |
| How reconciliation works and what it costs | Reconciliation |
| Metrics, health checks, Grafana | Monitoring |
| Tuning for large CAPI lists | Performance tuning |
| Measured numbers on real hardware | Benchmarking |
For machine consumption: llms.txt and llms-full.txt.
/metrics), structured logging, health endpoint (/health), LAPI usage metrics (active decisions, dropped traffic)Existing MikroTik bouncers have significant limitations that this project addresses:
| Feature | funkolab (archived) | nvtkaszpir-alt | cs-routeros-bouncer |
|---|---|---|---|
| Auto-create firewall rules | ❌ | ❌ | ✅ |
| Individual IP add/remove | ✅ | ❌ (bulk re-upload) | ✅ |
| No duplicate IPs | ✅ | ❌ | ✅ |
| State reconciliation on restart | ❌ | ❌ | ✅ |
| Remove rules on shutdown | ❌ | ❌ | ✅ |
| IPv6 support | ✅ | ✅ | ✅ |
| Output blocking | ❌ | ✅ | ✅ |
| Origin filtering (local-only mode) | ❌ | ❌ | ✅ |
| Prometheus metrics | ❌ | ✅ | ✅ |
| LAPI usage metrics (dropped traffic) | ❌ | ❌ | ✅ |
| Health endpoint | ❌ | ❌ | ✅ |
| Go (compiled, low resource usage) | ✅ | ✅ | ✅ |
This bouncer exists because of a specific setup, not as an exercise. It is the enforcement end of a pipeline that starts with a MikroTik router deliberately exposing closed ports to catch scanners, and with an nginx tier that slows down the ones that get through:
Both run on the author's own network against real traffic; the decisions in this bouncer (state reconciliation on restart, individual IP add/remove, origin filtering) come from that.
1sudo cscli bouncers add cs-routeros-bouncer
Save the API key shown in the output.
Connect to your MikroTik router and create a dedicated user:
1/user group add name=crowdsec policy=read,write,api,sensitive,!ftp,!local,!ssh,!reboot,!policy,!test,!password,!sniff,!romon,!rest-api2/user add name=crowdsec group=crowdsec password=YOUR_SECURE_PASSWORD
Choose your preferred installation method below.
1services:2 cs-routeros-bouncer:3 image: ghcr.io/jmrplens/cs-routeros-bouncer:latest4 container_name: cs-routeros-bouncer5 restart: unless-stopped6 ports:7 - "2112:2112" # Prometheus metrics (optional)8 environment:9 CROWDSEC_URL: "http://crowdsec:8080/"10 CROWDSEC_BOUNCER_API_KEY: "your-bouncer-api-key"11 MIKROTIK_HOST: "192.168.0.1:8728"12 MIKROTIK_USER: "crowdsec"13 MIKROTIK_PASS: "your-password"14 # Optional: mount a config file; this path is loaded automatically when present.15 # volumes:16 # - ./config.yaml:/etc/cs-routeros-bouncer/config.yaml
1docker compose up -d
Download the latest release from the Releases page:
Automatic setup (recommended):
1# Resolve the latest version (release assets embed it in the filename)2VERSION=$(curl -fsSL https://api.github.com/repos/jmrplens/cs-routeros-bouncer/releases/latest | sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p' | head -n1)3[ -n "$VERSION" ] || { echo "could not resolve the latest version; pick a tag from the Releases page" >&2; exit 1; }45# Download (replace with your architecture, e.g. x86_64, i386, arm64, armv6, armv7)6ARCH=x86_647wget "https://github.com/jmrplens/cs-routeros-bouncer/releases/download/v${VERSION}/cs-routeros-bouncer_${VERSION}_linux_${ARCH}.tar.gz"8tar xzf "cs-routeros-bouncer_${VERSION}_linux_${ARCH}.tar.gz"910# Automated install: copies binary, creates config, installs and starts systemd service11sudo ./cs-routeros-bouncer setup1213# Edit configuration with your CrowdSec API key and MikroTik credentials14sudo nano /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml1516# Restart after editing config17sudo systemctl restart cs-routeros-bouncer
The setup subcommand accepts optional flags:
| Flag | Default | Description |
|---|---|---|
-bin | /usr/local/bin/cs-routeros-bouncer | Installation path for the binary |
-config-dir | /etc/cs-routeros-bouncer | Directory for configuration files |
To uninstall:
1sudo cs-routeros-bouncer uninstall # Keeps config files2sudo cs-routeros-bouncer uninstall -purge # Also removes config
If setup used custom paths, pass the same values to uninstall:
1sudo cs-routeros-bouncer uninstall \2 -bin /opt/cs-routeros-bouncer/cs-routeros-bouncer \3 -config-dir /opt/cs-routeros-bouncer/config \4 -purge
1# Download (e.g. x86_64, i386, arm64, armv6, armv7 — see the release assets for the full list)2VERSION=$(curl -fsSL https://api.github.com/repos/jmrplens/cs-routeros-bouncer/releases/latest | sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p' | head -n1)3[ -n "$VERSION" ] || { echo "could not resolve the latest version; pick a tag from the Releases page" >&2; exit 1; }4ARCH=x86_645wget "https://github.com/jmrplens/cs-routeros-bouncer/releases/download/v${VERSION}/cs-routeros-bouncer_${VERSION}_linux_${ARCH}.tar.gz"6tar xzf "cs-routeros-bouncer_${VERSION}_linux_${ARCH}.tar.gz"78# Install (the archive ships the sample config under config/)9sudo install -m 755 cs-routeros-bouncer /usr/local/bin/10sudo mkdir -p /etc/cs-routeros-bouncer11sudo cp config/cs-routeros-bouncer.yaml /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml1213# Edit configuration14sudo nano /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml1516# Install systemd service17sudo tee /etc/systemd/system/cs-routeros-bouncer.service > /dev/null << 'EOF'18[Unit]19Description=CrowdSec RouterOS Bouncer20After=network-online.target crowdsec.service21Wants=network-online.target2223[Service]24Type=simple25ExecStart=/usr/local/bin/cs-routeros-bouncer -c /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml26Restart=on-failure27RestartSec=1028TimeoutStopSec=902930[Install]31WantedBy=multi-user.target32EOF3334sudo systemctl daemon-reload35sudo systemctl enable --now cs-routeros-bouncer
1git clone https://github.com/jmrplens/cs-routeros-bouncer.git2cd cs-routeros-bouncer3make build45# Option 1: Automated install6sudo bin/cs-routeros-bouncer setup78# Option 2: Manual install9sudo install -m 755 bin/cs-routeros-bouncer /usr/local/bin/
Settings come from a YAML file, environment variables, or both — environment variables win. The annotated sample ships with every release as config/cs-routeros-bouncer.yaml.
Everything below is either required or worth being explicit about. Nothing else has to be set.
1crowdsec:2 api_url: "http://localhost:8080/"3 api_key: "your-bouncer-api-key" # from `cscli bouncers add`45mikrotik:6 address: "192.168.0.1:8728" # 8729 with tls: true7 username: "crowdsec"8 password: "your-password"910firewall:11 ipv4:12 enabled: true13 ipv6:14 enabled: true1516logging:17 level: "info"
| Config key | Env variable | Default | Description |
|---|---|---|---|
crowdsec.api_url | CROWDSEC_URL | http://localhost:8080/ | CrowdSec LAPI URL |
crowdsec.api_key | CROWDSEC_BOUNCER_API_KEY | (required) | Bouncer API key |
crowdsec.origins | CROWDSEC_ORIGINS | (all) | Restrict to e.g. ["crowdsec","cscli"] for local-only |
crowdsec.reconciliation_interval | CROWDSEC_RECONCILIATION_INTERVAL | 15m | Periodic drift repair; 0 disables |
mikrotik.address | MIKROTIK_HOST | (required) | RouterOS API address (host:port) |
mikrotik.username | MIKROTIK_USER | (required) | API username |
mikrotik.password | MIKROTIK_PASS | (required) | API password |
mikrotik.tls | MIKROTIK_TLS | false | Use TLS (port 8729) |
mikrotik.pool_size | MIKROTIK_POOL_SIZE | 4 | Parallel API sessions |
firewall.ipv4.enabled | FIREWALL_IPV4_ENABLED | true | Enable IPv4 blocking |
firewall.ipv6.enabled | FIREWALL_IPV6_ENABLED | true | Enable IPv6 blocking |
firewall.deny_action | FIREWALL_DENY_ACTION | drop | drop or reject |
metrics.enabled | METRICS_ENABLED | false | Serve /metrics and /health |
logging.level | LOG_LEVEL | info | debug, info, warn, error |
That is 14 of 94 keys. The other 80 — decision filtering, TLS material, per-protocol rule placement, chain selection, logging to file, pprof — are in the generated configuration reference, which is built from the Go struct and fails CI when it disagrees with it.
On startup it connects to CrowdSec and RouterOS, creates its firewall rules, fetches every active decision and reconciles them against what the router already holds.
At runtime it streams new decisions: a ban adds an address-list entry, an unban removes one. Every reconciliation_interval it re-reads the list and repairs any drift.
On shutdown (SIGTERM/SIGINT) it removes the firewall rules it created. Address-list entries are left to expire via their MikroTik timeout, so a restart does not leave your router unprotected.
A stock configuration writes eight firewall rules — a deny rule and a passthrough counting rule, in both the filter and raw tables, for both IPv4 and IPv6. The counting rules come from metrics.track_processed, which defaults to true; set it to false and you get four. Each rule, its exact RouterOS command and its placement is listed on the firewall rules page, which renders from the same module the daemon reads.
For what reconciliation costs on real hardware, and how to size the interval for your list, see reconciliation and performance tuning.
Set metrics.enabled: true and the bouncer serves two endpoints on metrics.listen_port (default 2112):
1curl http://localhost:2112/health2# {"routeros_connected":true,"status":"ok","version":"X.Y.Z"}34curl http://localhost:2112/metrics
Metrics cover decisions processed, active decisions by origin, operation latencies, RouterOS connection state and router CPU/memory, plus CrowdSec LAPI usage metrics (including traffic dropped by the bouncer's own rules, which is what makes it show up in cscli metrics).
Neither endpoint is authenticated, and metrics.listen_addr defaults to 0.0.0.0 — so enabling metrics exposes them on every interface the host has. Bind them to 127.0.0.1, or firewall the port, unless the scrape genuinely comes from elsewhere. The same applies with more force to metrics.pprof_enabled: a heap profile carries fragments of whatever the process has held, including the CrowdSec API key and the RouterOS password.
The full metric list, a ready-made Grafana dashboard and alerting examples are in Monitoring.
/ip/service/print — api should be enabled on your routerapi policymikrotik.tls: true and the correct port (8729)top or position, the bouncer iterates through lower positions until it finds one where the managed block can be placed/ip/firewall/filter/print on the routercomment_match; matching is case-sensitivejournalctl -u cs-routeros-bouncer -f | grep -i placementdocker logs cs-routeros-bouncer | grep -i placementjournalctl -u cs-routeros-bouncer, or the process stderr if you run it by handfirewall.rule_placement: "top" is set, or use structured placement with strategy: "position", before_comment, or after_comment. Also check any YAML-only firewall.ipv4.rule_placement or firewall.ipv6.rule_placement override.sudo cscli decisions listlogging.level: debug for detailed decision processing logscrowdsec.origins, ensure it includes the expected sourcescrowdsec.origins: ["crowdsec", "cscli"] to sync only local decisionscrowdsec.reconciliation_interval (default 15m) and is usually light when there is no drift, while cached duplicates skip RouterOS entirelyalready have such entry or reconnect messages, and verify you are running a version where RouterOS device errors do not trigger reconnects.More failure modes, each with the check that identifies it, are on the troubleshooting page.
See CONTRIBUTING.md for development setup and guidelines.
1make build # Build binary2make test # Run tests3make lint # Run linter4make analyze # Every check CI runs, reporting all failures rather than the first5make docker-build # Build Docker image
A Bash suite exercises the compiled binary against a real MikroTik router over SSH, cscli, systemctl and SNMP — no Go internals imported. Nine groups, from data integrity to a ~28k-IP CAPI stress test:
1cp tests/functional/.env.example tests/functional/.env # fill in router + LAPI credentials2tests/functional/run_tests.sh --list # see the groups3tests/functional/run_tests.sh # all but the CAPI stress test4tests/functional/run_tests.sh --capi # include it
There is also a developer benchmark (go run ./cmd/benchmark) and self-installing router instrumentation that samples CPU and RAM at 10 Hz (go run ./cmd/perfmon install). Both, and the method for measuring against a live router without fooling yourself, are covered in Benchmarking.
See SECURITY.md for the security policy and responsible disclosure process.
internal/rosapi is vendored from (MIT), pruned to the synchronous subset this bouncer uses