====== Reaction, alternative à Fail2ban ====== [[https://framagit.org/ppom/reaction/-/tree/main|Reaction]] (comme dans "action -> réaction") est un logiciel permettant de faire comme Fail2ban mais sans les défauts de ce dernier : * Consomme moins * Plus facile à customiser * Carrément plus simple de faire des regex Et autres qualités que l'auteur explique bien. Faudra que je participe au wiki officiel mais en attendant je met mes idées en place ici. J'utilise la syntaxe jsonnet, qui est la plus complète et lisible à mes yeux. Ma config se met dans ''/etc/reaction/server.jsonnet'' ===== Commandes de base ===== Voir qui est banni : reaction show Débannir quelqu'un : reaction flush IP Consulter l'aide : reaction --help ===== Envoyer des mails quand il y a une action ===== J'ai une commande qui va envoyer un mail via un script, en ayant en paramètre deux variables : ''ip'' (l'ip bannie) et ''rule'' (la raison du bannissement). local sendmail(ip,rule) = { mail: { cmd: ['sh', '/root/scripts/sendmailreaction/mailreaction.sh', ip, rule], }, }; Le script : #!/bin/bash # Envoyer un mail dossiermail="/root/scripts/sendmailreaction/mail" titremail="$1 banni" # Création du fichier du corps du mail dans un fichier temporaire qui évite toute collision corpmail="$(mktemp -p $dossiermail)" listeip="/var/lib/reaction/reaction-matches.db" # Vérifier si l'adresse a déjà été banni pour éviter de flooder lorsqu'on relance if grep -q $1 $listeip; then exit else # Message pour le corps du mail echo "Méchant $1 ! $2 \n \n" >> $corpmail # Ajouter les logs pour les détails journalctl --since yesterday | grep $1 >> $corpmail #Envoyer le mail cat $corpmail | mail -s "$titremail" root # Effacer le corps du crime pour éviter d'encombrer sans intérêt rm $corpmail fi Penser évidement à créer les dossiers ''/root/scripts/sendmailreaction/'' et ''/root/scripts/sendmailreaction/mail'' avant de lancer le reste. Plus loin dans le fichier de configuration de Reaction, dans les streams, comment dire qu'il faut envoyer un mail (exemple sur ssh) : streams: { ssh: { cmd: ['journalctl', '-fn0', '-u', 'ssh.service'], filters: { failedlogin: { regex: [ @'authentication failure;.*rhost=', @'Connection reset by authenticating user .* ', @'Failed password for .* from ', ], retry: 6, retryperiod: '6h', actions: banFor('48h') + sendmail('','"Banni 48h pour tentative de co à SSH"'), }, }, }, ===== Actions par défaut ===== On peut créer des actions par défaut qu'on appelle ensuite ; cela permet par exemple de définir pour tout le monde la même durée de bannissement et de la changer à un seul endroit si besoin. Ce n'est pas forcément pertinent pour tout, par exemple je préfère préciser la raison du bannissement pour savoir quels services se font attaquer. On déclare la commande : local filter_default = { retry: 3, retryperiod: '6h', actions: banFor('24h'), }; Et dans les streams, on l'appelle de cette façon : { streams: { ssh: { filters: { failedlogin: filter_default + { regex: ['...'], }, }, }, }, } ===== Découper la configuration ===== On peut tout mettre dans un seul fichier, moi j'aime bien séparer pour m'y retrouver. Faut avouer que je prévois max de regex à un moment, et max de services couverts aussi. Dans le fichier principal, on peut appeler un autre fichier avec cette syntaxe : { streams: { ssh: import 'ssh.jsonnet', }, } et ''ssh.jsonnet'' va contenir notre morceau propre au stream. Mais j'ai du manquer un bout, ça me fait des erreurs ça... Il ne reconnait pas le "banfor" pourtant déclaré dans le fichier principal. ===== Mes bouts de stream et de config ===== Fichier principal // This file is using JSONNET, a complete configuration language based on JSON // See https://jsonnet.org // action pour bannir/débannir local banFor(time) = { ban: { cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP'], }, unban: { after: time, cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP'], }, }; // Envoyer un mail lors des actions et précisant ip et raison local sendmail(ip,rule) = { mail: { cmd: ['sh', '/root/scripts/sendmailreaction/mailreaction.sh', ip, rule], }, }; // pourquoi ça ouvre ici ? Mais, ça marche. { // patterns are substitued in regexes. when a filter performs an action, it replaces the found pattern. // reaction regex syntax is defined here: https://github.com/google/re2/wiki/Syntax // jsonnet's @'string' is for verbatim strings. patterns: { // IPs can be IPv4 or IPv6 // ip46tables (C program also in this repo) handles running the good commands ip: { regex: @'(([0-9]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})', ignore: ['127.0.0.1', '::1'], }, }, // Commandes exécutées au lancement start: [ ['ip46tables', '-w', '-N', 'reaction'], ['ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction'], ['echo', 'Le service a démarré', '|', 'mail', '-s', '"Démarrage de Reaction"', 'root'], ], // Commandes exécutées à l'arrêt, après tout le reste stop: [ ['ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction'], ['ip46tables', '-w', '-F', 'reaction'], ['ip46tables', '-w', '-X', 'reaction'], ['echo', 'Le service est éteint', '|', 'mail', '-s', '"Extinction de Reaction"', 'root'], ], // Streams : c'est là qu'on va définir les services et règles menant au bannissement streams: { ssh: import 'ssh.jsonnet', kernel: import 'kernel.jsonnet', badguypostfix: import 'badguypostfix.jsonnet', }, } Pour ssh et kernel, il s'agit des configurations par défaut auxquelles j'ai ajouté mon envoi de mail : { cmd: ['journalctl', '-fn0', '-u', 'ssh.service'], filters: { failedlogin: { regex: [ @'authentication failure;.*rhost=', @'Connection reset by authenticating user .* ', @'Failed password for .* from ', ], retry: 6, retryperiod: '6h', actions: banFor('48h') + sendmail('','"Banni 48h pour tentative de co à SSH"'), }, }, }, // Ban hosts which knock on closed ports. // It needs this iptables chain to be used to drop packets: // ip46tables -N log-refuse // ip46tables -A log-refuse -p tcp --syn -j LOG --log-level info --log-prefix 'refused connection: ' // ip46tables -A log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse // ip46tables -A log-refuse -j DROP { cmd: ['journalctl', '-fn0', '-k'], filters: { portscan: { regex: ['refused connection: .*SRC='], retry: 4, retryperiod: '6h', actions: banFor('720h') + sendmail('','"est banni un mois pour avoir fait un truc louche sur un port fermé"'), }, }, }, Pour postfix, pour le moment je cible certains bot cons. { cmd: ['journalctl', '-fn0', '-u', 'postfix@-.service'], filters: { badguy: { regex: [ @'^.* improper command pipelining after CONNECT from unknown\[\].*', @'^.*\[\].*tiscali.it.*', @'^.* NOQUEUE: reject: RCPT from unknown\[\]: 504 5.5.2 .* Helo command rejected: need fully-qualified hostname; .*', @'^.*connect from .*censys.*\[\]', @'^.*connect from .*stretchoid.*\[\]', ], retry: 1, retryperiod: '6h', actions: banFor('720h') + sendmail('','"Banni un mois sans seconde chance pour avoir mal causé à Postfix"'), }, }, }, {{tag>fail2ban sysadmin iptable}} [[https://creativecommons.org/publicdomain/zero/1.0/deed.fr|{{ https://liev.re/imagesweb/licences/cc-zero.png?100 | Ce texte est placé sous licence CC0}}]]