First commit

This commit is contained in:
2026-07-20 11:05:07 +02:00
commit b592ab669f
159 changed files with 10294 additions and 0 deletions
@@ -0,0 +1,32 @@
[Unit]
Description=Fail2banActionBanisher - émetteur : relais vers le master (master_client, SYNC_BAN)
After=network.target mosquitto.service
Requires=mosquitto.service
[Service]
Type=simple
User=__EMITTER_USER__
Group=__EMITTER_GROUP__
WorkingDirectory=__EMITTER_DIR__
EnvironmentFile=__ENV_FILE__
Environment=DJANGO_SETTINGS_MODULE=config.settings.production
Environment=PYTHONUNBUFFERED=1
ExecStart=__EMITTER_DIR__/.venv/bin/python manage.py master_client
Restart=on-failure
RestartSec=5
# Pas de NoNewPrivileges=true ici, contrairement aux autres services de
# l'émetteur : ce process a structurellement besoin d'escalader via sudo
# (execute_sync_ban -> `sudo fail2ban-client set master-sync banip <ip>`,
# cf. master_client.py). NoNewPrivileges bloque tout mécanisme de gain de
# privilège au niveau execve, y compris le setuid de sudo lui-même — sudo
# échoue alors avec "the 'no new privileges' flag is set", quels que soient
# les droits sudoers. Repéré seulement en conditions réelles (le premier
# vrai SYNC_BAN reçu d'un autre noeud) : les tests précédents mockaient
# subprocess.run, donc n'exerçaient jamais cette interaction avec systemd.
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=__EMITTER_DIR__
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,24 @@
[Unit]
Description=Fail2banActionBanisher - émetteur : ingestion master (master_listen, tous les noeuds)
After=network.target mosquitto.service
Requires=mosquitto.service
[Service]
Type=simple
User=__EMITTER_USER__
Group=__EMITTER_GROUP__
WorkingDirectory=__EMITTER_DIR__
EnvironmentFile=__ENV_FILE__
Environment=DJANGO_SETTINGS_MODULE=config.settings.production
Environment=PYTHONUNBUFFERED=1
ExecStart=__EMITTER_DIR__/.venv/bin/python manage.py master_listen
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=__EMITTER_DIR__
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,24 @@
[Unit]
Description=Fail2banActionBanisher - émetteur : ingestion MQTT (mqtt_listen)
After=network.target mosquitto.service
Requires=mosquitto.service
[Service]
Type=simple
User=__EMITTER_USER__
Group=__EMITTER_GROUP__
WorkingDirectory=__EMITTER_DIR__
EnvironmentFile=__ENV_FILE__
Environment=DJANGO_SETTINGS_MODULE=config.settings.production
Environment=PYTHONUNBUFFERED=1
ExecStart=__EMITTER_DIR__/.venv/bin/python manage.py mqtt_listen
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=__EMITTER_DIR__
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,62 @@
[Unit]
Description=Fail2banActionBanisher - émetteur Django (ASGI/Daphne)
After=network.target redis-server.service
Requires=redis-server.service
[Service]
Type=simple
User=__EMITTER_USER__
Group=__EMITTER_GROUP__
WorkingDirectory=__EMITTER_DIR__
EnvironmentFile=__ENV_FILE__
Environment=DJANGO_SETTINGS_MODULE=config.settings.production
Environment=PYTHONUNBUFFERED=1
# Loopback uniquement par défaut : éditer le bind ici pour un accès distant
# (ex. IP du VPN de confiance), pas d'exposition publique directe prévue.
ExecStart=__EMITTER_DIR__/.venv/bin/daphne -b 127.0.0.1 -p 8050 config.asgi:application
Restart=on-failure
RestartSec=5
# Pas de NoNewPrivileges=true ici (retiré 2026-07-18) : depuis l'endpoint
# /api/join/ (banevents/views.py::join_node, auto-inscription des noeuds
# par jeton), ce process a structurellement besoin d'escalader via sudo
# (sudo scripts/sign-node-csr.sh ...). Même piège déjà rencontré sur
# fail2ban-emitter-master-client.service : NoNewPrivileges bloque tout
# mécanisme de gain de privilège au niveau execve, y compris le setuid de
# sudo lui-même — sudo échoue alors avec "the 'no new privileges' flag is
# set", quels que soient les droits sudoers. Repéré en conditions réelles
# (premier vrai test de /api/join/), pas en lecture de code.
ProtectSystem=strict
PrivateTmp=true
# /etc/mosquitto/master-ca et master-acl.conf : sign-node-csr.sh (appelé
# via sudo par /api/join/) écrit un fichier .srl à côté de ca.crt
# (-CAcreateserial d'openssl) ET ajoute l'entitlement ACL du nouveau
# noeud — sudo ne crée pas de nouveau mount namespace, le sudo'd child
# hérite du même ProtectSystem=strict que ce process, donc ces deux
# chemins doivent être listés ici aussi, pas seulement __EMITTER_DIR__.
# Repéré en conditions réelles (premier vrai test de /api/join/, échec
# silencieux : openssl/l'ajout ACL échouaient sans qu'aucun message ne
# remonte jusqu'au subprocess.run() de la vue Django).
#
# Préfixe "-" obligatoire sur les deux chemins mosquitto : CE service
# tourne sur TOUS les noeuds (unité générique), pas seulement le master —
# /etc/mosquitto/master-ca et master-acl.conf n'existent QUE sur le
# master (déployés par install-master). ReadWritePaths= exige que le
# chemin existe déjà au moment de construire le mount namespace ; sans
# "-", le service refuse de démarrer PARTOUT ailleurs que sur le master
# ("Failed to set up mount namespacing: ... No such file or directory",
# status 226/NAMESPACE) — cassé en conditions réelles sur un vrai noeud
# (linuxtarn-vps1) le jour même du déploiement. "-" en tête dit à systemd
# d'ignorer silencieusement l'entrée si le chemin est absent.
# /var/spool/exim4 et /var/log/exim4 : django-sendmail-backend invoque
# /usr/sbin/sendmail directement (voir config/settings/base.py) pour
# l'email d'inscription à la communauté (banevents/views.py::
# community_landing, mail_admins) — même piège que ci-dessus, repéré en
# conditions réelles : "Cannot open main log file ... Permission denied"
# + "Failed to create spool file ... Read-only file system". Préfixe "-"
# : un noeud sans exim4 installé ne doit pas empêcher ce service de
# démarrer.
ReadWritePaths=__EMITTER_DIR__ -/etc/mosquitto/master-ca -/etc/mosquitto/master-acl.conf -/var/spool/exim4 -/var/log/exim4
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,24 @@
[Unit]
Description=Fail2banActionBanisher - émetteur : worker Celery (géolocalisation)
After=network.target redis-server.service
Requires=redis-server.service
[Service]
Type=simple
User=__EMITTER_USER__
Group=__EMITTER_GROUP__
WorkingDirectory=__EMITTER_DIR__
EnvironmentFile=__ENV_FILE__
Environment=DJANGO_SETTINGS_MODULE=config.settings.production
Environment=PYTHONUNBUFFERED=1
ExecStart=__EMITTER_DIR__/.venv/bin/celery -A config worker -l info
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=__EMITTER_DIR__
[Install]
WantedBy=multi-user.target
+18
View File
@@ -0,0 +1,18 @@
# Log dédié aux verrouillages django-axes (emitter/logs/admin-auth.log,
# cf. LOGGING dans config/settings/base.py et
# generic/fail2ban/jail.d/emitter-admin-auth.conf) — aucun logrotate.d
# système ne le couvre (chemin custom sous le home du user de déploiement,
# pas /var/log). copytruncate, pas le rename+recreate par défaut : le
# logging.FileHandler de Python garde son descripteur de fichier ouvert
# tant que le process (Daphne) tourne, il ne le rouvrirait jamais tout
# seul après un rename — copytruncate tronque le fichier EN PLACE (même
# inode), pas besoin de redémarrer le service pour rotation.
__EMITTER_DIR__/logs/admin-auth.log {
weekly
rotate 6
compress
delaycompress
missingok
notifempty
copytruncate
}
@@ -0,0 +1,20 @@
[Definition]
actionstart = iptables -N fail2ban-<name>
iptables -A fail2ban-<name> -j RETURN
iptables -I <chain> -p <protocol> -j fail2ban-<name>
actionstop = iptables -D <chain> -p <protocol> -j fail2ban-<name> || true
iptables -F fail2ban-<name> || true
iptables -X fail2ban-<name> || true
actioncheck = iptables -n -L <chain> | grep -q fail2ban-<name>
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j DROP
actionunban = iptables -D fail2ban-<name> -s <ip> -j DROP
[Init]
name = default
protocol = tcp
chain = INPUT
@@ -0,0 +1,30 @@
[Definition]
# Throttle (RATE_LIMIT, Phase 4) plutôt qu'un blocage total : au lieu de
# DROP toutes les connexions de <ip>, ne DROP que celles au-delà du débit
# autorisé. --hashlimit-mode srcip fait déjà le bucketing par IP source
# dans une table hashlimit partagée (f2b-<name>) : pas besoin d'un nom de
# table distinct par IP bannie, une seule table suffit pour toute la jail.
actionstart = iptables -N fail2ban-<name>
iptables -A fail2ban-<name> -j RETURN
iptables -I <chain> -p <protocol> -j fail2ban-<name>
actionstop = iptables -D <chain> -p <protocol> -j fail2ban-<name> || true
iptables -F fail2ban-<name> || true
iptables -X fail2ban-<name> || true
actioncheck = iptables -n -L <chain> | grep -q fail2ban-<name>
# actionunban doit reprendre EXACTEMENT la même spec de règle que
# actionban (seul -I vs -D change) : iptables -D échoue silencieusement
# ("Bad rule") si les deux ne correspondent pas au caractère près.
actionban = iptables -I fail2ban-<name> 1 -s <ip> -m hashlimit --hashlimit-above <hashlimit_rate> --hashlimit-burst <hashlimit_burst> --hashlimit-mode srcip --hashlimit-name f2b-<name> -j DROP
actionunban = iptables -D fail2ban-<name> -s <ip> -m hashlimit --hashlimit-above <hashlimit_rate> --hashlimit-burst <hashlimit_burst> --hashlimit-mode srcip --hashlimit-name f2b-<name> -j DROP
[Init]
name = default
protocol = tcp
chain = INPUT
hashlimit_rate = 10/sec
hashlimit_burst = 20
@@ -0,0 +1,22 @@
[Definition]
actionstart = iptables -N fail2ban-<name>
iptables -A fail2ban-<name> -j RETURN
iptables -I <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name>
# Correction : <iptables> était un tag invalide dans la version originale
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name> || true
iptables -F fail2ban-<name> || true
iptables -X fail2ban-<name> || true
actioncheck = iptables -n -L <chain> | grep -q fail2ban-<name>
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j DROP
actionunban = iptables -D fail2ban-<name> -s <ip> -j DROP
[Init]
name = default
port = ssh
protocol = tcp
chain = INPUT
@@ -0,0 +1,19 @@
[Definition]
actionstart =
actioncheck =
actionstop =
# <name> = nom du jail qui a déclenché l'action
# <port>/<protocol> = valeurs du jail si passées explicitement (voir jail.local),
# sinon les défauts "-" du [Init] ci-dessous (ex. jails
# recidive, qui ne ciblent pas un port/protocole précis)
actionban = /etc/fail2ban/action.d/f2b_mqtt_action_banisher.py \
"{\"action\":\"BAN\",\"ip\":\"<ip>\",\"time\":\"<time>\",\"name\":\"<name>\",\"bantime\":\"<bantime>\",\"port\":\"<port>\",\"protocol\":\"<protocol>\"}"
actionunban = /etc/fail2ban/action.d/f2b_mqtt_action_banisher.py \
"{\"action\":\"UNBAN\",\"ip\":\"<ip>\",\"time\":\"<time>\",\"name\":\"<name>\",\"bantime\":\"<bantime>\",\"port\":\"<port>\",\"protocol\":\"<protocol>\"}"
[Init]
init = Mqtt publisher loaded
port = -
protocol = -
@@ -0,0 +1,79 @@
#!/usr/bin/python3
"""Fail2ban action: publish each ban/unban event to the local MQTT broker."""
import configparser
import datetime
import json
import socket
import sys
import uuid
from pathlib import Path
from typing import Any
import paho.mqtt.publish as publish
CONFIG_FILE = Path('/etc/fail2ban/mqtt.conf')
LOG_FILE = '/var/log/fail2ban.log'
# Timeout global sur toutes les connexions socket (évite de bloquer fail2ban)
socket.setdefaulttimeout(5)
def load_mqtt_config() -> dict[str, Any]:
parser = configparser.ConfigParser()
if not parser.read(CONFIG_FILE):
raise FileNotFoundError(f'{CONFIG_FILE} introuvable — voir mqtt.conf.example')
section = parser['mqtt']
return {
'host': section.get('host', '127.0.0.1'),
'port': section.getint('port', 1883),
'username': section['username'],
'password': section['password'],
}
def log(message: str) -> None:
try:
with open(LOG_FILE, 'a') as f:
f.write('{} [MQTT PUBLISHER] {}\n'.format(datetime.datetime.now(), message))
except OSError as e:
print(f'fail2ban mqtt log error: {e}')
def publish_message(base_topic: str, mqtt_params: dict[str, Any], topic: str, **payload: Any) -> None:
try:
message = json.dumps(payload)
publish.single(base_topic + topic, message, **mqtt_params)
except Exception as e:
log(f'publish_message error: {e}')
def main() -> None:
try:
config = load_mqtt_config()
except Exception as e:
log(f'config error: {e}')
return
node_id = uuid.getnode()
node_type = 'fail2ban'
base_topic = f'{node_type}/{node_id}'
mqtt_params: dict[str, Any] = dict(
qos=0,
retain=False,
hostname=config['host'],
port=config['port'],
keepalive=60,
auth=dict(username=config['username'], password=config['password']),
client_id=f'{node_type}_{node_id}',
)
try:
payload = json.loads(sys.argv[1])
publish_message(base_topic, mqtt_params, '/jail', node=node_id, **payload)
except Exception as e:
log(f'main error: {e}')
if __name__ == '__main__':
main()
+3
View File
@@ -0,0 +1,3 @@
[DEFAULT]
# Conserver l'historique des bans 7 jours (défaut : 1 jour)
dbpurgeage = 7d
+25
View File
@@ -0,0 +1,25 @@
[Definition]
# Filtre générique réutilisable par TOUTE appli de ce VPS qui veut se
# protéger d'un brute-force applicatif (pas seulement ce projet — cf.
# jail.d/app-auth.conf.template pour le mode d'emploi). Une seule
# convention de ligne de log à respecter, peu importe l'appli :
# Verrouillage <NOM_APPLI> après échecs répétés depuis <IP> (...)
# <NOM_APPLI> n'est volontairement pas capturé (\S+ générique, un seul
# mot) : un seul filtre sert toutes les jails app-auth-*, quelle que soit
# l'appli protégée — pas de fichier filter.d dédié à réécrire à chaque
# nouvelle appli. Premier consommateur : emitter-admin-auth (Django +
# django-axes, cf. emitter/banevents/signals.py pour l'exemple concret).
failregex = ^.*Verrouillage \S+ après échecs répétés depuis <HOST>.*$
ignoreregex =
# Piège vécu (emitter-admin-auth, 2026-07-18) : le timestamp EN TÊTE de
# ligne doit porter un fuseau explicite (ex. suffixe "Z" ISO 8601, UTC).
# fail2ban lit l'horloge système réelle du VPS pour juger la fraîcheur
# d'une ligne ; un process applicatif qui journalise en UTC sans le dire
# (courant avec Django : TIME_ZONE='UTC' force tout le process en UTC via
# time.tzset(), cf. config/settings/base.py) produit des timestamps qui
# paraissent "vieux" de plusieurs heures dès que le fuseau système diffère
# d'UTC — fail2ban les ignore alors silencieusement juste après un
# restart ("Ignoring all log entries older than 3600s"), sans aucune
# erreur visible. Toute appli qui adopte ce filtre doit journaliser avec
# un marqueur de fuseau non ambigu.
@@ -0,0 +1,19 @@
# Generic configuration file for -botsearch filters
[Init]
# Block is the actual non-found directories to block
block = \/?(<webmail>|<phpmyadmin>|<wordpress>|cgi-bin|mysqladmin)[^,]*
# These are just convenient definitions that assist the blocking of stuff that
# isn't installed
webmail = roundcube|(ext)?mail|horde|(v-?)?webmail
phpmyadmin = (typo3/|xampp/|admin/|)(pma|(php)?[Mm]y[Aa]dmin)
wordpress = wp-(login|signup|admin)\.php
# DEV Notes:
# Taken from apache-botsearch filter
#
# Author: Frantisek Sumsal
+16
View File
@@ -0,0 +1,16 @@
[INCLUDES]
before = common.conf
[Definition]
# Coturn log format :
# 2024-01-15 10:23:45.123 : ERROR: <pid>: <session>,from=<ip>:<port>,...: Wrong credentials
# 2024-01-15 10:23:45.123 : WARNING: <pid>: ...: No credentials
#
# Vérifier le chemin des logs : /var/log/turnserver*.log ou /var/log/coturn.log
# et adapter si nécessaire avec : fail2ban-regex /var/log/coturn.log /etc/fail2ban/filter.d/coturn.conf
failregex = ^.*: (?:ERROR|WARNING).*from=<HOST>:\d+.*: (?:Wrong credentials|No credentials|Cannot find credentials|authentication failed|Auth error)
^.*: (?:ERROR|WARNING).*\(<HOST>,\d+\).*: (?:Wrong credentials|No credentials|Cannot find credentials)
ignoreregex =
@@ -0,0 +1,14 @@
[Definition]
# Filtre volontairement inerte : cette jail n'est jamais déclenchée par un
# log, uniquement par injection manuelle (fail2ban-client set banip),
# depuis master_client.py sur réception d'un SYNC_BAN du master. logpath
# pointe vers un fichier réel existant (fail2ban.log) pour satisfaire
# fail2ban au démarrage (cf. incident coturn, ROADMAP.md Phase 1.b), mais
# ce failregex ne doit jamais matcher quoi que ce soit dedans.
#
# <HOST> est obligatoire même ici : fail2ban rejette au chargement tout
# failregex sans groupe d'identification de l'hôte ("No failure-id group
# in ..."), qu'il soit ou non censé matcher un jour. Le préfixe littéral
# improbable garantit qu'aucune ligne réelle de fail2ban.log ne matchera.
failregex = ^NEVER_MATCH_ANYTHING_master_sync_is_manual_only <HOST>$
ignoreregex =
+24
View File
@@ -0,0 +1,24 @@
[INCLUDES]
before = common.conf
[Definition]
# Mosquitto ne logue pas l'IP sur la ligne d'échec d'auth — deux approches :
#
# Approche A (défaut) : capturer les connexions répétées sur le port TLS (8883/8884)
# Nécessite : log_type connect (ou all) dans mosquitto.conf
# Régler maxretry à 10+ pour éviter les faux positifs (reconnexions légitimes MQTT)
#
# Approche B (recommandée) : activer un plugin d'auth qui logue l'IP sur les échecs
# Ex: mosquitto-go-auth → logue "Login Failed: <user> from <ip>"
#
# Adapter la regex au format de ton mosquitto.log avec :
# fail2ban-regex /var/log/mosquitto/mosquitto.log /etc/fail2ban/filter.d/mosquitto.conf
# Approche A — connexions sur port TLS
failregex = ^.*: New connection from <HOST>(?::\d+)? on port (?:8883|8884)\.$
# Approche B — si plugin d'auth avec IP sur la ligne d'échec (décommenter)
#failregex = ^.*: (?:Login Failed|authentication failed|not authorised).*from <HOST>
ignoreregex =
+3
View File
@@ -0,0 +1,3 @@
[Definition]
failregex = ^<HOST>.*"(GET|POST).*" (400|403|404|405|444) .*$
ignoreregex = .*(robots\.txt|favicon\.ico|\.jpg|\.png|\.css|\.js)
@@ -0,0 +1,24 @@
# Fail2Ban configuration file
#
# Regexp to catch known spambots and software alike. Please verify
# that it is your intent to block IPs which were driven by
# above mentioned bots.
[Definition]
badbotscustom = EmailCollector|WebEMailExtrac|TrackBack/1\.02|sogou music spider|(?:Mozilla/\d+\.\d+ )?Jorgee
badbots = Atomic_Email_Hunter/4\.0|atSpider/1\.0|autoemailspider|bwh3_user_agent|China Local Browse 2\.6|ContactBot/0\.2|ContentSmartz|DataCha0s/2\.0|DBrowse 1\.4b|DBrowse 1\.4d|Demo Bot DOT 16b|Demo Bot Z 16b|DSurf15a 01|DSurf15a 71|DSurf15a 81|DSurf15a VA|EBrowse 1\.4b|Educate Search VxB|EmailSiphon|EmailSpider|EmailWolf 1\.00|ESurf15a 15|ExtractorPro|Franklin Locator 1\.8|FSurf15a 01|Full Web Bot 0416B|Full Web Bot 0516B|Full Web Bot 2816B|Guestbook Auto Submitter|Industry Program 1\.0\.x|ISC Systems iRc Search 2\.1|IUPUI Research Bot v 1\.9a|LARBIN-EXPERIMENTAL \(efp@gmx\.net\)|LetsCrawl\.com/1\.0 \+http\://letscrawl\.com/|Lincoln State Web Browser|LMQueueBot/0\.2|LWP\:\:Simple/5\.803|Mac Finder 1\.0\.xx|MFC Foundation Class Library 4\.0|Microsoft URL Control - 6\.00\.8xxx|Missauga Locate 1\.0\.0|Missigua Locator 1\.9|Missouri College Browse|Mizzu Labs 2\.2|Mo College 1\.9|MVAClient|Mozilla/2\.0 \(compatible; NEWT ActiveX; Win32\)|Mozilla/3\.0 \(compatible; Indy Library\)|Mozilla/3\.0 \(compatible; scan4mail \(advanced version\) http\://www\.peterspages\.net/?scan4mail\)|Mozilla/4\.0 \(compatible; Advanced Email Extractor v2\.xx\)|Mozilla/4\.0 \(compatible; Iplexx Spider/1\.0 http\://www\.iplexx\.at\)|Mozilla/4\.0 \(compatible; MSIE 5\.0; Windows NT; DigExt; DTS Agent|Mozilla/4\.0 efp@gmx\.net|Mozilla/5\.0 \(Version\: xxxx Type\:xx\)|NameOfAgent \(CMS Spider\)|NASA Search 1\.0|Nsauditor/1\.x|PBrowse 1\.4b|PEval 1\.4b|Poirot|Port Huron Labs|Production Bot 0116B|Production Bot 2016B|Production Bot DOT 3016B|Program Shareware 1\.0\.2|PSurf15a 11|PSurf15a 51|PSurf15a VA|psycheclone|RSurf15a 41|RSurf15a 51|RSurf15a 81|searchbot admin@google\.com|ShablastBot 1\.0|snap\.com beta crawler v0|Snapbot/1\.0|Snapbot/1\.0 \(Snap Shots&#44; \+http\://www\.snap\.com\)|sogou develop spider|Sogou Orion spider/3\.0\(\+http\://www\.sogou\.com/docs/help/webmasters\.htm#07\)|sogou spider|Sogou web spider/3\.0\(\+http\://www\.sogou\.com/docs/help/webmasters\.htm#07\)|sohu agent|SSurf15a 11 |TSurf15a 11|Under the Rainbow 2\.2|User-Agent\: Mozilla/4\.0 \(compatible; MSIE 6\.0; Windows NT 5\.1\)|VadixBot|WebVulnCrawl\.unknown/1\.0 libwww-perl/5\.803|Wells Search II|WEP Search 00
failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*"(?:%(badbots)s|%(badbotscustom)s)"$
ignoreregex =
datepattern = ^[^\[]*\[({DATE})
{^LN-BEG}
# DEV Notes:
# List of bad bots fetched from http://www.user-agents.org
# Generated on Thu Nov 7 14:23:35 PST 2013 by files/gen_badbots.
#
# Author: Yaroslav Halchenko
@@ -0,0 +1,25 @@
# Fail2Ban filter to match web requests for selected URLs that don't exist
#
[INCLUDES]
# Load regexes for filtering
before = botsearch-common.conf
[Definition]
failregex = ^<HOST> \- \S+ \[\] \"(GET|POST|HEAD) \/<block> \S+\" 404 .+$
^ \[error\] \d+#\d+: \*\d+ (\S+ )?\"\S+\" (failed|is not found) \(2\: No such file or directory\), client\: <HOST>\, server\: \S*\, request: \"(GET|POST|HEAD) \/<block> \S+\"\, .*?$
ignoreregex =
datepattern = {^LN-BEG}%%ExY(?P<_sep>[-/.])%%m(?P=_sep)%%d[T ]%%H:%%M:%%S(?:[.,]%%f)?(?:\s*%%z)?
^[^\[]*\[({DATE})
{^LN-BEG}
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
# DEV Notes:
# Based on apache-botsearch filter
#
# Author: Frantisek Sumsal
@@ -0,0 +1,32 @@
# Generic nginx error_log configuration items (to be used as interpolations) in other
# filters monitoring nginx error-logs
#
[DEFAULT]
# Type of log-file resp. log-format (file, short, journal):
logtype = file
# Daemon definition is to be specialized (if needed) in .conf file
_daemon = nginx
# Common line prefixes (beginnings) which could be used in filters
#
# [bsdverbose]? [hostname] [vserver tag] daemon_id spaces
#
# This can be optional (for instance if we match named native log files)
__prefix = <lt_<logtype>/__prefix>
__err_type = error
__prefix_line = %(__prefix)s\[%(__err_type)s\] \d+#\d+: \*\d+\s+
[lt_file]
__prefix = \s*
[lt_short]
__prefix = \s*(?:(?!\[)\S+ %(_daemon)s\[\d+\]: [^\[]*)?
[lt_journal]
__prefix = %(lt_short/__prefix)s
@@ -0,0 +1,43 @@
# fail2ban filter configuration for nginx
[INCLUDES]
before = nginx-error-common.conf
[Definition]
mode = normal
__err_type = <_ertp-<mode>>
_ertp-auth = error
mdre-auth = ^%(__prefix_line)suser "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$
_ertp-fallback = crit
mdre-fallback = ^%(__prefix_line)sSSL_do_handshake\(\) failed \(SSL: error:\S+(?: \S+){1,3} too (?:long|short)\)[^,]*, client: <HOST>
_ertp-normal = %(_ertp-auth)s
mdre-normal = %(mdre-auth)s
_ertp-aggressive = (?:%(_ertp-auth)s|%(_ertp-fallback)s)
mdre-aggressive = %(mdre-auth)s
%(mdre-fallback)s
failregex = <mdre-<mode>>
ignoreregex =
datepattern = {^LN-BEG}
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
# DEV NOTES:
# mdre-auth:
# Based on samples in https://github.com/fail2ban/fail2ban/pull/43/files
# Extensive search of all nginx auth failures not done yet.
#
# Author: Daniel Black
# mdre-fallback:
# Ban people checking for TLS_FALLBACK_SCSV repeatedly
# https://stackoverflow.com/questions/28010492/nginx-critical-error-with-ssl-handshaking/28010608#28010608
# Author: Stephan Orlowsky
@@ -0,0 +1,56 @@
# Fail2ban filter configuration for nginx :: limit_req
# used to ban hosts, that were failed through nginx by limit request processing rate
#
# Author: Serg G. Brester (sebres)
#
# To use 'nginx-limit-req' filter you should have `ngx_http_limit_req_module`
# and define `limit_req` and `limit_req_zone` as described in nginx documentation
# http://nginx.org/en/docs/http/ngx_http_limit_req_module.html
#
# Example:
#
# http {
# ...
# limit_req_zone $binary_remote_addr zone=lr_zone:10m rate=1r/s;
# ...
# # http, server, or location:
# location ... {
# limit_req zone=lr_zone burst=1 nodelay;
# ...
# }
# ...
# }
# ...
#
[INCLUDES]
before = nginx-error-common.conf
[Definition]
# Specify following expression to define exact zones, if you want to ban IPs limited
# from specified zones only.
# Example:
#
# ngx_limit_req_zones = lr_zone|lr_zone2
#
ngx_limit_req_zones = [^"]+
# Depending on limit_req_log_level directive (may be: info | notice | warn | error):
__err_type = [a-z]+
# Use following full expression if you should range limit request to specified
# servers, requests, referrers etc. only :
#
# failregex = ^%(__prefix_line)slimiting requests, excess: [\d\.]+ by zone "(?:%(ngx_limit_req_zones)s)", client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(, referrer: "\S+")?\s*$
# Shortly, much faster and stable version of regexp:
failregex = ^%(__prefix_line)slimiting requests, excess: [\d\.]+ by zone "(?:%(ngx_limit_req_zones)s)", client: <HOST>,
ignoreregex =
datepattern = {^LN-BEG}
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
@@ -0,0 +1,5 @@
[Definition]
failregex = ^<HOST> -.*GET .*/~.*
ignoreregex =
@@ -0,0 +1,5 @@
[Definition]
failregex = ^<HOST> -.*GET http.*
ignoreregex =
@@ -0,0 +1,6 @@
[Definition]
failregex = ^<HOST> -.*GET.*(\.php|\.asp|\.exe|\.pl|\.cgi|\.scgi)
ignoreregex =
@@ -0,0 +1,3 @@
[Definition]
failregex = ^<HOST>.*"(GET|POST).*" 401 .*$
ignoreregex =
+16
View File
@@ -0,0 +1,16 @@
[INCLUDES]
before = common.conf
[Definition]
_daemon = openvpn
# TLS handshake échouée et AUTH_FAILED
# Format log : <ip>:<port> TLS Error: ...
# Compatible avec log-append et syslog
failregex = ^%(__prefix_line)s<HOST>:\d+ TLS Error: TLS key negotiation failed
^%(__prefix_line)s<HOST>:\d+ TLS Error: TLS handshake failed
^%(__prefix_line)s<HOST>:\d+ AUTH_FAILED
^%(__prefix_line)s<HOST>:\d+ Connection reset, restarting
ignoreregex =
@@ -0,0 +1,16 @@
[INCLUDES]
before = common.conf
[Definition]
_daemon = (?:fail2ban(?:-server|\.actions)\s*)
_jailname = recidive
failregex = ^%(__prefix_line)s(?:\s*fail2ban\.actions\s*%(__pid_re)s?:\s+)?NOTICE\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$
datepattern = ^{DATE}
# Ne pas traiter les bans émis par les jails recidive eux-mêmes
ignoreregex = \[recidive.*\]\s+Ban\s+<HOST>
journalmatch = _SYSTEMD_UNIT=fail2ban.service PRIORITY=5
@@ -0,0 +1,48 @@
# GABARIT réutilisable pour protéger une NOUVELLE appli contre le
# brute-force applicatif — n'importe quelle appli sur ce VPS, pas
# seulement ce projet (trouvé utile après un résidu de jail appartenant à
# une autre appli Django sur ce même serveur partagé, cf. CONTEXT.md).
# Ce fichier n'est PAS déployé tel quel (extension .template, ignorée par
# la boucle de copie générique d'install.sh::deploy_fail2ban) : c'est un
# modèle à copier et adapter. Exemple concret déjà en place :
# emitter-admin-auth.conf + emitter/banevents/signals.py (Django +
# django-axes).
#
# Mode d'emploi :
# 1. Faire journaliser à l'appli une ligne EXACTEMENT au format attendu
# par filter.d/app-auth.conf (générique, partagé par toutes les
# jails app-auth-*, rien à écrire côté filtre) :
# Verrouillage <NOM_APPLI> après échecs répétés depuis <IP> (...)
# <NOM_APPLI> = un seul mot (\S+ côté filtre), ex. "émetteur",
# "webmail". L'appli décide QUAND verrouiller (son propre seuil
# applicatif, cf. AXES_FAILURE_LIMIT pour l'exemple django-axes) —
# fail2ban ne fait que réagir à cette décision déjà prise (d'où
# maxretry=1 ci-dessous : chaque ligne EST déjà la décision, pas la
# peine de recompter un 2e seuil derrière).
# IMPORTANT — timestamp en tête de ligne : marquer explicitement le
# fuseau (ex. suffixe "Z" ISO 8601, UTC), voir le commentaire dans
# filter.d/app-auth.conf pour le pourquoi (piège vécu avec Django,
# TIME_ZONE='UTC' + time.tzset() : sans marqueur, fail2ban ignore
# silencieusement les lignes fraîches juste après son propre
# redémarrage si le fuseau système du VPS diffère d'UTC).
# 2. Copier ce fichier vers jail.d/<nom-appli>-auth.conf, remplacer
# __APP_NAME__ (nom de la jail, unique) et __APP_AUTH_LOGPATH__
# (chemin réel du fichier de log).
# 3. Si ce chemin dépend du VPS (répertoire de déploiement différent
# d'un serveur à l'autre — le cas courant) : NE PAS le laisser dans
# la boucle de copie générique d'install.sh::deploy_fail2ban (même
# piège que coturn.conf/emitter-admin-auth.conf, voir CONTEXT.md).
# Ajouter plutôt une fonction deploy_<nom>_jail dédiée (sed sur le
# placeholder), sur le modèle de deploy_emitter_admin_auth_jail.
# 4. S'assurer que le fichier de log existe déjà (touch) avant le
# premier démarrage de fail2ban après déploiement — un logpath
# inexistant fait planter fail2ban au chargement (incident coturn,
# voir ROADMAP.md Phase 1.b).
[__APP_NAME__-auth]
enabled = true
backend = auto
filter = app-auth
logpath = __APP_AUTH_LOGPATH__
maxretry = 1
findtime = 1h
bantime = 24h
+19
View File
@@ -0,0 +1,19 @@
# logpath : chemin réel du log coturn sur CE VPS, substitué par install.sh
# (deploy_fail2ban) depuis "log-file=" dans /etc/turnserver.conf s'il existe
# — jamais une valeur codée en dur partagée entre noeuds. Un chemin diffère
# réellement d'un VPS à l'autre selon la config coturn locale (vécu :
# /var/log/coturn/turnserver.log sur l'un, /var/log/turnserver.log sur
# l'autre, même paquet Debian). Si coturn n'est pas installé sur ce noeud,
# install.sh désactive cette jail plutôt que de laisser un chemin invalide
# faire planter fail2ban au démarrage ("Have not found any log file").
[coturn]
enabled = true
backend = auto
filter = coturn
logpath = __COTURN_LOGPATH__
port = 3478
protocol = tcp
banaction = f2b-iptables-allports
maxretry = 5
findtime = 5m
bantime = 24h
+18
View File
@@ -0,0 +1,18 @@
# Désactivée (2026-07-17) : ne protège rien de réel. Le filtre référencé
# (filter.d/django-auth.conf) n'existe pas dans ce dépôt — sur le VPS où
# ce jail.d a été trouvé déployé, un fichier du même nom existe bien, mais
# c'est un résidu appartenant à une autre appli Django hébergée sur ce VPS
# partagé (pas ce projet), et logpath (/var/log/django/*.log) pointe vers
# un fichier vide que rien n'alimente. Notre propre émetteur Django
# (emitter/) n'a aucune config LOGGING et n'écrit jamais là — l'ancien
# commentaire "bloqué sur Phase 2" n'a plus de sens, Phase 2 est terminée
# depuis longtemps et ne visait de toute façon pas ce fichier. À réactiver
# seulement avec un vrai filtre + logpath réels si un jour ce projet doit
# protéger la connexion admin de l'émetteur lui-même.
[django-auth]
enabled = false
backend = auto
port = http,https
logpath = /var/log/django/*.log
maxretry = 3
bantime = 1h
@@ -0,0 +1,21 @@
# logpath : chemin réel du log axes sur CE VPS (emitter/logs/admin-auth.log
# dans le checkout), substitué par install.sh (deploy_fail2ban) — jamais
# une valeur codée en dur, le checkout vit sous un $HOME différent par VPS
# (même piège que coturn.conf/master-sync : voir CONTEXT.md).
#
# maxretry=1 : chaque ligne journalisée EST déjà la décision de
# verrouillage d'axes (AXES_FAILURE_LIMIT, cf. config/settings/base.py) —
# fail2ban n'a pas à recompter un second seuil, il bannit dès la première
# occurrence.
#
# filter = app-auth (générique, pas emitter-admin-auth) : premier
# consommateur du gabarit réutilisable par toute appli du VPS, voir
# filter.d/app-auth.conf et jail.d/app-auth.conf.template.
[emitter-admin-auth]
enabled = true
backend = auto
filter = app-auth
logpath = __EMITTER_ADMIN_AUTH_LOGPATH__
maxretry = 1
findtime = 1h
bantime = 24h
@@ -0,0 +1,15 @@
# Jail dédiée à la commande ESCALATE (Phase 4) — jamais déclenchée par un
# log, uniquement par injection manuelle (fail2ban-client set banip)
# depuis master_client.py sur réception d'un ESCALATE (auto-publié par
# master_listen.py::correlation_rule_escalate quand une IP est bannie
# indépendamment sur plusieurs noeuds distincts, cf. ROADMAP.md).
# banaction = f2b-iptables-allports, bantime = -1 (permanent) : contraire
# à master-sync (24h), une IP qui frappe plusieurs noeuds indépendamment
# n'a pas droit à un simple ban temporaire.
[master-escalate]
enabled = true
filter = master-sync
logpath = /var/log/fail2ban.log
banaction = f2b-iptables-allports
bantime = -1
maxretry = 999999
@@ -0,0 +1,13 @@
# Jail dédiée à la commande RATE_LIMIT (Phase 4) — jamais déclenchée par
# un log, uniquement par injection manuelle (fail2ban-client set banip)
# depuis master_client.py sur réception de RATE_LIMIT (déclenchée à la
# main via manage.py publish_command, cf. master-sync.conf pour le même
# schéma). banaction = f2b-iptables-hashlimit : throttle (10 req/s, burst
# 20) au lieu d'un blocage total — cas limite, faux positifs probables.
[master-ratelimit]
enabled = true
filter = master-sync
logpath = /var/log/fail2ban.log
banaction = f2b-iptables-hashlimit
bantime = 1h
maxretry = 999999
+12
View File
@@ -0,0 +1,12 @@
# Jail dédiée à la propagation SYNC_BAN reçue du master (Phase 3/4) —
# jamais déclenchée par un log, seulement par injection manuelle
# (fail2ban-client set master-sync banip <ip>) depuis master_client.py.
# banaction = f2b-iptables-allports : un SYNC_BAN bloque l'IP sur tous les
# ports, cohérent avec la menace globale qu'il signale (vue ailleurs).
[master-sync]
enabled = true
filter = master-sync
logpath = /var/log/fail2ban.log
banaction = f2b-iptables-allports
bantime = 1d
maxretry = 999999
+13
View File
@@ -0,0 +1,13 @@
[mosquitto]
enabled = true
backend = auto
filter = mosquitto
logpath = /var/log/mosquitto/mosquitto.log
port = 8883,8884
protocol = tcp
banaction = f2b-iptables-multiport
# maxretry élevé car les clients MQTT se reconnectent légitimement
# Réduire à 5 si tu utilises l'approche B (plugin avec IP sur échec auth)
maxretry = 15
findtime = 1m
bantime = 1h
+76
View File
@@ -0,0 +1,76 @@
[nginx-http-auth]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/*error.log
[nginx-req-limit]
enabled = true
backend = auto
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/*error.log
findtime = 30s
maxretry = 5
bantime = 10m
[nginx-botsearch]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/*error.log
[nginx-noscript]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/*access.log
maxretry = 3
findtime = 5m
bantime = 15m
[nginx-unauthorized]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/*access.log
maxretry = 3
findtime = 5m
bantime = 15m
[nginx-badbots]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/*access.log
maxretry = 1
findtime = 1h
bantime = 24h
[nginx-nohome]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/*access.log
maxretry = 5
findtime = 5m
bantime = 15m
[nginx-noproxy]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/*access.log
maxretry = 3
findtime = 5m
bantime = 15m
# Activer si trop de 404/403 en dehors des autres filtres
[nginx-4xx]
enabled = false
backend = auto
port = http,https
logpath = /var/log/nginx/*.log
maxretry = 10
findtime = 5m
bantime = 15m
+14
View File
@@ -0,0 +1,14 @@
[openvpn]
enabled = true
backend = auto
filter = openvpn
# Adapter le chemin selon la config OpenVPN (log ou log-append)
logpath = /var/log/openvpn/openvpn.log
/var/log/openvpn.log
port = 41194
protocol = udp
# Bannir tous les ports (pas seulement VPN) car la menace est globale
banaction = f2b-iptables-allports
maxretry = 3
findtime = 5m
bantime = 24h
+25
View File
@@ -0,0 +1,25 @@
# Récidive — escalade automatique des bans
# Les jails recidive-* sont exclus de la détection croisée par le filtre
# (ignoreregex = \[recidive.*\])
# Niveau 1 : 3 bans en 24h → 24h de ban
[recidive]
enabled = true
filter = recidive-filter
logpath = /var/log/fail2ban.log
action = f2b-iptables-allports
f2b-mqtt-action-banisher
findtime = 1d
bantime = 1d
maxretry = 3
# Niveau 2 : 3 bans en 1 semaine → ban permanent
[recidive-hard]
enabled = true
filter = recidive-filter
logpath = /var/log/fail2ban.log
action = f2b-iptables-allports
f2b-mqtt-action-banisher
findtime = 1w
bantime = -1
maxretry = 3
+15
View File
@@ -0,0 +1,15 @@
[sshd]
enabled = true
port = 22
maxretry = 3
bantime = 24h
[pam-generic]
enabled = true
maxretry = 3
# facility 10 = auth/security (PAM, su, sudo…)
journalmatch = SYSLOG_FACILITY=10
[mysqld-auth]
enabled = true
journalmatch = _SYSTEMD_UNIT=mariadb.service
+29
View File
@@ -0,0 +1,29 @@
[DEFAULT]
backend = systemd
usedns = no
# Explicite (sinon fail2ban logue un WARNING par jail : "'allowipv6' not
# defined ... Using default one: 'auto'"). Cohérent avec le pare-feu, qui
# désactive IPv6 partout (sysctl disable_ipv6=1, ip6tables en DROP total,
# voir generic/firewall/firewall-launcher.sh) : rien à laisser matcher.
allowipv6 = false
# IPs exclues : loopback, VPN clients, réseau local
ignoreip = 127.0.0.0/8 10.8.0.0/24 192.168.250.0/24 192.168.1.0/24
findtime = 5m
maxretry = 3
bantime = 1h
# banaction en ligne unique — le port est passé explicitement dans action (voir ci-dessous)
banaction = f2b-iptables-multiport
banaction_allports = f2b-iptables-allports
# Passage explicite de port/protocol/chain pour éviter le défaut port=ssh (22)
# du [Init] de l'action. %(port)s est résolu dans le contexte de chaque jail.
action = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
f2b-mqtt-action-banisher[port="%(port)s", protocol="%(protocol)s"]
destemail = postmaster@domain.net
sendername = Fail2Ban
mta = mail
+10
View File
@@ -0,0 +1,10 @@
; Identifiants MQTT du client fail2ban-mqtt-action-banisher.
; Copier ce fichier vers /etc/fail2ban/mqtt.conf (hors dépôt git, cf. .gitignore)
; et y renseigner les vraies valeurs. install.sh le fait automatiquement s'il
; est absent, sans écraser un fichier existant.
[mqtt]
host = 127.0.0.1
port = 1883
username = fail2ban
password = changeme
+93
View File
@@ -0,0 +1,93 @@
# firewall-filters.conf — règles spécifiques aux services
# Chargé par fw.sh après les protections de base.
# Variables disponibles : $ETH_IF, $VPN_IF, $WG_IF
IP="x.x.x.x"
VPN_NET="10.8.0.0/24"
VPN_PORT="1194"
WG_NET="10.10.0.0/24"
WG_PORT="51820"
WG_WEB_PORT="51821"
PORTAINER_PORT="9000"
PORTAINER_SSL_PORT="9443"
SSH_PORT="22"
echo " [Services : SSH / HTTP / OpenVPN / Coturn / MQTT]"
# ─── SSH ──────────────────────────────────────────────────────────────────────
iptables -A INPUT -i $ETH_IF -p tcp --dport $SSH_PORT \
-m conntrack --ctstate NEW \
-m connlimit --connlimit-above 4 --connlimit-mask 32 -j LOG_DROP
iptables -A INPUT -i $ETH_IF -p tcp --dport $SSH_PORT \
-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# ─── HTTP / HTTPS ─────────────────────────────────────────────────────────────
iptables -A INPUT -i $ETH_IF -p tcp --dport 80 \
-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
iptables -A INPUT -i $ETH_IF -p tcp --dport 443 \
-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# ─── MOSQUITTO TLS (accessible depuis internet) ───────────────────────────────
iptables -A INPUT -i $ETH_IF -p tcp --dport 8883 \
-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
#iptables -A INPUT -i $ETH_IF -p tcp --dport 8884 \
# -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# ─── COTURN ───────────────────────────────────────────────────────────────────
# Signaling STUN/TURN
iptables -A INPUT -i $ETH_IF -p udp --dport 3478 \
-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
iptables -A INPUT -i $ETH_IF -p tcp --dport 3478 \
-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# Relai TURN UDP — hashlimit par IP source (anti-amplification)
iptables -A INPUT -i $ETH_IF -p udp --dport 49152:65535 \
-m hashlimit --hashlimit-name coturn-relay \
--hashlimit-upto 100/s --hashlimit-burst 200 \
--hashlimit-mode srcip -j ACCEPT
# ─── REDIS : DROP explicite depuis internet ────────────────────────────────────
# Redis doit rester sur 127.0.0.1 ou socket Unix — cette règle est un filet de sécurité
iptables -A INPUT -i $ETH_IF -p tcp --dport 6379 -j DROP
# ─── Transmission (décommenter si actif) ──────────────────────────────────────
#iptables -A INPUT -i $ETH_IF -p tcp --dport 51413 \
# -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# ─── MAIL (décommenter si besoin) ─────────────────────────────────────────────
#iptables -A INPUT -i $ETH_IF -p tcp --dport 465 \
# -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
#iptables -A INPUT -i $ETH_IF -p tcp --dport 993 \
# -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# ─── WIREGUARD ──────────────────────────────────────────────────────────────────
#iptables -A INPUT -i $ETH_IF -p udp --dport $WG_PORT \
# -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
#iptables -I INPUT -i $WG_IF -p tcp --dport $WG_WEB_PORT -j ACCEPT
#iptables -I INPUT -i $WG_IF -p tcp --dport $PORTAINER_PORT -j ACCEPT
#iptables -I INPUT -i $WG_IF -p tcp --dport $PORTAINER_SSL_PORT -j ACCEPT
# ─── DOCKER USER ──────────────────────────────────────────────────────────────────
iptables -I DOCKER-USER -i $ETH_IF -j DROP
iptables -I DOCKER-USER -i $WG_IF -j RETURN # VPN autorisé
iptables -I DOCKER-USER -i lo -j RETURN # loopback autorisé
# ─── OPENVPN ──────────────────────────────────────────────────────────────────
iptables -A INPUT -i $ETH_IF -p udp --dport $VPN_PORT \
-m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# Accès complet au serveur depuis les clients VPN
iptables -A INPUT -i $VPN_IF -j ACCEPT
iptables -A OUTPUT -o $VPN_IF -j ACCEPT
# ─── FORWARD VPN → internet ───────────────────────────────────────────────────
iptables -A FORWARD -i $VPN_IF -o $ETH_IF \
-m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i $ETH_IF -o $VPN_IF \
-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -s $VPN_NET -o $ETH_IF -j MASQUERADE
+291
View File
@@ -0,0 +1,291 @@
#!/bin/bash
# Firewall - v2.0 (2025)
# Debian 13 / iptables / fail2ban / OpenVPN / Mosquitto / Nginx
# ─── CONFIGURATION ────────────────────────────────────────────────────────────
ETH_IF="${ETH_IF:-eth0}" # valeur réelle fournie par .env (EnvironmentFile=, voir firewall.service) ; eth0 = filet de sécurité si absent
VPN_IF="${VPN_IF:-tun0}"
WG_IF="${WG_IF:-wg0}"
LAN_NET="${LAN_NET:-}" # voir section ANTI-SPOOF plus bas
FILTERS="/etc/firewall/firewall-filters.conf"
BAN_FILE="/etc/firewall/ban_ip.txt"
SYSCTL_FILE="/etc/sysctl.d/90-firewall.conf"
LOG_LIMIT="5/min"
LOG_BURST=10
IPT="iptables"
IP6T="ip6tables"
# ──────────────────────────────────────────────────────────────────────────────
fw_sysctl() {
cat > "$SYSCTL_FILE" << 'EOF'
# Anti-spoofing (Reverse Path Filtering)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# ICMP
net.ipv4.icmp_echo_ignore_all = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
# SYN cookies (protection SYN flood)
net.ipv4.tcp_syncookies = 1
# Pas de redirections ICMP ni de source routing
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
# Forwarding activé pour OpenVPN
net.ipv4.ip_forward = 1
# Timeouts anti-DoS (sans casser les perfs modernes)
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 1800
# NE PAS désactiver window_scaling ni sack : indispensables sur réseaux modernes
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_sack = 1
net.ipv4.ip_default_ttl = 64
# IPv6 — désactivé (adapter si utilisé)
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOF
sysctl -p "$SYSCTL_FILE" -q
echo " [sysctl appliqué depuis $SYSCTL_FILE]"
}
fw_sysctl_restore() {
cat > "$SYSCTL_FILE" << 'EOF'
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.ip_forward = 0
net.ipv4.tcp_fin_timeout = 60
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_sack = 1
net.ipv4.ip_default_ttl = 64
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
EOF
sysctl -p "$SYSCTL_FILE" -q
}
case "$1" in
start)
echo " [sysctl]"
fw_sysctl
echo " [FLUSH]"
$IPT -F
$IPT -X
$IPT -t nat -F
$IPT -t nat -X
$IPT -t mangle -F
$IPT -t mangle -X
echo " [POLITIQUES PAR DEFAUT : DROP]"
$IPT -P INPUT DROP
$IPT -P OUTPUT DROP
$IPT -P FORWARD DROP
# ─── CHAÎNES DE LOGGING ───────────────────────────────────────────────────
echo " [CHAÎNES LOG]"
$IPT -N LOG_DROP
$IPT -A LOG_DROP -m limit --limit $LOG_LIMIT --limit-burst $LOG_BURST \
-j LOG --log-prefix '[FW DROP] ' --log-level warning
$IPT -A LOG_DROP -j DROP
$IPT -N LOG_INVALID
$IPT -A LOG_INVALID -m limit --limit $LOG_LIMIT --limit-burst $LOG_BURST \
-j LOG --log-prefix '[FW INVALID] ' --log-level warning
$IPT -A LOG_INVALID -j DROP
$IPT -N LOG_SPOOF
$IPT -A LOG_SPOOF -m limit --limit $LOG_LIMIT --limit-burst $LOG_BURST \
-j LOG --log-prefix '[FW SPOOF] ' --log-level warning
$IPT -A LOG_SPOOF -j DROP
$IPT -N LOG_SCAN
$IPT -A LOG_SCAN -m limit --limit $LOG_LIMIT --limit-burst $LOG_BURST \
-j LOG --log-prefix '[FW SCAN] ' --log-level warning
$IPT -A LOG_SCAN -j DROP
$IPT -N LOG_FLOOD
$IPT -A LOG_FLOOD -m limit --limit $LOG_LIMIT --limit-burst $LOG_BURST \
-j LOG --log-prefix '[FW FLOOD] ' --log-level warning
$IPT -A LOG_FLOOD -j DROP
# ─── FAIL2BAN ─────────────────────────────────────────────────────────────
# Pré-création des chaînes fail2ban pour éviter les erreurs si fw démarre
# avant fail2ban. fail2ban les vide et les recrée à son démarrage.
echo " [FAIL2BAN : pré-création des chaînes]"
for chain in fail2ban-INPUT fail2ban-sshd fail2ban-nginx fail2ban-mosquitto; do
$IPT -N $chain 2>/dev/null || true
$IPT -F $chain 2>/dev/null || true
$IPT -A $chain -j RETURN
done
$IPT -I INPUT -j fail2ban-INPUT
# ─── LOOPBACK ─────────────────────────────────────────────────────────────
echo " [LOOPBACK]"
$IPT -A INPUT -i lo -j ACCEPT
$IPT -A OUTPUT -o lo -j ACCEPT
# Anti-spoof loopback depuis l'extérieur
$IPT -A INPUT ! -i lo -s 127.0.0.0/8 -j LOG_SPOOF
# ─── PAQUETS INVALIDES ────────────────────────────────────────────────────
echo " [PAQUETS INVALIDES]"
$IPT -A INPUT -m conntrack --ctstate INVALID -j LOG_INVALID
$IPT -A FORWARD -m conntrack --ctstate INVALID -j DROP
$IPT -A OUTPUT -m conntrack --ctstate INVALID -j DROP
# ─── CONNEXIONS ÉTABLIES ──────────────────────────────────────────────────
echo " [CONNEXIONS ETABLIES/LIEES]"
$IPT -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
$IPT -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# ─── ICMP ─────────────────────────────────────────────────────────────────
echo " [ICMP]"
# Ping entrant : limité par source (hashlimit > limit : par IP)
$IPT -A INPUT -p icmp --icmp-type echo-request \
-m hashlimit --hashlimit-name icmp-in --hashlimit-upto 5/s \
--hashlimit-burst 10 --hashlimit-mode srcip -j ACCEPT
# Types ICMP utiles en entrée
$IPT -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
$IPT -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
$IPT -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
$IPT -A OUTPUT -p icmp -j ACCEPT
# ─── ANTI-SPOOF (martians) ────────────────────────────────────────────────
# Écrit pour une IP publique dédiée (VPS) : toute IP source privée vue
# sur $ETH_IF y est forcément usurpée. Sur un noeud derrière une box
# résidentielle, $ETH_IF est aussi l'unique interface LAN — du trafic
# local légitime (192.168.x.x, parfois l'IP de la box elle-même via
# son NAT loopback/hairpin) y arrive normalement et ne doit pas être
# traité comme du spoofing. LAN_NET (vide par défaut, donc aucun
# changement de comportement pour un VPS) exempte ce sous-réseau
# avant la boucle ci-dessous — à ne définir que sur un noeud dans
# cette situation (ex. LAN_NET=192.168.1.0/24 dans .env).
if [ -n "$LAN_NET" ]; then
echo " [ANTI-SPOOF : LAN_NET=$LAN_NET exempté (trafic local légitime)]"
$IPT -A INPUT -i "$ETH_IF" -s "$LAN_NET" -j ACCEPT
fi
echo " [ANTI-SPOOF / MARTIANS]"
for net in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 \
169.254.0.0/16 224.0.0.0/4 240.0.0.0/5 \
0.0.0.0/8 127.0.0.0/8; do
$IPT -A INPUT -i "$ETH_IF" -s $net -j LOG_SPOOF
done
# ─── FLAGS TCP INVALIDES ──────────────────────────────────────────────────
echo " [FLAGS TCP INVALIDES]"
$IPT -A INPUT -p tcp --tcp-flags ALL NONE -j LOG_SCAN # NULL scan
$IPT -A INPUT -p tcp --tcp-flags ALL ALL -j LOG_SCAN # XMAS scan
$IPT -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j LOG_SCAN
$IPT -A INPUT -p tcp --tcp-flags SYN,RST SYN,RST -j LOG_SCAN
$IPT -A INPUT -p tcp --tcp-flags FIN,RST FIN,RST -j LOG_SCAN
$IPT -A INPUT -p tcp --tcp-flags ACK,FIN FIN -j LOG_SCAN
$IPT -A INPUT -p tcp --tcp-flags ACK,PSH PSH -j LOG_SCAN
$IPT -A INPUT -p tcp --tcp-flags ACK,URG URG -j LOG_SCAN
# ─── SYN FLOOD ────────────────────────────────────────────────────────────
echo " [SYN FLOOD]"
$IPT -N syn-flood
$IPT -A INPUT -i "$ETH_IF" -p tcp --syn -j syn-flood
$IPT -A syn-flood -m limit --limit 25/s --limit-burst 50 -j RETURN
$IPT -A syn-flood -j LOG_FLOOD
# ─── FRAGMENTS ────────────────────────────────────────────────────────────
echo " [FRAGMENTS]"
$IPT -A INPUT -i "$ETH_IF" -f -j LOG_DROP
# ─── PORTS BRUITÉS : drop silencieux (évite de saturer les logs) ──────────
echo " [DROP SILENCIEUX : NetBIOS / MSSQL / RPC / BitTorrent]"
for port in 135 137 138 139 445 1433 1025 1026 1027; do
$IPT -A INPUT -i "$ETH_IF" -p tcp --dport $port -j DROP
$IPT -A INPUT -i "$ETH_IF" -p udp --dport $port -j DROP
done
$IPT -A INPUT -i "$ETH_IF" -p udp --dport 1434 -j DROP
$IPT -A INPUT -i "$ETH_IF" -p tcp --dport 6881:6889 -j DROP
$IPT -A INPUT -i "$ETH_IF" -p udp --dport 6881:6889 -j DROP
# ─── RÈGLES SERVICES ──────────────────────────────────────────────────────
echo " [FILTRAGE NET VERS SERVEUR]"
# shellcheck disable=SC1090 # chemin résolu au déploiement (install.sh), pas statique
[ -f "$FILTERS" ] && source "$FILTERS"
# ─── SORTIE SERVEUR ───────────────────────────────────────────────────────
echo " [SORTIE SERVEUR VERS NET]"
$IPT -A OUTPUT -o "$ETH_IF" -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT
# ─── BANS IP ──────────────────────────────────────────────────────────────
if [ -f "$BAN_FILE" ]; then
echo " [BANS IP]"
# shellcheck disable=SC1090 # chemin résolu au déploiement (install.sh), pas statique
. "$BAN_FILE"
fi
# ─── IPv6 ─────────────────────────────────────────────────────────────────
echo " [IPv6 : blocage total]"
$IP6T -F 2>/dev/null || true
$IP6T -X 2>/dev/null || true
$IP6T -P INPUT DROP 2>/dev/null || true
$IP6T -P OUTPUT DROP 2>/dev/null || true
$IP6T -P FORWARD DROP 2>/dev/null || true
$IP6T -A INPUT -i lo -j ACCEPT 2>/dev/null || true
$IP6T -A OUTPUT -o lo -j ACCEPT 2>/dev/null || true
# ─── DROP FINAL ───────────────────────────────────────────────────────────
echo " [DROP FINAL]"
$IPT -A FORWARD -j DROP
$IPT -A INPUT -j LOG_DROP
$IPT -A OUTPUT -j LOG_DROP
echo " [FIREWALL ACTIF]"
;;
status)
echo "=== IPv4 filter ==="
$IPT -vn -L
echo ""
echo "=== IPv4 nat ==="
$IPT -vn -L -t nat
echo ""
echo "=== IPv6 ==="
$IP6T -vn -L 2>/dev/null || echo "(ip6tables non disponible ou IPv6 désactivé)"
;;
stop)
echo " [ARRÊT FIREWALL]"
for t in filter nat mangle raw; do
$IPT -t $t -F 2>/dev/null
$IPT -t $t -X 2>/dev/null
for p in INPUT OUTPUT FORWARD PREROUTING POSTROUTING; do
$IPT -t $t -P $p ACCEPT 2>/dev/null || true
done
done
fw_sysctl_restore
echo " [FIREWALL ARRÊTÉ - SYSTÈME OUVERT]"
;;
reload)
echo " [RELOAD]"
$0 stop
$0 start
;;
*)
echo "Usage: $0 {start|stop|reload|status}"
exit 1
;;
esac
exit 0
+25
View File
@@ -0,0 +1,25 @@
[Unit]
Description=Firewall iptables
# Démarre après la détection des interfaces réseau,
# mais avant que le trafic soit autorisé et avant fail2ban
After=network-pre.target
Before=network.target fail2ban.service
Wants=network-pre.target
# Si fail2ban redémarre, le firewall doit être actif en premier
PartOf=fail2ban.service
[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=__ENV_FILE__
ExecStart=/etc/firewall/firewall-launcher.sh start
ExecStop=/etc/firewall/firewall-launcher.sh stop
ExecReload=/etc/firewall/firewall-launcher.sh reload
# Sécurité du service lui-même
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# certbot-deploy-mosquitto.sh — copie le certificat Let's Encrypt vers un
# emplacement lisible par l'utilisateur système 'mosquitto' (non-root), et
# redémarre mosquitto pour qu'il recharge le nouveau certificat.
#
# /etc/letsencrypt est verrouillé root-only par certbot (le dossier lui-même,
# pas seulement privkey.pem) : mosquitto ne peut donc jamais lire les
# fichiers directement à leur emplacement d'origine, quels que soient leurs
# propres droits. D'où cette copie découplée, dans un répertoire que install.sh
# rend group-lisible pour 'mosquitto'.
#
# Installé par install.sh (install-master) comme deploy-hook certbot :
# /etc/letsencrypt/renewal-hooks/deploy/ — s'exécute automatiquement à
# chaque renouvellement. Peut aussi être lancé à la main.
set -euo pipefail
DOMAIN="__MQTT_MASTER_DOMAIN__"
SRC="/etc/letsencrypt/live/$DOMAIN"
DEST="/etc/mosquitto/master-tls"
install -d -o root -g mosquitto -m 750 "$DEST"
cp "$SRC/fullchain.pem" "$DEST/fullchain.pem"
cp "$SRC/privkey.pem" "$DEST/privkey.pem"
chown root:mosquitto "$DEST/fullchain.pem" "$DEST/privkey.pem"
chmod 640 "$DEST/fullchain.pem" "$DEST/privkey.pem"
systemctl restart mosquitto.service
+38
View File
@@ -0,0 +1,38 @@
# ACL du broker master — %u est le CN du certificat client (cf.
# use_identity_as_username dans mosquitto-master.conf). Chaque noeud ne peut
# publier que sous son propre topic de ban, et ne lire que ses propres
# commandes plus le canal de diffusion générale.
#
# Déployé par install.sh (install-master) vers /etc/mosquitto/master-acl.conf
pattern write fail2ban/%u/ban
# heartbeat (Phase 3, supervision) : chaque noeud publie périodiquement
# pour prouver qu'il est vivant, indépendamment de toute activité de ban.
# ack (Phase 2.b) : accusé de réception publié après exécution réelle d'une
# commande reçue du master (ex. SYNC_BAN), succès ou échec.
pattern write fail2ban/%u/heartbeat
pattern write fail2ban/%u/ack
pattern read fail2ban/%u/action
# Topic fixe (identique pour tous les noeuds) : "topic", pas "pattern" —
# "pattern" est réservé aux entrées substituées par client (%u/%c) et
# mosquitto avertit (pattern ignoré) si on l'utilise sans placeholder.
topic read fail2ban/broadcast/action
# Identité dédiée à l'ingestion/décision côté master
# (banevents.master_listen) : lecture de fail2ban/+/ban (TOUS les noeuds),
# et écriture sur le canal de diffusion (pour publier un SYNC_BAN) — deux
# droits plus larges que ceux d'un noeud pris individuellement (pattern
# ci-dessus, limité à fail2ban/%u/ban). "user <nom>" scope les règles
# suivantes à cette seule identité de certificat — généré via
# scripts/generate-node-cert.sh master-internal.
user master-internal
topic read fail2ban/+/ban
topic read fail2ban/+/heartbeat
topic read fail2ban/+/ack
topic write fail2ban/broadcast/action
# Roster périodique (Phase 3) : liste des noeuds connus + compteurs
# jail/pays globaux, republié localement par chaque noeud (NodeRegistry +
# cache Redis, voir banevents/stats.py) — sans ça la sidebar d'un noeud
# client n'aurait jamais que ses propres données.
topic write fail2ban/broadcast/roster
+41
View File
@@ -0,0 +1,41 @@
# Broker Mosquitto local — consommé par f2b-mqtt-action-banisher et par
# l'emetteur Django (Phase 2). Jamais exposé sur Internet : écoute en
# loopback, plus (optionnellement, cf. MQTT_VPN_LISTENER_IP dans .env) un
# listener additionnel sur le réseau VPN de confiance de CE noeud, pour
# permettre le développement/test de l'émetteur depuis un poste distant sur
# ce VPN. Cette IP est propre à chaque VPS (jamais une valeur partagée
# entre noeuds) — install.sh (deploy_mosquitto) remplace le marqueur
# ci-dessous à partir de .env, ou le retire entièrement si
# MQTT_VPN_LISTENER_IP est vide. NE PAS répéter ce marqueur ailleurs dans ce
# fichier, y compris en commentaire : la substitution est un remplacement
# global (gsub), toute autre occurrence littérale serait aussi affectée.
#
# Déployé par install.sh vers /etc/mosquitto/conf.d/mosquitto-local.conf
per_listener_settings true
# Avec per_listener_settings, allow_anonymous/password_file doivent être
# répétés sous chaque listener : ils ne s'appliquent qu'au listener sous
# lequel ils apparaissent, jamais rétroactivement aux précédents.
listener 1883 127.0.0.1
protocol mqtt
allow_anonymous false
password_file /etc/mosquitto/passwd_local
__VPN_LISTENER_BLOCK__
# Pas de persistance : les événements sont consommés en temps réel,
# aucun besoin de rejouer l'historique après un redémarrage du broker.
persistence false
# Pas de log_dest ici : /etc/mosquitto/mosquitto.conf (paquet Debian) en
# déclare déjà un ("log_dest file /var/log/mosquitto/mosquitto.log"),
# lu avant ce conf.d — en rajouter un second (même en syslog) fait
# planter mosquitto au chargement ("Duplicate 'log_dest file' value").
# C'est précisément ce fichier que surveille la jail fail2ban "mosquitto"
# (jail.d/mosquitto.conf) : si jamais un jour ce défaut du paquet
# disparaît/change, il faudra le redéclarer ici explicitement plutôt que
# supposer qu'il existe toujours.
log_type error
log_type warning
log_type notice
+33
View File
@@ -0,0 +1,33 @@
# Broker Mosquitto master (Phase 3) — écoute 8883, TLS obligatoire.
# Cohabite avec le listener local (mosquitto-local.conf) dans le même
# processus mosquitto : per_listener_settings s'applique par listener, pas
# de conflit entre les deux fichiers de conf.d.
#
# Deux chaînes de confiance distinctes :
# - certfile/keyfile : certificat Let's Encrypt du domaine __MQTT_MASTER_DOMAIN__
# (identité TLS présentée par le serveur) — copié par
# certbot-deploy-mosquitto.sh vers /etc/mosquitto/master-tls/, jamais lu
# directement dans /etc/letsencrypt/ (verrouillé root-only par certbot,
# illisible pour l'utilisateur système 'mosquitto')
# - cafile : CA privée dédiée (scripts/master-ca-init.sh), qui NE SERT QU'À
# vérifier les certificats clients des noeuds (mTLS) — Let's Encrypt ne
# signe pas de certificats clients.
#
# Déployé par install.sh (install-master) vers /etc/mosquitto/conf.d/mosquitto-master.conf
listener 8883
protocol mqtt
cafile /etc/mosquitto/master-ca/ca.crt
certfile /etc/mosquitto/master-tls/fullchain.pem
keyfile /etc/mosquitto/master-tls/privkey.pem
tls_version tlsv1.2
# mTLS : le certificat client est obligatoire, et son CN devient l'identité
# MQTT du noeud (exploité par master-acl.conf pour restreindre chaque noeud
# à ses propres topics).
require_certificate true
use_identity_as_username true
allow_anonymous false
acl_file /etc/mosquitto/master-acl.conf
@@ -0,0 +1,40 @@
# Renforcement du service mosquitto par défaut du paquet Debian.
# L'utilisateur dédié "mosquitto" est déjà créé par le paquet ; ce drop-in
# ne fait que restreindre ses droits système.
#
# Déployé par install.sh vers /etc/systemd/system/mosquitto.service.d/override.conf
[Service]
# User=/Group= explicites : sans ça, RuntimeDirectory= (ci-dessous) crée
# /run/mosquitto appartenant à root (propriétaire par défaut quand User=
# est vide), car ce chemin est bind-monté dans le namespace propre à
# ExecStart avec cette ownership-là — le `chown mosquitto:mosquitto
# /run/mosquitto` du ExecStartPre du paquet Debian n'a aucun effet
# persistant sur cette vue. Résultat observé : mosquitto démarre root,
# abandonne ses privilèges en interne vers l'utilisateur "mosquitto"
# (aucune directive "user" dans mosquitto.conf nécessaire, comportement
# par défaut du binaire), puis échoue avec "Unable to write pid file"
# (EACCES sur /run/mosquitto/mosquitto.pid, confirmé via strace). Fixer
# User=/Group= ici fait créer /run/mosquitto directement avec le bon
# propriétaire, sans dépendre du chown du ExecStartPre.
User=mosquitto
Group=mosquitto
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/mosquitto /var/log/mosquitto
# /run/mosquitto est du tmpfs, recréé à chaque démarrage : lister ce chemin
# dans ReadWritePaths= exige qu'il existe déjà AVANT que systemd ne monte le
# namespace de l'unité (donc avant même le premier ExecStartPre), ce que
# rien ne garantit après un redémarrage du VPS ("Failed to set up mount
# namespacing: /run/mosquitto: No such file or directory", 226/NAMESPACE).
# RuntimeDirectory= est le mécanisme systemd natif pour ce cas précis : il
# crée /run/mosquitto (proprio mosquitto, cf. User=/Group= ci-dessus) lui-
# même, avant tout Exec*, et le recrée à chaque démarrage — pas de
# dépendance sur un état laissé par un run précédent.
RuntimeDirectory=mosquitto
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
@@ -0,0 +1,81 @@
# Reverse proxy nginx devant le tableau de bord Django (ASGI/Daphne,
# 127.0.0.1:8050) — WebSocket (Django Channels) inclus. PAS déployé
# automatiquement par install.sh : à copier et adapter manuellement.
#
# Pas de location /static/ dédiée ici : l'émetteur sert ses statiques
# lui-même via WhiteNoise (emitter/config/settings/production.py,
# CompressedManifestStaticFilesStorage), donc tout — /static/ compris —
# passe par le proxy_pass général vers Daphne. Ça évite une dépendance aux
# droits Unix sur staticfiles/ pour l'utilisateur nginx (ex. un $HOME en
# 700 empêche www-data de traverser jusqu'au dossier, même si staticfiles/
# lui-même est world-readable — vécu en déployant banishing.miraceti.net).
#
# Marche à suivre :
# 1. sudo apt-get install nginx
# 2. sudo certbot certonly --nginx -d __DASHBOARD_DOMAIN__ (ou webroot,
# selon la méthode déjà utilisée pour les autres domaines de ce VPS)
# 3. sed "s#__DASHBOARD_DOMAIN__#dashboard.example.org#g" \
# generic/nginx/fail2ban-emitter.example.conf \
# | sudo tee /etc/nginx/sites-available/fail2ban-emitter.conf
# 4. sudo ln -s /etc/nginx/sites-available/fail2ban-emitter.conf /etc/nginx/sites-enabled/
# 5. sudo nginx -t && sudo systemctl reload nginx
# 6. Dans .env : DJANGO_ALLOWED_HOSTS=__DASHBOARD_DOMAIN__,
# DJANGO_CSRF_TRUSTED_ORIGINS=https://__DASHBOARD_DOMAIN__,
# DJANGO_SECURE_SSL=true, puis redémarrer fail2ban-emitter-web.service
#
# Le renouvellement certbot recharge nginx automatiquement (paquet Debian,
# renewal-hooks intégré) — pas besoin d'un deploy-hook dédié comme pour
# mosquitto (nginx peut lire /etc/letsencrypt directement, contrairement à
# un utilisateur système non-root sans accès en lecture).
server {
listen 80;
listen [::]:80;
server_name __DASHBOARD_DOMAIN__;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
# Forme choisie automatiquement par install.sh (deploy_nginx) selon la
# version nginx détectée sur CE noeud : la directive séparée "http2 on;"
# n'existe qu'à partir de nginx 1.25.1 ("unknown directive http2" sur
# une version plus ancienne — vécu en conditions réelles) ; avant cette
# version, c'est l'inverse : la forme séparée n'existe pas encore, seul
# "listen ... http2" est valide. Ne jamais coder l'une des deux en dur
# ci-dessous (marqueur substitué par install.sh — ne pas le répéter
# ailleurs dans ce fichier, y compris en commentaire : la substitution
# est un remplacement global, toute autre occurrence littérale serait
# aussi affectée).
__HTTP2_LISTEN_BLOCK__
server_name __DASHBOARD_DOMAIN__;
ssl_certificate /etc/letsencrypt/live/__DASHBOARD_DOMAIN__/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/__DASHBOARD_DOMAIN__/privkey.pem;
location /ws/ {
proxy_pass http://127.0.0.1:8050;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
location / {
proxy_pass http://127.0.0.1:8050;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
@@ -0,0 +1,18 @@
; Gabarit supervisor pour le relais vers le broker master (manage.py
; master_client, SYNC_BAN). Alternative optionnelle au service systemd
; fail2ban-emitter-master-client.service — voir
; generic/supervisor/fail2ban-emitter-web.example.conf pour la marche à
; suivre complète (ne pas activer les deux mécanismes en même temps).
; Ne se connectera réellement qu'une fois un certificat de noeud généré
; (scripts/generate-node-cert.sh) et MQTT_MASTER_NODE_NAME renseigné dans .env.
[program:fail2ban-emitter-master-client]
command=/bin/bash -lc 'set -a; source __ENV_FILE__; set +a; exec __EMITTER_DIR__/.venv/bin/python manage.py master_client'
directory=__EMITTER_DIR__
user=__EMITTER_USER__
environment=DJANGO_SETTINGS_MODULE="config.settings.production",PYTHONUNBUFFERED="1"
autostart=true
autorestart=true
stopsignal=TERM
stdout_logfile=/var/log/supervisor/fail2ban-emitter-master-client.log
stderr_logfile=/var/log/supervisor/fail2ban-emitter-master-client.log
@@ -0,0 +1,16 @@
; Gabarit supervisor pour l'ingestion côté master (manage.py master_listen,
; Phase 3 — uniquement sur le VPS qui héberge le broker master). Alternative
; optionnelle au service systemd fail2ban-emitter-master-listen.service —
; voir generic/supervisor/fail2ban-emitter-web.example.conf pour la marche à
; suivre complète (ne pas activer les deux mécanismes en même temps).
[program:fail2ban-emitter-master-listen]
command=/bin/bash -lc 'set -a; source __ENV_FILE__; set +a; exec __EMITTER_DIR__/.venv/bin/python manage.py master_listen'
directory=__EMITTER_DIR__
user=__EMITTER_USER__
environment=DJANGO_SETTINGS_MODULE="config.settings.production",PYTHONUNBUFFERED="1"
autostart=true
autorestart=true
stopsignal=TERM
stdout_logfile=/var/log/supervisor/fail2ban-emitter-master-listen.log
stderr_logfile=/var/log/supervisor/fail2ban-emitter-master-listen.log
@@ -0,0 +1,15 @@
; Gabarit supervisor pour l'ingestion MQTT locale (manage.py mqtt_listen).
; Alternative optionnelle au service systemd fail2ban-emitter-mqtt.service —
; voir generic/supervisor/fail2ban-emitter-web.example.conf pour la marche à
; suivre complète (ne pas activer les deux mécanismes en même temps).
[program:fail2ban-emitter-mqtt]
command=/bin/bash -lc 'set -a; source __ENV_FILE__; set +a; exec __EMITTER_DIR__/.venv/bin/python manage.py mqtt_listen'
directory=__EMITTER_DIR__
user=__EMITTER_USER__
environment=DJANGO_SETTINGS_MODULE="config.settings.production",PYTHONUNBUFFERED="1"
autostart=true
autorestart=true
stopsignal=TERM
stdout_logfile=/var/log/supervisor/fail2ban-emitter-mqtt.log
stderr_logfile=/var/log/supervisor/fail2ban-emitter-mqtt.log
@@ -0,0 +1,31 @@
; Gabarit supervisor pour le tableau de bord Django (ASGI/Daphne).
; Alternative optionnelle au service systemd fail2ban-emitter-web.service —
; les deux mécanismes ne doivent jamais gérer le même process en parallèle.
;
; Déployé automatiquement par `sudo ./install.sh install-supervisor` (arrête
; et désactive les services systemd correspondants, installe supervisor,
; substitue les __PLACEHOLDER__ ci-dessous et démarre supervisorctl).
; `sudo ./install.sh uninstall-supervisor` fait le chemin inverse.
;
; Pour un déploiement manuel (sans passer par install.sh) :
; 1. sudo apt-get install supervisor
; 2. sed les __PLACEHOLDER__ ci-dessous (chemin du dépôt, .env, utilisateur)
; 3. cp vers /etc/supervisor/conf.d/fail2ban-emitter-web.conf
; 4. sudo systemctl disable --now fail2ban-emitter-web.service (pas les deux à la fois)
; 5. sudo supervisorctl reread && sudo supervisorctl update
;
; __EMITTER_DIR__ : chemin absolu vers le sous-répertoire emitter/ du dépôt
; (ex. /home/banisher/Fail2banMqttActionBanisher/emitter)
; __ENV_FILE__ : chemin absolu vers le .env à la racine du dépôt
; __EMITTER_USER__: utilisateur système propriétaire du dépôt (ex. banisher)
[program:fail2ban-emitter-web]
command=/bin/bash -lc 'set -a; source __ENV_FILE__; set +a; exec __EMITTER_DIR__/.venv/bin/daphne -b 127.0.0.1 -p 8050 config.asgi:application'
directory=__EMITTER_DIR__
user=__EMITTER_USER__
environment=DJANGO_SETTINGS_MODULE="config.settings.production",PYTHONUNBUFFERED="1"
autostart=true
autorestart=true
stopsignal=TERM
stdout_logfile=/var/log/supervisor/fail2ban-emitter-web.log
stderr_logfile=/var/log/supervisor/fail2ban-emitter-web.log
@@ -0,0 +1,15 @@
; Gabarit supervisor pour le worker Celery (géolocalisation asynchrone).
; Alternative optionnelle au service systemd fail2ban-emitter-worker.service —
; voir generic/supervisor/fail2ban-emitter-web.example.conf pour la marche à
; suivre complète (ne pas activer les deux mécanismes en même temps).
[program:fail2ban-emitter-worker]
command=/bin/bash -lc 'set -a; source __ENV_FILE__; set +a; exec __EMITTER_DIR__/.venv/bin/celery -A config worker -l info'
directory=__EMITTER_DIR__
user=__EMITTER_USER__
environment=DJANGO_SETTINGS_MODULE="config.settings.production",PYTHONUNBUFFERED="1"
autostart=true
autorestart=true
stopsignal=TERM
stdout_logfile=/var/log/supervisor/fail2ban-emitter-worker.log
stderr_logfile=/var/log/supervisor/fail2ban-emitter-worker.log