9.3 KiB
Fail2banActionBanishment
Distributed malicious-host banishing system built on fail2ban and iptables, with event propagation over MQTT.
Each node detects and bans locally (fail2ban + iptables), publishes the event to a local Mosquitto broker, then relays it to a private master broker. The master centralizes the decision (multi-node correlation, global recidive detection, shared reputation) and republishes the actions to be executed to all subscribed nodes.
Target machine: Debian 13 VPS.
Full documentation (installation, detailed production deployment,
token-based node enrollment, MQTT topics and commands reference):
make docs-serve then http://127.0.0.1:8001/, or make docs to
generate the static site into site/. See also README.md
for the French version.
Architecture (overview)
[node] fail2ban/iptables → local mosquitto (1883) → master mosquitto (8883, TLS)
│
decision / correlation
│
[node] ◄──────────────── MQTT action (BAN, SYNC_BAN, UNBAN, ...) ◄──────────
Use cases
The master/nodes model doesn't assume any ownership relationship between the protected servers — only that they share a common decision authority. That makes the architecture relevant beyond a single operator running their own VPSes:
- A set of sites (several VPSes run by the same operator, each hosting
one or more services): an IP attacking one site gets banned everywhere
via
SYNC_BAN, without manual per-site monitoring. - A community (several independent administrators, each responsible for their own server, who agree to pool their detections): every member keeps their own node and their own local fail2ban — only ban propagation is shared through the common master. No member gets SSH access to another member's server.
- An IT services company managing several clients' sites: a centralized master gives an aggregated view (jail/country counters, per-node history, per-client aliases) without mixing client data — each node only sees its own events, the master sees everything.
In all three cases, a new server joins the set without ever handing out
SSH access to the others: a single-use token-based enrollment is enough
(make join-token / make join, detailed below).
Repository layout
generic/fail2ban/— jails, filters and the MQTT action (f2b-mqtt-action-banisher)generic/firewall/— iptables launcher script (firewall-launcher.sh), per-service rulesgeneric/mosquitto/— local MQTT broker (127.0.0.1:1883, no persistence) and master broker (mosquitto-master.conf, port 8883, mTLS)generic/emitter/— systemd service templates (web/ASGI, MQTT ingestion, Celery worker, master relay)generic/supervisor/—.example.conftemplates (optional alternative to systemd,make install-supervisor)generic/nginx/—.example.confTLS reverse-proxy template for the dashboard (not deployed automatically)emitter/— Django emitter (baneventsapp): MQTT ingestion (manage.py mqtt_listen), master relay (manage.py master_client),BanEventmodel, dashboard + map + history, geolocation (Celery).env.example— configuration template (copied to.envbyinstall.sh, never committed)install.sh— idempotent deployment (packages, config files, systemd services, including the emitter); wrapped by themaketargets belowscripts/test-fail2ban-filters.sh— custom filter checks viafail2ban-regexscripts/master-ca-init.sh,scripts/generate-node-cert.sh— private CA and mTLS client certs for the master broker (manual flow, see token-based enrollment below for the recommended flow)Makefile— every entry point (make helpfor the full list)docs/— mkdocs documentation (installation, production deployment, reference)
Installation (Debian 13 VPS)
Deployment model: no copy into /opt. The repo cloned into a dedicated
user's $HOME (e.g. banisher) is the deployment — venv, SQLite
database, staticfiles/ and .env all live inside that checkout, owned by
that user. install.sh detects this user from the repo's actual owner (no
hardcoded name):
useradd -m -s /bin/bash banisher # once, if the account doesn't exist yet
su - banisher -c 'git clone <repo-url> ~/Fail2banMqttActionBanisher'
cd ~banisher/Fail2banMqttActionBanisher
sudo make install
sudo make status
make install (= sudo ./install.sh install):
- installs/checks a
NOPASSWD:ALLsudoers entry for that user (/etc/sudoers.d/<user>-full-access, created once if missing) — this account should therefore be dedicated to this project, not shared with other uses; - copies
.env.exampleto.envif missing (never regenerated afterward,DJANGO_SECRET_KEYgenerated randomly) — edit.envfor real credentials (MQTT password for theemitteraccount, master domain, ...); - creates the emitter's Python venv (
emitter/.venv), applies migrations andcollectstatic, all run as the deployment user (never root); - enables four systemd services:
fail2ban-emitter-web(ASGI/Daphne dashboard,127.0.0.1:8050),fail2ban-emitter-mqtt(manage.py mqtt_listen),fail2ban-emitter-worker(Celery worker, geolocation) andfail2ban-emitter-master-client(manage.py master_client, relay/execution of the master's commands — won't actually connect until a node certificate has been obtained, see below).
Also edit /etc/fail2ban/mqtt.conf (real MQTT credentials, copied from
generic/fail2ban/mqtt.conf.example if it doesn't exist yet).
To update the emitter after a git pull, just rerun sudo make install:
dependencies/migrations are reapplied and all four services restarted.
Additional deployment options (detailed in the deployment
documentation, make docs-serve for the
browsable version):
- master (
sudo make install-master) — public mTLS MQTT broker, private CA, multi-node ingestion; - token-based node enrollment (
make join-tokenon the master, thensudo make joinon the new node) — recommended way to attach a node to a master, without ever copying a certificate by hand; - nginx (
sudo make install-nginx) — TLS reverse proxy (Let's Encrypt) in front of the dashboard, for public access; - supervisor (
sudo make install-supervisor/sudo make uninstall-supervisor) — optional alternative to systemd; - MariaDB (
sudo make install-mariadb) — switches the database from SQLite (default) to a dedicated local MariaDB server.
Django emitter (development)
Requires a reachable Redis (Django Channels channel layer, e.g. docker run -p 6379:6379 redis).
make run # dashboard + map at http://127.0.0.1:8000/ (real-time WebSocket included)
make mqtt # in a second terminal: MQTT ingestion
make worker # in a third terminal: asynchronous geolocation
Environment variables (MQTT_BROKER_HOST, MQTT_BROKER_USERNAME,
MQTT_BROKER_PASSWORD, REDIS_HOST, REDIS_PORT, ...): copy
.env.example to .env at the repo root and edit it — loaded
automatically by Django (python-dotenv), nothing to export by hand.
Geolocation uses the free ip-api.com fallback API until a local GeoLite2
(MaxMind) database is configured.
Translations (fr/en)
French is the source language of the template code; English is a
translation maintained in emitter/locale/en/LC_MESSAGES/django.po. A
selector in the dashboard's sidebar switches the active language
(django_language cookie).
After adding/changing a {% trans %}/{% blocktrans %} in a template:
cd emitter
.venv/bin/python manage.py makemessages -l en --no-location --ignore ".venv"
# edit locale/en/LC_MESSAGES/django.po (fill in the missing msgstr entries)
.venv/bin/python manage.py compilemessages -l en --ignore ".venv"
--ignore ".venv": without it, compilemessages also recompiles (slowly,
with no useful effect) every one of Django's own bundled translations found
inside the venv, since it lives under emitter/ — harmless but pointless.
Master broker and node enrollment
See the deployment documentation for the full detail: master provisioning, the enrollment security model (single-use token, private key that never leaves the node, token automatically given back on a transient failure), troubleshooting. In short:
# on the master
make join-token NODE=my-node
# on the new node
sudo make join MASTER=https://master-dashboard NODE=my-node TOKEN=<token>
sudo make install
Master-side ingestion (manage.py master_listen) systematically
republishes a SYNC_BAN for every ingested ban, and correlates
independent bans across nodes to trigger an automatic escalation — see
the reference for the full list of master
commands and MQTT topics.
Development constraints
- Source code in English, comments in French
- Indentation: 4 spaces, never tabs, see
.editorconfig(exception: Makefile recipes, which must be tab-indented — a constraint of Make itself) - Python type-checked with
pyright(pyrightconfig.json), shell scripts withshellcheck—make lint, also run in CI - JavaScript: ES6 classes
License
AGPL-3.0 — see LICENSE.