Reaction, alternative à Fail2ban
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
Installation et lancement automatique
Pour l'installer, il y a un paquet debian mais pas dans les dépôts. Installer aussi Minisign pour vérifier l'intégrité. Ci-dessous en exemple mais la page des releases donne les bons numéros de release…
sudo apt install minisign wget https://static.ppom.me/reaction/releases/v1.4.1/reaction_1.4.1-1_amd64.deb \ https://static.ppom.me/reaction/releases/v1.4.1/reaction_1.4.1-1_amd64.deb.minisig minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m reaction_1.4.1-1_amd64.deb && rm reaction_1.4.1-1_amd64.deb.minisig && sudo apt install ./reaction_1.4.1-1_amd64.deb
Ensuite on crée un fichier de config, on le modifie, et aussi un truc pour systemd.
sudo mkdir /etc/reaction/ sudo nano /etc/reaction/reaction.jsonnet
(voir plus bas la/les confs).
sudo nano /etc/systemd/system/reaction.service
[Install] WantedBy=multi-user.target [Service] ExecStart=/usr/bin/reaction start -c /etc/reaction/server.jsonnet StateDirectory=reaction RuntimeDirectory=reaction WorkingDirectory=/var/lib/reaction Restart=on-failure RestartSec=3
Vu que j'ai eu des plantages muets, je lui dis de se relancer si ça lui arrive.
Et puis on y lance :
sudo systemctl enable reaction.service sudo service reaction start
Mais avant de démarrer le service, c'est pas mal de tester sa configuration en lançant seulement la ligne de démarrage dans le terminal, ce qui casse à la première erreur :
sudo reaction start -c /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=<ip>', @'Connection reset by authenticating user .* <ip>', @'Failed password for .* from <ip>', ], retry: 6, retryperiod: '6h', actions: banFor('48h') + sendmail('<ip>','"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
- server.jsonnet
// 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', '<ip>', '-j', 'DROP'], }, unban: { after: time, cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-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 :
- ssh.jsonnet
{ cmd: ['journalctl', '-fn0', '-u', 'ssh.service'], filters: { failedlogin: { regex: [ @'authentication failure;.*rhost=<ip>', @'Connection reset by authenticating user .* <ip>', @'Failed password for .* from <ip>', ], retry: 6, retryperiod: '6h', actions: banFor('48h') + sendmail('<ip>','"Banni 48h pour tentative de co à SSH"'), }, }, },
- kernel.jsonnet
// 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=<ip>'], retry: 4, retryperiod: '6h', actions: banFor('720h') + sendmail('<ip>','"est banni un mois pour avoir fait un truc louche sur un port fermé"'), }, }, },
Pour postfix, pour le moment je cible certains bot cons.
- badguypostfix.jsonnet
{ cmd: ['journalctl', '-fn0', '-u', 'postfix@-.service'], filters: { badguy: { regex: [ @'^.* improper command pipelining after CONNECT from unknown\[<ip>\].*', @'^.*\[<ip>\].*tiscali.it.*', @'^.* NOQUEUE: reject: RCPT from unknown\[<ip>\]: 504 5.5.2 .* Helo command rejected: need fully-qualified hostname; .*', @'^.*connect from .*censys.*\[<ip>\]', @'^.*connect from .*stretchoid.*\[<ip>\]', ], retry: 1, retryperiod: '6h', actions: banFor('720h') + sendmail('<ip>','"Banni un mois sans seconde chance pour avoir mal causé à Postfix"'), }, }, },