mirror of
https://github.com/deunix-educ/Fail2banMqttActionBanishment.git
synced 2026-08-24 03:11:58 +02:00
First commit
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
# Production deployment
|
||||
|
||||
This page details deploying a master + nodes set in production, beyond
|
||||
the single isolated node covered in [Installation](installation.en.md).
|
||||
|
||||
## Overview
|
||||
|
||||
Three ways to use this project, from simplest to most complete — the
|
||||
same base install (`sudo make install`) is the foundation for all three:
|
||||
|
||||
- **Solo**: `sudo make install`, alone. This server detects and bans
|
||||
locally, without centralizing anything elsewhere. Enough to protect a
|
||||
single VPS. The relay service to a master
|
||||
(`fail2ban-emitter-master-client`) still runs, but silently keeps
|
||||
retrying in a loop as long as no master is configured (no harm done,
|
||||
`Restart=on-failure`).
|
||||
- **Solo + master**: `sudo make install` **then** `sudo make
|
||||
install-master`, on the **same** server. This VPS protects its own
|
||||
services (like solo mode) and also serves as a master, ready to take on
|
||||
other nodes later — useful for starting out alone today without
|
||||
closing the door on a second server tomorrow.
|
||||
- **Master + nodes** (several VPSes): one server in solo+master mode, and
|
||||
one or more other servers, each installed solo and then attached to
|
||||
that master via token-based enrollment (below). The master itself
|
||||
stays a node like any other (it detects and bans locally in addition
|
||||
to centralizing the decision) — going from "solo+master on its own" to
|
||||
"master + nodes" therefore requires no reinstall of the master, just
|
||||
adding nodes to it over time.
|
||||
|
||||
Recommended order for moving to several nodes:
|
||||
|
||||
1. Deploy the master (once — solo+master if this same server should also
|
||||
protect itself, which is the common case).
|
||||
2. On the master, issue a token for each new node.
|
||||
3. On each new node, enroll using that token.
|
||||
|
||||
## Deploying the master
|
||||
|
||||
```sh
|
||||
sudo make install-master
|
||||
```
|
||||
|
||||
Requires a first `sudo make install` already done on this same machine.
|
||||
Provisions:
|
||||
|
||||
- a dedicated **private CA** (`scripts/master-ca-init.sh`), distinct from
|
||||
the Let's Encrypt certificate that serves the broker's TLS identity —
|
||||
it only signs nodes' client certificates (mTLS authentication);
|
||||
- the public Mosquitto listener (port 8883, TLS, `MQTT_MASTER_DOMAIN` in
|
||||
`.env`) with per-node ACLs;
|
||||
- master-side ingestion (`fail2ban-emitter-master-listen`, runs with its
|
||||
own mTLS identity `master-internal`, generated automatically).
|
||||
|
||||
`MQTT_MASTER_DOMAIN` must point to a real public domain (DNS already set
|
||||
up) — the broker obtains its Let's Encrypt certificate for that name.
|
||||
|
||||
## Token-based node enrollment (recommended)
|
||||
|
||||
### The problem
|
||||
|
||||
Adding a node by hand would require generating a key + certificate on the
|
||||
master, then copying those files to the new node (`scp`), renaming them
|
||||
correctly, and editing its `.env` — several manual, cross-machine steps,
|
||||
and the private key would travel over the network (never a good
|
||||
practice).
|
||||
|
||||
### The model
|
||||
|
||||
The master issues a **single-use token**, short-lived (1h by default).
|
||||
The new node uses that token to enroll itself:
|
||||
|
||||
- The node generates its **own private key locally** and never sends it
|
||||
— only a CSR (signing request) goes to the master.
|
||||
- The token is never stored in plaintext on the master: only its SHA-256
|
||||
hash is persisted (same logic as a password hash).
|
||||
- The token is **atomically marked used** as soon as it's validated, and
|
||||
**automatically given back** if a later step fails (inconsistent CSR,
|
||||
signing failure) — a transient hiccup doesn't force issuing an
|
||||
entirely new token.
|
||||
- The CN of the received CSR must match the node name declared when the
|
||||
token was issued — a stolen token can't be used to claim a different
|
||||
name.
|
||||
- Any error returned to the client is **deliberately generic**
|
||||
("invalid or expired token"), regardless of the actual cause — the
|
||||
detail (unknown, expired, or already-used token, inconsistent CN,
|
||||
signing failure) only goes into the master's logs, so as to give no
|
||||
hint to a third party probing the endpoint.
|
||||
- The transport is the master's HTTPS dashboard (already publicly
|
||||
exposed, nginx + Let's Encrypt): no new port or service to open, and it
|
||||
solves the chicken-and-egg problem (a node can't use mTLS MQTT to
|
||||
obtain... its first mTLS certificate).
|
||||
|
||||
### Step by step
|
||||
|
||||
On the **master**:
|
||||
|
||||
```sh
|
||||
make join-token NODE=node-name
|
||||
```
|
||||
|
||||
Prints the plaintext token **only once**, along with the full command to
|
||||
run on the new node (lost token: simply issue a new one, the previous
|
||||
one stays valid independently until it expires).
|
||||
|
||||
On the **new node** (having already gone through `sudo make install`):
|
||||
|
||||
```sh
|
||||
sudo make join MASTER=https://<master-domain> NODE=node-name TOKEN=<token>
|
||||
sudo make install
|
||||
```
|
||||
|
||||
The second `make install` applies the received configuration
|
||||
(`MQTT_MASTER_NODE_NAME` in `.env`, certificate in place) and restarts
|
||||
the services.
|
||||
|
||||
### What happens behind the scenes
|
||||
|
||||
```
|
||||
[new node] [master]
|
||||
generates key + CSR (local, never sent)
|
||||
POST /api/join/ {node_name, token, csr} ──►
|
||||
validates the token (hash, expiry, single use)
|
||||
checks the CSR's CN == node_name
|
||||
signs the CSR with the private CA
|
||||
adds the ACL entitlement for this node
|
||||
restarts mosquitto
|
||||
{cert, ca_cert} ◄────
|
||||
writes cert/ca_cert, updates .env
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **"Invalid or expired token"** — the message is deliberately generic.
|
||||
Possible causes: token already used (run `make join-token` again to
|
||||
issue a new one), token expired (1h default, adjustable via
|
||||
`make join-token NODE=... TTL=<minutes>`), or a typo in the node name
|
||||
(must be identical between token issuance and `make join`).
|
||||
- The new node must be able to reach the master over HTTPS (443) — a
|
||||
firewall blocking outbound traffic would prevent enrollment.
|
||||
|
||||
## Databases
|
||||
|
||||
SQLite is the default, nothing to configure. Two alternatives:
|
||||
|
||||
```sh
|
||||
sudo make install-mariadb # provisions a local MariaDB server + dedicated database/user
|
||||
sudo make install # re-run afterward to apply migrations on the new database
|
||||
```
|
||||
|
||||
PostgreSQL is supported at the configuration level (`DB_ENGINE=postgresql`
|
||||
in `.env`, driver `emitter/requirements-postgresql.txt`) but without an
|
||||
automated provisioning subcommand for now — server and database must be
|
||||
created manually before switching `DB_ENGINE`.
|
||||
|
||||
Migrating data from one backend to another (engine-agnostic dump via
|
||||
`manage.py dumpdata`/`loaddata`):
|
||||
|
||||
```sh
|
||||
make dump-db # writes emitter/db-dump-<date>.json (git-ignored, contains real data)
|
||||
# ... switch DB_ENGINE, migrate ...
|
||||
emitter/.venv/bin/python emitter/manage.py loaddata <file>.json
|
||||
```
|
||||
|
||||
## nginx reverse proxy + public TLS
|
||||
|
||||
```sh
|
||||
sudo make install-nginx # reads DASHBOARD_DOMAIN from .env
|
||||
```
|
||||
|
||||
Obtains a Let's Encrypt certificate and deploys an nginx vhost in front of
|
||||
the dashboard (WebSocket included) — needed for public access; without
|
||||
it, the dashboard is only reachable locally/over VPN.
|
||||
|
||||
## Alternative to systemd: supervisor
|
||||
|
||||
```sh
|
||||
sudo make install-supervisor # switches the emitter's 4 services to supervisor
|
||||
sudo make uninstall-supervisor # rollback (re-enables systemd)
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
```sh
|
||||
git pull
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Reapplies dependencies/migrations and restarts the affected services, on
|
||||
any node (including the master).
|
||||
@@ -0,0 +1,193 @@
|
||||
# Déploiement en production
|
||||
|
||||
Cette page détaille le déploiement d'un ensemble master + noeuds en
|
||||
production, au-delà du premier noeud isolé couvert par
|
||||
[Installation](installation.md).
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Trois façons d'utiliser ce projet, du plus simple au plus complet — la
|
||||
même installation de base (`sudo make install`) sert de socle aux trois :
|
||||
|
||||
- **Solo** : `sudo make install`, seul. Ce serveur détecte et bannit
|
||||
localement, sans rien centraliser ailleurs. Suffisant pour protéger un
|
||||
unique VPS. Le service de relais vers un master
|
||||
(`fail2ban-emitter-master-client`) tourne quand même, mais réessaie
|
||||
silencieusement en boucle tant qu'aucun master n'est configuré (aucune
|
||||
gêne, `Restart=on-failure`).
|
||||
- **Solo + master** : `sudo make install` **puis** `sudo make
|
||||
install-master`, sur le **même** serveur. Ce VPS protège ses propres
|
||||
services (comme en mode solo) et sert en plus de master, prêt à
|
||||
accueillir d'autres noeuds plus tard — utile pour démarrer seul
|
||||
aujourd'hui sans fermer la porte à un second serveur demain.
|
||||
- **Master + noeuds** (plusieurs VPS) : un serveur en mode solo+master,
|
||||
et un ou plusieurs autres serveurs, chacun installé en solo puis
|
||||
rattaché à ce master via l'auto-inscription par jeton (ci-dessous). Le
|
||||
master lui-même reste un noeud comme les autres (il détecte et bannit
|
||||
localement en plus de centraliser la décision) — passer de "solo+master
|
||||
tout seul" à "master + noeuds" ne demande donc aucune réinstallation du
|
||||
master, seulement d'y ajouter des noeuds au fur et à mesure.
|
||||
|
||||
Ordre recommandé pour passer à plusieurs noeuds :
|
||||
|
||||
1. Déployer le master (une seule fois — solo+master si ce même serveur
|
||||
doit aussi se protéger lui-même, ce qui est le cas courant).
|
||||
2. Sur le master, émettre un jeton pour chaque nouveau noeud.
|
||||
3. Sur chaque nouveau noeud, s'auto-inscrire avec ce jeton.
|
||||
|
||||
## Déployer le master
|
||||
|
||||
```sh
|
||||
sudo make install-master
|
||||
```
|
||||
|
||||
Nécessite un premier `sudo make install` déjà effectué sur cette même
|
||||
machine. Provisionne :
|
||||
|
||||
- une **CA privée** dédiée (`scripts/master-ca-init.sh`), distincte du
|
||||
certificat Let's Encrypt qui sert l'identité TLS du broker — elle ne
|
||||
sert qu'à signer les certificats clients des noeuds (authentification
|
||||
mTLS) ;
|
||||
- le listener Mosquitto public (port 8883, TLS, `MQTT_MASTER_DOMAIN` dans
|
||||
`.env`) avec ACL par noeud ;
|
||||
- l'ingestion côté master (`fail2ban-emitter-master-listen`, tourne avec
|
||||
sa propre identité mTLS `master-internal`, générée automatiquement).
|
||||
|
||||
`MQTT_MASTER_DOMAIN` doit pointer vers un domaine public réel (DNS déjà en
|
||||
place) — le broker obtient son certificat Let's Encrypt à ce nom.
|
||||
|
||||
## Auto-inscription d'un noeud (recommandé)
|
||||
|
||||
### Le problème
|
||||
|
||||
Ajouter un noeud à la main demanderait de générer une clé + un certificat
|
||||
sur le master, puis de copier ces fichiers vers le nouveau noeud (`scp`),
|
||||
de les renommer correctement, et d'éditer son `.env` — plusieurs étapes
|
||||
manuelles, cross-machine, et la clé privée transiterait par le réseau
|
||||
(jamais une bonne pratique).
|
||||
|
||||
### Le modèle
|
||||
|
||||
Le master émet un **jeton à usage unique**, à courte durée de vie (1h par
|
||||
défaut). Le nouveau noeud utilise ce jeton pour s'inscrire lui-même :
|
||||
|
||||
- Le noeud génère sa **propre clé privée localement** et ne l'envoie
|
||||
jamais — seule une CSR (demande de signature) part vers le master.
|
||||
- Le jeton n'est jamais stocké en clair côté master : seul son hash
|
||||
SHA-256 est persisté (même logique qu'un hachage de mot de passe).
|
||||
- Le jeton est **marqué utilisé de façon atomique** dès sa validation, et
|
||||
**rendu automatiquement** si une étape suivante échoue (CSR
|
||||
incohérente, échec de signature) — un accroc transitoire ne force pas
|
||||
à réémettre un jeton entièrement nouveau.
|
||||
- Le CN de la CSR reçue doit correspondre au nom de noeud déclaré au
|
||||
moment de l'émission du jeton — un jeton volé ne permet pas de réclamer
|
||||
un autre nom.
|
||||
- Toute erreur renvoyée au client est **volontairement générique**
|
||||
("jeton invalide ou expiré"), quelle que soit la cause réelle — le
|
||||
détail (jeton inconnu, expiré, déjà utilisé, CN incohérent, échec de
|
||||
signature) part uniquement dans les journaux du master, pour ne donner
|
||||
aucun indice à un tiers qui sonderait l'endpoint.
|
||||
- Le transport est le tableau de bord HTTPS du master (déjà exposé
|
||||
publiquement, nginx + Let's Encrypt) : aucun nouveau port ni service à
|
||||
ouvrir, et ça résout le problème d'oeuf-et-poule (un noeud ne peut pas
|
||||
utiliser mTLS MQTT pour obtenir... son premier certificat mTLS).
|
||||
|
||||
### Étape par étape
|
||||
|
||||
Sur le **master** :
|
||||
|
||||
```sh
|
||||
make join-token NODE=nom-du-noeud
|
||||
```
|
||||
|
||||
Affiche le jeton en clair **une seule fois**, avec la commande complète à
|
||||
lancer sur le nouveau noeud (jeton perdu : simplement en émettre un
|
||||
nouveau, celui-ci reste valide indépendamment jusqu'à expiration).
|
||||
|
||||
Sur le **nouveau noeud** (déjà passé par `sudo make install` au
|
||||
préalable) :
|
||||
|
||||
```sh
|
||||
sudo make join MASTER=https://<domaine-du-master> NODE=nom-du-noeud TOKEN=<jeton>
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Le second `make install` applique la configuration reçue
|
||||
(`MQTT_MASTER_NODE_NAME` dans `.env`, certificat en place) et redémarre
|
||||
les services.
|
||||
|
||||
### Ce qui se passe en coulisse
|
||||
|
||||
```
|
||||
[nouveau noeud] [master]
|
||||
génère clé + CSR (locale, jamais transmise)
|
||||
POST /api/join/ {node_name, token, csr} ──►
|
||||
vérifie le jeton (hash, expiration, usage unique)
|
||||
vérifie le CN de la CSR == node_name
|
||||
signe la CSR via la CA privée
|
||||
ajoute l'entitlement ACL pour ce noeud
|
||||
redémarre mosquitto
|
||||
{cert, ca_cert} ◄────
|
||||
écrit cert/ca_cert, met à jour .env
|
||||
```
|
||||
|
||||
### Dépannage
|
||||
|
||||
- **"Jeton invalide ou expiré"** — le message est volontairement
|
||||
générique. Causes possibles : jeton déjà utilisé (relancer
|
||||
`make join-token` pour en émettre un nouveau), jeton expiré (durée par
|
||||
défaut 1h, ajustable via `make join-token NODE=... TTL=<minutes>`), ou
|
||||
faute de frappe dans le nom de noeud (doit être identique entre
|
||||
l'émission du jeton et `make join`).
|
||||
- Le nouveau noeud doit pouvoir joindre le master en HTTPS (443) — un
|
||||
pare-feu qui bloquerait la sortie empêcherait l'auto-inscription.
|
||||
|
||||
## Bases de données
|
||||
|
||||
SQLite est le défaut, sans rien à configurer. Deux alternatives :
|
||||
|
||||
```sh
|
||||
sudo make install-mariadb # provisionne un serveur MariaDB local + base/utilisateur dédiés
|
||||
sudo make install # à relancer ensuite pour appliquer les migrations sur la nouvelle base
|
||||
```
|
||||
|
||||
PostgreSQL est supporté côté configuration (`DB_ENGINE=postgresql` dans
|
||||
`.env`, driver `emitter/requirements-postgresql.txt`) mais sans
|
||||
sous-commande de provisionnement automatique pour l'instant — serveur et
|
||||
base à créer manuellement avant de basculer `DB_ENGINE`.
|
||||
|
||||
Portage d'une base vers une autre (dump indépendant du moteur, via
|
||||
`manage.py dumpdata`/`loaddata`) :
|
||||
|
||||
```sh
|
||||
make dump-db # écrit emitter/db-dump-<date>.json (exclu de git, contient des données réelles)
|
||||
# ... bascule DB_ENGINE, migrate ...
|
||||
emitter/.venv/bin/python emitter/manage.py loaddata <fichier>.json
|
||||
```
|
||||
|
||||
## Reverse proxy nginx + TLS public
|
||||
|
||||
```sh
|
||||
sudo make install-nginx # lit DASHBOARD_DOMAIN dans .env
|
||||
```
|
||||
|
||||
Obtient un certificat Let's Encrypt et déploie un vhost nginx devant le
|
||||
tableau de bord (WebSocket inclus) — nécessaire pour un accès public ;
|
||||
sans ça, le tableau de bord n'est accessible qu'en local/VPN.
|
||||
|
||||
## Alternative à systemd : supervisor
|
||||
|
||||
```sh
|
||||
sudo make install-supervisor # bascule les 4 services de l'émetteur vers supervisor
|
||||
sudo make uninstall-supervisor # retour arrière (réactive systemd)
|
||||
```
|
||||
|
||||
## Mise à jour
|
||||
|
||||
```sh
|
||||
git pull
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Réapplique dépendances/migrations et redémarre les services concernés,
|
||||
sur n'importe quel noeud (y compris le master).
|
||||
@@ -0,0 +1,60 @@
|
||||
# Fail2banActionBanisher
|
||||
|
||||
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 (mTLS). 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**.
|
||||
|
||||
## Architecture (overview)
|
||||
|
||||
```
|
||||
[node] fail2ban/iptables → local mosquitto (1883) → master mosquitto (8883, TLS)
|
||||
│
|
||||
decision / correlation
|
||||
│
|
||||
[node] ◄──────────────── MQTT action (BAN, SYNC_BAN, UNBAN, ...) ◄──────────
|
||||
```
|
||||
|
||||
A Django dashboard (real-time, WebSocket) runs on every node and on the
|
||||
master, with an aggregated multi-node view on the master side.
|
||||
|
||||
## 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](deployment.en.md#token-based-node-enrollment-recommended) is
|
||||
enough — particularly relevant for the community and services-company
|
||||
cases, where the servers belong to different parties.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [Installation](installation.en.md) — development setup, first
|
||||
production node.
|
||||
- [Production deployment](deployment.en.md) — master, token-based node
|
||||
enrollment, databases, reverse proxy, alternatives to systemd.
|
||||
- [Reference](reference.en.md) — MQTT topics, master commands, custom
|
||||
fail2ban jails.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Fail2banActionBanishment
|
||||
|
||||
Système distribué de bannissement d'hôtes malveillants basé sur **fail2ban**
|
||||
et **iptables**, avec propagation des événements par **MQTT**.
|
||||
|
||||
Chaque noeud détecte et bannit localement (fail2ban + iptables), publie
|
||||
l'événement sur un broker Mosquitto local, puis relaie l'information vers un
|
||||
broker **master** privé (mTLS). Le master centralise la décision
|
||||
(corrélation multi-noeuds, récidive globale, réputation partagée) et
|
||||
republie les actions à exécuter vers l'ensemble des noeuds abonnés.
|
||||
|
||||
Machine cible : **VPS Debian 13**.
|
||||
|
||||
## Architecture (résumé)
|
||||
|
||||
```
|
||||
[noeud] fail2ban/iptables → mosquitto local (1883) → mosquitto master (8883, TLS)
|
||||
│
|
||||
décision / corrélation
|
||||
│
|
||||
[noeud] ◄──────────────── action MQTT (BAN, SYNC_BAN, UNBAN, ...) ◄──────────
|
||||
```
|
||||
|
||||
Un tableau de bord Django (temps réel, WebSocket) tourne sur chaque noeud et
|
||||
sur le master, avec une vue agrégée multi-noeuds côté master.
|
||||
|
||||
## Cas d'usage
|
||||
|
||||
Le modèle master/noeuds ne suppose aucun lien d'appartenance entre les
|
||||
serveurs protégés — seulement qu'ils partagent une même autorité de
|
||||
décision. Ça rend l'architecture pertinente au-delà d'un opérateur unique
|
||||
sur ses propres VPS :
|
||||
|
||||
- **Un ensemble de sites** (plusieurs VPS d'un même opérateur, chacun
|
||||
hébergeant un ou plusieurs services) : une IP qui attaque un site est
|
||||
bannie partout via `SYNC_BAN`, sans surveillance manuelle site par site.
|
||||
- **Une communauté** (plusieurs administrateurs indépendants, chacun
|
||||
responsable de son propre serveur, qui acceptent de mutualiser leurs
|
||||
détections) : chaque membre garde son propre noeud et son propre
|
||||
fail2ban local — seule la propagation des bans est partagée via le
|
||||
master commun. Aucun accès aux serveurs des autres membres n'est requis
|
||||
ni accordé.
|
||||
- **Une société de services informatiques** qui gère les sites de
|
||||
plusieurs clients : un master centralisé donne une vue agrégée
|
||||
(compteurs jail/pays, historique par noeud, alias par client) sans
|
||||
mélanger les données de client à client — chaque noeud ne voit que ses
|
||||
propres événements, le master voit tout.
|
||||
|
||||
Dans les trois cas, un nouveau serveur rejoint l'ensemble sans jamais
|
||||
donner accès SSH aux autres : une [auto-inscription par jeton à usage
|
||||
unique](deployment.md#auto-inscription-dun-noeud-recommandé) suffit —
|
||||
pertinent en particulier pour la communauté et la société de services, où
|
||||
les serveurs appartiennent à des tiers différents.
|
||||
|
||||
## Pour aller plus loin
|
||||
|
||||
- [Installation](installation.md) — mise en route en développement, premier
|
||||
noeud en production.
|
||||
- [Déploiement en production](deployment.md) — master, auto-inscription
|
||||
d'un noeud, bases de données, reverse proxy, alternatives à systemd.
|
||||
- [Référence](reference.md) — topics MQTT, commandes du master, jails
|
||||
fail2ban personnalisées.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Installation
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Debian 13** VPS, root access (or an account with `sudo`).
|
||||
- The repository cloned into the `$HOME` of a **non-root** user dedicated
|
||||
to this project (e.g. `banisher`) — `install.sh`/`make install`
|
||||
determine this user from the repository's actual owner
|
||||
(`stat -c '%U' <repo>`), never hardcoded. Trade-off accepted: the user
|
||||
gets full `sudo` access (`NOPASSWD:ALL`), to be reserved for an account
|
||||
strictly dedicated to this project.
|
||||
|
||||
```sh
|
||||
git clone <repo-url> ~/Fail2banMqttActionBanisher
|
||||
cd ~/Fail2banMqttActionBanisher
|
||||
```
|
||||
|
||||
## Development (without real fail2ban/iptables)
|
||||
|
||||
Requires a reachable Redis (Django Channels channel layer), e.g.
|
||||
`docker run -p 6379:6379 redis`.
|
||||
|
||||
```sh
|
||||
make run # dashboard + map, http://127.0.0.1:8000/
|
||||
make mqtt # in a second terminal: MQTT ingestion
|
||||
make worker # in a third terminal: async geolocation
|
||||
```
|
||||
|
||||
These targets automatically create the Python venv (`emitter/.venv`) and
|
||||
install dependencies on first run.
|
||||
|
||||
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.
|
||||
|
||||
## First production node
|
||||
|
||||
!!! warning "Must be adapted before `make install`"
|
||||
`generic/firewall/firewall-filters.conf` and the files under
|
||||
`generic/fail2ban/jail.d/` come from the reference server
|
||||
(`miraceti-vps1`, a master) and contain rules specific to it
|
||||
(OpenVPN, Coturn, WireGuard, Portainer, a hardcoded public IP,
|
||||
`DOCKER-USER`...). On a plain node, keep only what's actually
|
||||
exposed (typically SSH + 80/443) — the unneeded sections reference
|
||||
interfaces (`$VPN_IF`, `$WG_IF`) that don't exist on a node, which
|
||||
produces silent `iptables` errors (the script keeps going, but the
|
||||
intended rule is never applied). Also check `ETH_IF` in
|
||||
`generic/firewall/firewall-launcher.sh` (real interface name:
|
||||
`ip -o link show`), and disable `ufw` if present (having both
|
||||
`iptables` rulesets active at once breaks one of them in
|
||||
unpredictable ways).
|
||||
|
||||
```sh
|
||||
sudo make install
|
||||
sudo make status
|
||||
```
|
||||
|
||||
Deploys and enables: system packages (fail2ban, iptables, mosquitto,
|
||||
Redis), firewall, custom fail2ban jails, local Mosquitto broker, and the
|
||||
Django emitter's four services (web, MQTT ingestion, Celery worker,
|
||||
master relay). Idempotent: re-running `sudo make install` after a
|
||||
`git pull` reapplies dependencies/migrations and restarts the affected
|
||||
services.
|
||||
|
||||
Edit `/etc/fail2ban/mqtt.conf` (real MQTT credentials, copied from
|
||||
`generic/fail2ban/mqtt.conf.example` if it doesn't exist yet) before the
|
||||
first deployment.
|
||||
|
||||
A freshly installed node already works standalone (local detection and
|
||||
banning) ; the relay to a master (`fail2ban-emitter-master-client`) will
|
||||
keep retrying in a loop until a node certificate has been obtained — see
|
||||
[Production deployment](deployment.en.md) for what's next.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Installation
|
||||
|
||||
## Prérequis
|
||||
|
||||
- VPS **Debian 13**, accès root (ou un compte avec `sudo`).
|
||||
- Le dépôt cloné dans le `$HOME` d'un utilisateur **non-root** dédié à ce
|
||||
projet (ex. `banisher`) — `install.sh`/`make install` déterminent cet
|
||||
utilisateur à partir du propriétaire réel du dépôt
|
||||
(`stat -c '%U' <dépôt>`), jamais codé en dur. Contrepartie assumée :
|
||||
l'utilisateur reçoit un accès `sudo` complet (`NOPASSWD:ALL`), à
|
||||
réserver à un compte strictement dédié à ce projet.
|
||||
|
||||
```sh
|
||||
git clone <url-du-dépôt> ~/Fail2banMqttActionBanisher
|
||||
cd ~/Fail2banMqttActionBanisher
|
||||
```
|
||||
|
||||
## Développement (sans fail2ban/iptables réels)
|
||||
|
||||
Nécessite un Redis accessible (channel layer Django Channels), par exemple
|
||||
`docker run -p 6379:6379 redis`.
|
||||
|
||||
```sh
|
||||
make run # tableau de bord + carte, http://127.0.0.1:8000/
|
||||
make mqtt # dans un second terminal : ingestion MQTT
|
||||
make worker # dans un troisième terminal : géolocalisation asynchrone
|
||||
```
|
||||
|
||||
Ces cibles créent automatiquement le venv Python (`emitter/.venv`) et
|
||||
installent les dépendances au premier lancement.
|
||||
|
||||
Variables d'environnement (`MQTT_BROKER_HOST`, `MQTT_BROKER_USERNAME`,
|
||||
`MQTT_BROKER_PASSWORD`, `REDIS_HOST`, `REDIS_PORT`, ...) : copier
|
||||
`.env.example` vers `.env` à la racine du dépôt et l'éditer — chargé
|
||||
automatiquement par Django (`python-dotenv`), rien à exporter à la main.
|
||||
|
||||
## Premier noeud en production
|
||||
|
||||
!!! warning "À adapter impérativement avant `make install`"
|
||||
`generic/firewall/firewall-filters.conf` et les fichiers de
|
||||
`generic/fail2ban/jail.d/` proviennent du serveur de référence
|
||||
(`miraceti-vps1`, un master) et contiennent des règles qui lui sont
|
||||
propres (OpenVPN, Coturn, WireGuard, Portainer, IP publique codée en
|
||||
dur, `DOCKER-USER`...). Sur un simple noeud, ne garder que ce qui est
|
||||
réellement exposé (typiquement SSH + 80/443) — les sections
|
||||
superflues réfèrent des interfaces (`$VPN_IF`, `$WG_IF`) absentes
|
||||
d'un noeud, ce qui produit des erreurs `iptables` silencieuses (le
|
||||
script continue, mais la règle voulue n'est jamais posée). Vérifier
|
||||
aussi `ETH_IF` dans `generic/firewall/firewall-launcher.sh` (nom
|
||||
réel de l'interface : `ip -o link show`) et désactiver `ufw` s'il est
|
||||
présent (coexister avec les règles `iptables` posées ici casserait
|
||||
l'un des deux jeux de règles de façon imprévisible).
|
||||
|
||||
```sh
|
||||
sudo make install
|
||||
sudo make status
|
||||
```
|
||||
|
||||
Déploie et active : paquets système (fail2ban, iptables, mosquitto,
|
||||
Redis), pare-feu, jails fail2ban personnalisées, broker Mosquitto local,
|
||||
et les quatre services de l'émetteur Django (web, ingestion MQTT, worker
|
||||
Celery, relais vers le master). Idempotent : relancer `sudo make install`
|
||||
après un `git pull` réapplique dépendances/migrations et redémarre les
|
||||
services concernés.
|
||||
|
||||
Éditer `/etc/fail2ban/mqtt.conf` (identifiants MQTT réels, copié depuis
|
||||
`generic/fail2ban/mqtt.conf.example` s'il n'existe pas encore) avant le
|
||||
premier déploiement.
|
||||
|
||||
Un noeud fraîchement installé fonctionne déjà en autonome (détection et
|
||||
ban locaux) ; le relais vers un master (`fail2ban-emitter-master-client`)
|
||||
tentera de se connecter en boucle tant qu'aucun certificat de noeud n'a
|
||||
été obtenu — voir [Déploiement en production](deployment.md) pour la
|
||||
suite.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Reference
|
||||
|
||||
## MQTT topics
|
||||
|
||||
| Topic | Direction | Role |
|
||||
|---|---|---|
|
||||
| `fail2ban/<node>/ban` | node → master | ban/notice event published by this node |
|
||||
| `fail2ban/<node>/heartbeat` | node → master | periodic liveness proof (independent of any ban activity) |
|
||||
| `fail2ban/<node>/ack` | node → master | acknowledgement after executing a command received from the master |
|
||||
| `fail2ban/<node>/action` | master → node | command addressed specifically to this node |
|
||||
| `fail2ban/broadcast/action` | master → all nodes | command broadcast to all subscribed nodes |
|
||||
| `fail2ban/broadcast/roster` | master → all nodes | list of known nodes + global jail/country counters, periodic |
|
||||
|
||||
`<node>` in the topic segment is the mTLS client certificate's CN
|
||||
(enforced by the broker's ACL) — the node's internal identifier
|
||||
(`uuid.getnode()`) travels in the message body, not in the topic.
|
||||
|
||||
## Master commands
|
||||
|
||||
| Command | Effect on the receiving node | Trigger |
|
||||
|---|---|---|
|
||||
| `SYNC_BAN` | Full ban (all ports) via a dedicated jail | Automatic: the master systematically publishes a `SYNC_BAN` for every ban it ingests |
|
||||
| `ESCALATE` | **Permanent** ban via a dedicated jail | Automatic: the master correlates — an IP independently banned on several distinct nodes within 24h triggers an escalation |
|
||||
| `BAN_ALLPORTS` | Full ban (all ports), same mechanism as `SYNC_BAN` | Manual (targeted human decision) |
|
||||
| `WHITELIST` | Added to `ignoreip` on all active jails + unbanned — in-memory, not persistent | Manual |
|
||||
| `RATE_LIMIT` | Throttle (10 req/s, burst 20) instead of a full block | Manual — edge case, likely false positives |
|
||||
| `NOTIFY_ONLY` | No network action, just a visible dashboard notification | Manual — observation mode |
|
||||
| `REPORT_ABUSE` | Report to AbuseIPDB — **not broadcast to nodes**, submitted directly by the master (one report per IP is enough) | Manual, dry-run until `ABUSEIPDB_ENABLED`/`ABUSEIPDB_API_KEY` are configured |
|
||||
|
||||
Manual commands are triggered on the master, by hand:
|
||||
|
||||
```sh
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command WHITELIST 203.0.113.5
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command BAN_ALLPORTS 203.0.113.5
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command RATE_LIMIT 203.0.113.5
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command REPORT_ABUSE 203.0.113.5
|
||||
```
|
||||
|
||||
Geolocation of banned IPs is automatic (asynchronous, on ingestion of
|
||||
each event); to retry the ones left without coordinates (API failure,
|
||||
private IP):
|
||||
|
||||
```sh
|
||||
emitter/.venv/bin/python emitter/manage.py retry_geolocation # all ungeolocated events
|
||||
emitter/.venv/bin/python emitter/manage.py retry_geolocation --ip <ip> # a specific IP
|
||||
```
|
||||
|
||||
## Custom fail2ban jails
|
||||
|
||||
| Jail | Role |
|
||||
|---|---|
|
||||
| `emitter-admin-auth` | Dashboard `/admin/` lockout (django-axes) → network ban |
|
||||
| `master-sync` | Receiving a `SYNC_BAN`/`BAN_ALLPORTS` — never triggered by a log, only by manual injection from the master relay |
|
||||
| `master-ratelimit` | Receiving a `RATE_LIMIT` |
|
||||
| `master-escalate` | Receiving an `ESCALATE` — permanent ban |
|
||||
|
||||
`emitter-admin-auth` follows a reusable template
|
||||
(`generic/fail2ban/jail.d/app-auth.conf.template` +
|
||||
`generic/fail2ban/filter.d/app-auth.conf`) to protect any other
|
||||
application exposed on the same server the same way, not just this
|
||||
dashboard.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Référence
|
||||
|
||||
## Topics MQTT
|
||||
|
||||
| Topic | Sens | Rôle |
|
||||
|---|---|---|
|
||||
| `fail2ban/<noeud>/ban` | noeud → master | événement de ban/notice publié par ce noeud |
|
||||
| `fail2ban/<noeud>/heartbeat` | noeud → master | preuve de vie périodique (indépendante de toute activité de ban) |
|
||||
| `fail2ban/<noeud>/ack` | noeud → master | accusé de réception après exécution d'une commande reçue du master |
|
||||
| `fail2ban/<noeud>/action` | master → noeud | commande adressée spécifiquement à ce noeud |
|
||||
| `fail2ban/broadcast/action` | master → tous les noeuds | commande diffusée à tous les noeuds abonnés |
|
||||
| `fail2ban/broadcast/roster` | master → tous les noeuds | liste des noeuds connus + compteurs jail/pays globaux, périodique |
|
||||
|
||||
`<noeud>` dans le segment de topic est le CN du certificat client mTLS
|
||||
(imposé par l'ACL du broker) — l'identifiant interne du noeud
|
||||
(`uuid.getnode()`) voyage dans le corps du message, pas dans le topic.
|
||||
|
||||
## Commandes du master
|
||||
|
||||
| Commande | Effet sur le noeud qui la reçoit | Déclenchement |
|
||||
|---|---|---|
|
||||
| `SYNC_BAN` | Ban total (toutes ports) via une jail dédiée | Automatique : le master publie systématiquement un `SYNC_BAN` pour chaque ban qu'il ingère |
|
||||
| `ESCALATE` | Ban **permanent** via une jail dédiée | Automatique : le master corrèle — une IP bannie indépendamment sur plusieurs noeuds distincts en 24h déclenche une escalade |
|
||||
| `BAN_ALLPORTS` | Ban total (toutes ports), même mécanisme que `SYNC_BAN` | Manuel (décision humaine ciblée) |
|
||||
| `WHITELIST` | Ajout à `ignoreip` sur toutes les jails actives + débannit — en mémoire, non persistant | Manuel |
|
||||
| `RATE_LIMIT` | Throttle (10 req/s, burst 20) au lieu d'un blocage total | Manuel — cas limite, faux positifs probables |
|
||||
| `NOTIFY_ONLY` | Aucune action réseau, juste une notification visible sur le tableau de bord | Manuel — mode observation |
|
||||
| `REPORT_ABUSE` | Signalement à AbuseIPDB — **pas diffusé aux noeuds**, soumis directement par le master (un rapport par IP suffit) | Manuel, dry-run tant que `ABUSEIPDB_ENABLED`/`ABUSEIPDB_API_KEY` ne sont pas configurés |
|
||||
|
||||
Les commandes manuelles se déclenchent sur le master, à la main :
|
||||
|
||||
```sh
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command WHITELIST 203.0.113.5
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command BAN_ALLPORTS 203.0.113.5
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command RATE_LIMIT 203.0.113.5
|
||||
emitter/.venv/bin/python emitter/manage.py publish_command REPORT_ABUSE 203.0.113.5
|
||||
```
|
||||
|
||||
La géolocalisation des IP bannies est automatique (asynchrone, dès
|
||||
l'ingestion d'un événement) ; pour relancer celles restées sans
|
||||
coordonnées (échec d'API, IP privée) :
|
||||
|
||||
```sh
|
||||
emitter/.venv/bin/python emitter/manage.py retry_geolocation # tous les événements non géolocalisés
|
||||
emitter/.venv/bin/python emitter/manage.py retry_geolocation --ip <ip> # une IP précise
|
||||
```
|
||||
|
||||
## Jails fail2ban personnalisées
|
||||
|
||||
| Jail | Rôle |
|
||||
|---|---|
|
||||
| `emitter-admin-auth` | Verrouillage `/admin/` du tableau de bord (django-axes) → ban réseau |
|
||||
| `master-sync` | Réception d'un `SYNC_BAN`/`BAN_ALLPORTS` — jamais déclenchée par un log, seulement par injection manuelle depuis le relais master |
|
||||
| `master-ratelimit` | Réception d'un `RATE_LIMIT` |
|
||||
| `master-escalate` | Réception d'un `ESCALATE` — ban permanent |
|
||||
|
||||
`emitter-admin-auth` suit un gabarit réutilisable
|
||||
(`generic/fail2ban/jail.d/app-auth.conf.template` +
|
||||
`generic/fail2ban/filter.d/app-auth.conf`) pour protéger de la même façon
|
||||
n'importe quelle autre application exposée sur le même serveur, pas
|
||||
seulement ce tableau de bord.
|
||||
Reference in New Issue
Block a user