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,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