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,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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
# Conserver l'historique des bans 7 jours (défaut : 1 jour)
|
||||
dbpurgeage = 7d
|
||||
@@ -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
|
||||
@@ -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 =
|
||||
@@ -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 =
|
||||
@@ -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, \+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 =
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user