Table des matières

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 :

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"'),
        },
      },
    },

 Ce texte est placé sous licence CC0