Ceci est une ancienne révision du document !
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
Commandes de base
Commandes:
start: Démarre le démon Reactionreaction start [OPTIONS] –config <CONFIG>- Options possibles :
-c(Requis),-l,-s,-h - Par exemple, pour un fichier
reaction start -c /etc/reaction/server.jsonnet - Et un dossier :
reaction start -c /etc/reaction/ - Dans le cas des dossiers, les seuls fichiers du dossier qui seront lus automatiquement
- Doivent se terminer par les extensions
.json,.jsonnet,.ymlou.yaml. - Ne doivent pas démarrer par
.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- Options possibles :
-s,-f,-l,-h
flush: Retire une cible de Reaction (unban)reaction flush [OPTIONS] [NAME=PATTERN]- Options possibles :
-s,-f,-l,-h - Débannir quelqu'un :
reaction flush IP
test-regex: Tester une regex- reaction test-regex –config <CONFIG> <REGEX> [LINE]
- Arguments:
- <REGEX> Regex to test
- [LINE] Line to be tested
- Options possibles :
-c(requis),-h
test-config: Tester une configurationreaction test-config [OPTIONS] –config <CONFIG>- Options possibles :
-c(requis),-f,-v,-h
help,-h: Affiche l'aide sur les commandes et sous-commandes-V,–version: Affiche la version de reaction
Pour 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.
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.
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 (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
Démarrage automatique
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
On va créer un service :
sudo nano /etc/systemd/system/reaction.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
Vu que j'ai eu des plantages muets, je lui dis de se relancer si ça lui arrive1). Ce serait bien d'être averti quand ça plante, ceci dit
On démarre le service :
sudo systemctl enable reaction.service sudo service reaction start
Configuration
Pour la doc complète, se référer à https://reaction.ppom.me/. Concernant ma configuration, je pars avec ces spécificités :
- Utilisation de nftables
- Découpage de la configuration par services (pour faciliter certaines automatisations)
- Analyse des ip bannies pour en déduire des plages et bannir ces dernières ?
- Rapport (journalier ?) sur les ip bannies (histoire de pouvoir suivre ce qui se passe et la charge) ?
Découpage des fichiers
Il s'agit de me simplifier un peu la lecture et la maintenance. Sur l'exemple ici, j'ai 3 types de fichiers
- Définitions : ce qu'est l'action pour bannir/débannir, les périodes par défaut de bannissement… C'est le fichier
_lib.jsonnet. server.jsonnetqui contient les trucs de base : comment les ip sont formatées et ce qui se passe quand on démarre/arrête le service.- Les fichiers de stream (
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).
server.jsonnet
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.
- server.jsonnet
{ 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'], ], }
_lib.jsonnet : actions par défaut
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.
- _lib.jsonnet
local banFor(time) = { ban: { cmd: ['nft46', 'add element inet reaction ipvXbans { <ip> }'], }, unban: { cmd: ['nft46', 'delete element inet reaction ipvXbans { <ip> }'], after: time, }, }; local filter_default = { retry: 3, retryperiod: '3h', actions: banFor('24h'), }; { banFor: banFor, filter_default: filter_default, }
Streams
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.
- ssh.jsonnet
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_defaultetlib.banFor: on utilise les actionsbanForetfilter_defaultmais comme elles sont dans le fichier_lib.jsonnet, on appelle la variable définie avant (lib). Sinon, ben… ça marche pas.
Ce qui est ici date des premières versions de reaction, on va voir ce qu'on garde.
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"'),
},
},
},
Mes bouts de stream et de config
Fichier principal
- server.jsonnet
// 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>\]', @'^.*RCPT from unknown\[<ip>\].*5yuehaoyunqi\.xyz.*', @'^.*RCPT from unknown\[<ip>\].*funteensex\.com.*', ], retry: 1, retryperiod: '6h', actions: banFor('720h') + sendmail('<ip>','"Banni un mois sans seconde chance pour avoir mal causé à Postfix"'), }, }, },
