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,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
|
||||
Reference in New Issue
Block a user