Reaction (comme dans “action → réaction”) est un logiciel permettant de faire comme Fail2ban mais sans les défauts de ce dernier :
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:
start
: Démarre le démon Reactionreaction start [OPTIONS] –config <CONFIG>
-c
(Requis), -l
, -s
, -h
reaction start -c /etc/reaction/server.jsonnet
reaction start -c /etc/reaction/
.json
, .jsonnet
, .yml
ou .yaml
..
ou _
.show
: Affiche les correspondances actuelles et les actions (= qui est banni)reaction show [OPTIONS] [NAME=PATTERN]
[NAME=PATTERN]
: n'affiche que les éléments correspondant à name=PATTERN regex-s
, -f
, -l
, -h
flush
: Retire une cible de Reaction (unban)reaction flush [OPTIONS] [NAME=PATTERN]
-s
, -f
, -l
, -h
reaction flush IP
test-regex
: Tester une regex-c
(requis), -h
test-config
: Tester une configurationreaction test-config [OPTIONS] –config <CONFIG>
-c
(requis), -f
, -v
, -h
help
, -h
: Affiche l'aide sur les commandes et sous-commandes-V
, –version
: Affiche la version de reactionPour les options possibles (suivant les commandes) :
-c
, –config <CONFIG>
: Fichier de configuration en format json, jsonnet ou yaml ; ou dossier contenant ces fichers. Requis.-l
, –loglevel <LOGLEVEL>
: Niveau de log minimum à afficher [default: INFO].-s
, –socket <SOCKET>
: chemin d'accès au socket de communication client-daemon [default: /run/reaction/reaction.sock].-h
, –help
: Affiche l'aide.-f
, –format <FORMAT>
: comment formater la sortie [default: yaml] [valeurs possibles : json, yaml].-v
, –verbose
: affiche plus d'infos en sortie.Voir qui est banni :
reaction show
Débannir quelqu'un :
reaction flush IP
Tester le dossier de configuration :
reaction test-config -c /etc/reaction
Démarrer (sans systemd) :
reaction start -c /etc/reaction/
Par défaut les bases de données sont dans /var/lib/reaction/
(reaction-matches.db
et reaction-flushes.db
). Les effacer remet tout à zéro.
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 (et les instructions)… J'utilise wget plutôt que curl, ce dernier ayant quelques soucis de sécurité (et puis wget est déjà de base sur mon serveur).
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 le ou les fichiers de config, et on les bidouillent. Voir plus bas la/les confs.
sudo mkdir /etc/reaction/ sudo nano /etc/reaction/reaction.jsonnet
Avant de démarrer automatiquement 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
On peut aussi vérifier la syntaxe avec :
sudo reaction test-config -c /etc/reaction
Avec la version 2 il y a un à présent un service systemd déjà fourni. Il se trouve sur /lib/systemd/system/reaction@.service.
Je laisse La vieille doc “pour mémoire” mais ce n'est plus forcément utile.
Après installation et vérification, pour utiliser systemd :
sudo systemctl daemon-reload sudo systemctl enable --now reaction@reaction
Ce qui suit après le @ est le chemin vers la configuration dans /etc/ ; comme je lui fais lire le dossier, c'est donc juste “reaction” pour “/etc/reaction”.
Je vais créer deux services : un pour Reaction proprement dit, un pour avertir en cas de plantage. Vu que j'ai eu des plantages muets, je lui dis de se relancer si ça lui arrive1) et sinon, j'ai une alerte.
[Unit] Description=Reaction to ban bad ip After=network.target # Alerte si ça "fail" OnFailure=reaction-alert.service [Install] WantedBy=multi-user.target [Service] ExecStart=/usr/bin/reaction start -c /etc/reaction/ StateDirectory=reaction RuntimeDirectory=reaction WorkingDirectory=/var/lib/reaction Restart=on-failure RestartSec=3 # Anti-flood de redémarrage en boucle StartLimitIntervalSec=400 StartLimitBurst=3
[Unit] Description=Envoie une alerte si Reaction plante [Service] Type=oneshot ExecStart=/usr/local/bin/reaction-alert.sh
Et le script pour envoyer un mail (ça aurait pu être autre chose, mais j'aime bien les mails).
#!/bin/bash LOCKFILE="/tmp/reaction-alert.lock" # Délai en minutes, donc 3 h = 180 DELAY=180 # Si une alerte a déjà été envoyée il y a moins de $DELAY, on quitte if [ -e "$LOCKFILE" ] && [ "$(find "$LOCKFILE" -mmin -lt $DELAY)" ]; then logger -t reaction-alert "Notification de plantage de Reaction déjà envoyée récemment, aucune alerte renvoyée." exit 0 fi touch "$LOCKFILE" SUBJECT="Reaction a planté sur $(hostname)" TO="monemail@domaine.org" BODY="Le service Reaction sur $(hostname) semble avoir un souci. Arrêt à $(date). " echo "$BODY" | mail -s "$SUBJECT" "$TO" logger -t reaction-alert "Alerte envoyée par mail à $TO pour le plantage de Reaction"
On démarre le service :
sudo systemctl enable reaction.service sudo service reaction start
Pour la doc complète, se référer à https://reaction.ppom.me/. Concernant ma configuration, je pars avec ces spécificités :
Point côté syntaxe : dans les noms, on peut utiliser le tiret bas, mais pas de tiret classique, ça va faire des erreurs car jsonnet l'interprète… Donc “mon_filtre” est ok, pas “mon-filtre”.
Il s'agit de me simplifier un peu la lecture et la maintenance. Sur l'exemple ici, j'ai 3 types de fichiers
_lib.jsonnet
.server.jsonnet
qui contient les trucs de base : comment les ip sont formatées et ce qui se passe quand on démarre/arrête le service.ssh.jsonnet
, web.jsonnet
, etc) qui vont lister, par service, quel fichier de log analyser et sur quelles regex on va bannir.On paramètre évidement le lancement de Reaction (avec service ou direct en ligne de commande) pour lire tout le contenu du dossier (ici /etc/reaction).
Ici c'est assez classique, le seul point important étant que je tourne avec nftables. Il peut être utile de spécifier les ip à ne pas bannir avec Reaction, couplé avec un fichier nftables complet (entre autre sur la vérification des ip locales), ça évite de se bannir son propre routeur ou en tant que sysadmin.
{ patterns: { ip: { // Both IPv4 and IPv6, do not accept malformed IPs regex: @'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))', ignore: [ // Ne pas oublier la virgule après chaque ip, pour l'énumération ;) '127.0.0.1', '::1', // Sous-réseau d'une box '192.168.1.0/24' // Sous-réseau proxmox ? //'10.0.0.0/8' // Ip fixes de sysadmins // ip de bastion ], }, }, start: [ ['nft', ||| table inet reaction { set ipv4bans { type ipv4_addr flags interval auto-merge } set ipv6bans { type ipv6_addr flags interval auto-merge } chain input { type filter hook input priority 0 policy accept ip saddr @ipv4bans drop ip6 saddr @ipv6bans drop } } ||| ], ], stop: [ ['nft', 'delete table inet reaction'], ], }
Ce fichier sert à définir les actions telles que “banFor”, qu'on configure évidement avec notre commande pour nftables2).
On peut ainsi définir des actions par défaut : combien de temps on bannit, combien de lignes dans les logs avant de bannir, etc. 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.
Le fait que le fichier commence par “_” fait qu'il ne sera pas lu automatiquement au lancement de Reaction, il faudra l'appeler là où il est utile (dans les streams). Si on ne fais pas ça (s'il est noté “lib.jsonnet” par exemple), on aura l'erreur “ERROR While reading ssh.jsonnet in /etc/reaction: variable is not defined: filter_default” lors de l'execution.
local banFor(time) = { ban: { cmd: ['nft46', 'add element inet reaction ipvXbans { <ip> }'], }, unban: { cmd: ['nft46', 'delete element inet reaction ipvXbans { <ip> }'], after: time, }, }; // retry et retryperiod sont quand il y a plusieurs tentatives autorisées // juste mettre le banFor sinon... // Filtre (et options) par défaut : ni trop doux, ni trop cruel. local filter_default = { retry: 3, retryperiod: '3h', actions: banFor('48h'), }; // Filtre doux : c'est peut-être légitime. Et peut-être pas. local filter_soft = { retry: 6, retryperiod: '3h', actions: banFor('10s'), }; // Filtre violent : un seul essai, banni un mois. local filter_hard = { actions: banFor('720h'), }; local stream_name(name) ={ reason: { cmd: ['logger', 'REACTION BLOCK <ip> because: ', name], }, }; // Exposer les définitions précédentes pour qu'elles soient accessibles depuis un autre fichier Jsonnet { banFor: banFor, filter_default: filter_default, filter_soft: filter_soft, filter_hard: filter_hard, stream_name: stream_name, }
Je met ici une config très basique pour ssh (Faudra que je mette le tout sur une forge, une fois fini…), suffisante pour lister les “pièges” dans la syntaxe.
local lib = import '_lib.jsonnet'; { streams: { ssh: { cmd: ['journalctl', '-fn0', '-u', 'sshd.service'], filters: { failedlogin: lib.filter_default + { regex: [ @'authentication failure;.*rhost=<ip>', @'Connection reset by authenticating user .* <ip>', @'Failed password for .* from <ip>', ], retry: 3, retryperiod: '6h', actions: lib.banFor('48h'), }, }, }, }, }
local lib = import '_lib.jsonnet';
⇒ il est nécessaire d'appeler le fichier avec les définitions pour la suite, et on fait une variable pour les appels la concernant (lib
).cmd: ['journalctl', '-fn0', '-u', 'sshd.service']
⇒ indique quel fichier de log lire.lib.filter_default
et lib.banFor
: on utilise les actions banFor
et filter_default
mais comme elles sont dans le fichier _lib.jsonnet
, on appelle la variable définie avant (lib
). Sinon, ben… ça marche pas.Concernant l'appel des diverses fonctions, il y a plein de petites subtilités. Par exemple, si on veut appliquer les actions par défaut (même durée de ban, même valeurs pour retry, retryperiod) mais déclarer une action complémentaire comme “stream_name”, alors on fait ceci :
[...] apache_auth: lib.filter_default + { regex: [ @'^.*client <ip>.* regex', ], actions: lib.filter_default.actions + lib.stream_name('apache_auth'), }, [...]
Ici lib.filter_default.actions
précise qu'il faut appliquer les actions par défaut de “filter_default”. + lib.stream_name('apache_auth')
permet d'ajouter l'action stream_name (qui permet de loguer pour “quoi” on a banni), en précisant une raison personnalisée donc. Il faut quand même déclarer lib.filter_default
au niveau du stream (après “apache_auth” dans l'exemple), afin que les paramètres “retry” et “retryperiod” s'appliquent sur le stream.
Lorsqu'il y a bannissement via Reaction, j'aimerais pouvoir réaliser certaines actions, comme être avertie par mail.
Bon, en vrai, ça dépend. Sur certains services, la surveillance via des mails, au début, permet d'affiner les règles et éviter les faux positifs. Ensuite, ça fait surtout du bruit. Il devient alors plus utile de loguer les ip bannies, et de vérifier s'il y a des schémas : même plages d'ip par exemple. Ou ip qui continuent d'être bannies mois après mois.
Bref, tout ça va se faire via notre fichier _lib.jsonnet, avec des actions diverses.
Si on veut bannir ET réaliser une action autre dans la foulée (par exemple envoyer un mail), le plus simple sera finalement de faire un script de ce type :
#!/bin/bash # Bannissement nft46 add element inet reaction ipvXbans { "$1" } # Envoi d'un mail # cf le script plus bas...
On modifie alors _lib.jsonnet sur la partie cmd :
local banFor(time) = { ban: { cmd: ['nft46', 'add element inet reaction ipvXbans { <ip> }'], }
devient
local banFor(time) = { ban: { cmd: ['sh', '-c', '/etc/reaction/_ban.sh <ip>'], }
Ce qui autorise toutes les fantaisies.
On peut aussi déclarer une action spéciale pour l'envoi de mail, ce qui permet de l'appeler dans les streams et de personnaliser le message en rapport avec le filtre déclenché.
Le script suivant prend deux variables : ip
(l'ip bannie) et rule
(la raison du bannissement).
local sendmail(ip,rule) = { mail: { cmd: ['sh', '/etc/reaction/_mail.sh', ip, rule], }, };
Le script :
#!/bin/bash # Envoyer un mail dossiermail="/root/scripts/sendmailreaction" 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 le dossier /root/scripts/sendmailreaction/
avant de lancer le reste.
Dans les streams (en ayant des fichiers modulaires), 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: lib.banFor('48h') + lib.sendmail('<ip>','"Banni 48h pour tentative de co à SSH"'), }, }, },