Tutoriel en français
Ce tutoriel concerne la configuration de reaction. Pour l’installation, voir la page des releases.
Trois langages de configuration sont disponibles :
- JSON,
- YAML,
- JSONnet (❤️)
Je ne vous présente pas les deux premiers, mais je parle du dernier à la fin.
Les Patterns
On commence par préciser comment reaction va reconnaitre une IP.
On définit donc un pattern, de type ipv4, qu’on appelle myip.
patterns:
myip:
type: 'ipv4'
Les patterns ont des types spéciaux :
ipv4pour des IPv4,ipv6pour des IPv6, etippour reconnaitre à la fois des IPv4 et des IPv6.Si vous voulez créer des patterns qui permettent de capturer autre chose que des IPs, vous pouvez renseigner vous-même vos propres regex :
patterns: # Un pattern appelé name qui reconnait un mot ASCII # commençant par une majuscule name: regex: '[A-Z][a-z]*'
ℹ️ Référence de configuration des Patterns
Les Streams
En remplacement des jails de fail2ban, on a des streams, qui définissent une source de données (par exemple tail -F -n0 /var/log/nginx/access.log pour nginx).
patterns:
myip:
type: 'ipv4'
streams:
ssh:
cmd: ['journalctl', '-n0', '-fu', 'sshd.service']
ℹ️ Référence de configuration des Streams
Les Filters
À ces streams, on attache un ou plusieurs filters. Ce sont des groupes d’expressions régulières. C’est aussi sur un filter qu’on décide du nombre de mauvais essais (retry) qu’on accorde à une IP avant de réagir.
Ici, on précise que le Filter appelé authfail doit être déclenché (“trigger”) si le pattern myip était retrouvé 3 fois en 3h.
patterns:
myip:
type: 'ipv4'
streams:
ssh:
cmd: ['journalctl', '-n0', '-fu', 'sshd.service']
filters:
authfail:
regex:
- 'authentication failure;.*rhost=<myip>'
retry: 3
retryperiod: '3h'
ℹ️ Référence de configuration des Filters
Les Actions
À un filter, on rajoute une ou plusieurs actions, qui seront exécutées quand le filter se déclenche.
Ici, on crée une action sobrement appelée ban qui insère l’IP dans un set IPset nommé reactionv4.
patterns:
myip:
type: 'ipv4'
streams:
ssh:
cmd: ['journalctl', '-n0', '-fu', 'sshd.service']
filters:
authfail:
regex:
- 'authentication failure;.*rhost=<myip>'
retry: 3
retryperiod: '3h'
actions:
ban:
cmd: ['ipset', '-exist', '-A', 'reactionv4', '<myip>']
Une action peut être effectuée tout de suite, ou être délayée avec after, ce qui permet de bannir une IP maintenant, et de la débannir quelque temps plus tard.
On ajoute ici une action pour débannir l’IP 24h plus tard, qu’on choisit d’appeler unban.
patterns:
myip:
type: 'ipv4'
streams:
ssh:
cmd: ['journalctl', '-n0', '-fu', 'sshd.service']
filters:
authfail:
regex:
- 'authentication failure;.*rhost=<myip>'
retry: 3
retryperiod: '3h'
actions:
ban:
cmd: ['ipset', '-exist', '-A', 'reactionv4', '<myip>']
unban:
cmd: ['ipset', '-D', 'reactionv4', '<myip>']
after: '24h'
ℹ️ Référence de configuration des Actions
Start / Stop
Ces commandes ipset ont besoin de l’existence du set reactionv4 dans le pare-feu.
Au démarrage, on demande à reaction de le créer, et de l’ajouter aux chaines INPUT et FORWARD, qui contrôlent les connexions entrantes et les connexions transférées (FORWARD est utile si vous avez des conteneurs ou des VMs).
start:
- ['ipset', '-exist', '-N', 'reactionv4', 'hash:net', 'family', 'inet']
- ['iptables', '-w', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
- ['iptables', '-w', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
On lui demande aussi de supprimer le set et retirer ses règles en quittant :
stop:
- ['iptables', '-w', '-D', 'INPUT', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
- ['iptables', '-w', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
- ['ipset', '-X', 'reactionv4' ]
Récap
patterns:
myip:
type: 'ipv4'
streams:
ssh:
cmd: ['journalctl', '-n0', '-fu', 'sshd.service']
filters:
authfail:
regex:
- 'authentication failure;.*rhost=<myip>'
retry: 3
retryperiod: '3h'
actions:
ban:
cmd: ['ipset', '-exist', '-A', 'reactionv4', '<myip>']
unban:
cmd: ['ipset', '-D', 'reactionv4', '<myip>']
after: '24h'
start:
- ['ipset', '-exist', '-N', 'reactionv4', 'hash:net', 'family', 'inet']
- ['iptables', '-w', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
- ['iptables', '-w', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
stop:
- ['iptables', '-w', '-D', 'INPUT', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
- ['iptables', '-w', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP']
- ['ipset', '-X', 'reactionv4' ]
Et voilà. En 28 lignes de configuration, sans aucune configuration par défaut, reaction va surveiller les connexions SSH et bannir les connexions malveillantes pendant 24h au bout de 3 essais infructueux.
Remarques :
-
Pour faire simple ici, on ne gère que les IPv4. Mais c’est possible de définir une certaine action pour les IPv4 et une autre pour les IPv6.
-
Toujours pour faire simple, on n’a défini qu’une regex pour les logs SSH. Mais OpenSSH peut logger d’autres lignes qui indiquent un échec de connexion. Voir la doc du Filter SSH.
-
Il est possible de faire n’importe quelle action : lancer les commandes pour n’importe quel firewall, envoyer un mail, etc. Voir les exemples d’actions pour des inspirations.
-
Il est conseillé de jeter un coup d’oeil sur le chapitre des Streams pour comprendre pourquoi il vaut mieux utiliser
tail -Fquetail -f, pourquoi on utilise-n0, etc. -
Lire aussi le chapitre Sécurité pour ne pas faire de bêtises. On est là pour améliorer la sécurité de ses serveurs, pas l’empirer !
-
Les Plugins permettent de nouveaux types de Streams et d’Actions, qu’on n’a pas vus ici.
JSONnet
C’est un langage simple et flexible, avec une syntaxe proche de Javascript et JSON. Par défaut, c’est juste un langage qui ressemble à JSON, mais en plus flexible :
// On peut mettre des commentaires
{
// Pas besoin de mettre des "quotes" partout
streams: {
ssh: {
// On peut avoir des virgules après le dernier élément ↓
cmd: [' journalctl', '-fu', 'sshd.service'],
}
}
}
Pour éviter les répétitions, on écrit des variables et des fonctions.
local hour2second(i) = i * 60 * 60;
{
seconds: [
hour2second(1),
hour2second(3),
hour2second(5),
],
}
JSONnet fonctionne comme un préprocesseur qui va générer une structure de données compatible JSON. C’est ce résultat qui est donné à reaction.
{
"seconds": [ 3600, 10800, 18000 ]
}
C’est un peu comme le langage Nix, mais avec une syntaxe plus agréable.
Maintenant que JSONnet est présenté, on reprend l’exemple précédemment écrit en YAML. On peut le réécrire en JSONnet en imaginant qu’on rajoute un deuxième stream pour protéger un autre service.
On veut éviter d’écrire plusieurs fois les commandes ipset (une fois suffit amplement 😆).
On va donc écrire une fonction banFor(), qui prend en argument la durée selon laquelle on va bannir les IPs, et retourne une liste d’actions.
On peut la réutiliser sur chaque stream.
local banFor(time) = {
ban: {
cmd: ['ipset', '-exist', '-A', 'reactionv4', '<myip>']
},
unban: {
after: time,
cmd: ['ipset', '-D', 'reactionv4', '<myip>']
},
};
{
patterns: {
myip: {
type: 'ipv4',
},
},
streams: {
ssh: {
cmd: ['journalctl', '-n0', '-fu', 'sshd.service'],
filters: {
authfail: {
regex: [
'authentication failure;.*rhost=<myip>'
],
retry: 3,
retryperiod: '3h',
actions: banFor('24h'),
},
},
},
nginx: {
cmd: ['tail', '-f', '/var/log/nginx/access.log'],
filters: {
directus: {
regex: [ @'^<myip> .* "POST /auth/login HTTP/..." 401', ],
retry: 6,
retryperiod: '4h',
actions: banFor('4h'),
},
},
},
},
start: [
['ipset', '-exist', '-N', 'reactionv4', 'hash:net', 'family', 'inet'],
['iptables', '-w', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
['iptables', '-w', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
],
stop: [
['iptables', '-w', '-D', 'INPUT', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
['iptables', '-w', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
['ipset', '-X', 'reactionv4' ],
],
}
Et voilà ! On a écrit le soupçon de généricité dont on avait besoin. On peut maintenant définir en 8 lignes comment protéger un nouveau service.
En l’occurence, c’est Directus, que je conseille pour construire des interfaces et des bases de données sur-mesure simplement.
Séparer la configuration en plusieurs fichiers
On peut avoir plusieurs bonnes raisons de vouloir séparer sa configuration en plusieurs fichiers.
Ainsi chaque pattern, stream, filter, start/stop, peut être défini dans un fichier séparé.
Voilà un exemple de découpage de la configuration précédente en plusieurs fichiers :
/etc/reaction/startstop.jsonnet
{
start: [
['ipset', '-exist', '-N', 'reactionv4', 'hash:net', 'family', 'inet'],
['iptables', '-w', '-I', 'INPUT', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
['iptables', '-w', '-I', 'FORWARD', '1', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
],
stop: [
['iptables', '-w', '-D', 'INPUT', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
['iptables', '-w', '-D', 'FORWARD', '-m', 'set', '--match-set', 'reactionv4', 'src', '-j', 'DROP'],
['ipset', '-X', 'reactionv4' ],
],
}
On a renommé le pattern myip en ip ici.
/etc/reaction/ip.yaml
patterns:
ip:
type: 'ipv4'
On peut écrire dans un fichier commençant par _ des définitions JSONnet qui ne seront pas chargées directement par reaction. Elles ne seront chargées que si explicitement importées dans un autre fichier :
/etc/reaction/_ban.jsonnet
local banFor(time) = {
ban: {
cmd: ['ipset', '-exist', '-A', 'reactionv4', '<ip>']
},
unban: {
after: time,
cmd: ['ipset', '-D', 'reactionv4', '<ip>']
},
};
banFor
/etc/reaction/ssh.jsonnet
local banFor = import '_ban.jsonnet';
{
streams: {
ssh: {
cmd: ['journalctl', '-n0', '-fu', 'sshd.service'],
filters: {
authfail: {
regex: [
'authentication failure;.*rhost=<ip>'
],
retry: 3,
retryperiod: '3h',
actions: banFor('24h'),
},
},
},
},
}
/etc/reaction/nginx.jsonnet
local banFor = import '_ban.jsonnet';
{
streams: {
nginx: {
cmd: ['tail', '-f', '/var/log/nginx/access.log'],
filters: {
directus: {
regex: [ @'^<ip> .* "POST /auth/login HTTP/..." 401', ],
retry: 6,
retryperiod: '4h',
actions: banFor('4h'),
},
},
},
},
}
Utilisation
- Étudier les logs avec
journalctl -f -u reaction.service. - Connaitre l’état actuel des bans avec
reaction show$ reaction show ssh: login: 4.3.2.1: actions: unban: - "2025-11-01 22:00:00" 112.113.114.115: actions: unban: - "2025-10-29 00:16:16" - Enlever un ban avec
reaction flush ip=4.3.2.1 - Tester ses regex avec
reaction test-regex - Consulter l’aide pour toutes ces commandes avec
reaction help
Conclusion
C’est bon ! Tu peux maintenant explorer le reste de la doc, et chercher ce qui te manque.
Si tu écris de nouveaux filtres et actions pour reaction, pense à les ajouter ici ! Merci beaucoup !