Reaction à la maison
-------------------------------------------------
[28/06/2025] - ~8mins - #linux #réseau #adminsys
-------------------------------------------------
J'ai toujous eu un LAN très openbar.
Je pense avoir un semblant de sécurité et donc j'ai jamais vraiment cherché à le blinder.
Mais ces derniers temps je commence à réaliser que ça me bouffe pas mal de ressources pour du vent.
C'est notamment les gaveurs d'IA qui me font bien chier.
J'ai beau ne pas leur répondre au niveau de nginx, il n'en reste pas moins qu'il établissent une connexion et font des tentatives et tout et j'ai envie de les bloquer plus vite.
Du coup je voulais un truc qui bouffe des logs et qui réagisse en fonction de ce qui l'y trouve.
Mais j'aime vraiment pas Fail2Ban.
Déjà parceque c'est en python mais surtout parceque les confs fournies sont affreuses.
Mais ces derniers temps il y a une nouvelle alternative qui commence à émerger : **Reaction [1]** !
Initialement en Go, sa v2 qui arrive est une réécriture en rust.
Le truc se configure avec une conf en yaml ou en jsonnet (boarf) ou json (ouf).
Il se vend comme étant beaucoup plus léger et performant que fail2ban et il est présent dans les dépots testing d'alpine donc facilement installable.
Mais bon mon but ne va pas être de bloquer au niveau de la machine mais du réseau.
Il va donc faloir que reaction pousse ses blocages vers le firewall.
Archi
Donc sur mon LAN j'ai plusieurs machines qui hébergent des services.
Mais je centralise tous les logs avec **rsyslog** sur une seule (vraiment dès que vous avez plus de deux machines c'est pas mal du tout à mettre en place et c'est ultra simple).
C'est donc celle-ci qui va accueillir **reaction** puisqu'elle peut tout observer !
Quand reaction va vouloir bloquer un truc, il va lancer une commande sur le routeur où se trouve le firewall via **ssh**.
(ha et je le pousse également vers un VPS externe pour le protéger par la même occase)
Et … voilà.
Firewall
Mon routeur est un **Turris Omnia** tournant sous **OpenWRT**.
C'est du linux mais avec quelques subtilités.
Une d'entre elle c'est d'avoir une conf de firewall qui me plaît pas.
Du coup j'utilise du firewall à ma sauce à base de **nft** et de script **init** pour le mettre en place.
Ouai c'est pas la mode mais ça marche très bien.
Mais le truc c'est que j'en ai profité pour moderniser la conf pour être un peu plus dualstack (ça l'était déjà mais avec tout dédoublé en ipv4 et ipv6 et là je suis passé en "inet" pour pouvoir presque mélanger les deux réseaux).
J'ai donc converti mes règles de nft add chain ip … en nft add chain inet ….
Rien de bien méchant.
Et la deuxième modif a été d'adapter les règles de dnat de nft add rule inet nat PREROUTING ip daddr $WAN_IP tcp dport 80 counter dnat to $DESTINATION en nft add rule inet nat PREROUTING ip daddr $WAN_IP tcp dport 80 counter dnat ip to $DESTINATION (le ptit "ip" en plus après le "dnat").
Et du coup plus besoin de tables séparées pour ipv4 et ipv6.
Bon maintenant que je vais avoir des ip à bloquer à la pelle, je vais ranger tout ça dans 2 sets : 1 pour les ipv4 et 1 pour les ipv6.
Et donc je me suis repenché sur le wiki de nftables et notamment la page des sets [2].
Et j'ai découvert qu'on peut faire en sorte que les règles aient une durée de vie limitée !
Encore plus simple que prévu !
Du coup, dans mon script d'initialisation j'ai mis ces lignes :
{{}}
REACTION
nft add set inet filter blackholev4 {'type ipv4_addr ; flags interval,timeout ; counter ; auto-merge ; comment "Rempli par reaction"; '}
nft add set inet filter blackholev6 {'type ipv6_addr ; flags interval,timeout ; counter ; auto-merge ; comment "Rempli par reaction"; '}
nft add rule inet filter FORWARD ip saddr @blackholev4 drop
nft add rule inet filter FORWARD ip6 saddr @blackholev6 drop
{{}}
Voilà, on crée donc deux sets pour accueillir des ips avec des compteurs pour voir les stats et un timeout pour que les règles aient une durée de vie.
helper
Reaction est concon : il lit des logs et déclenche des actions.
C'est tout ce qu'on lui demande.
Mais du coup reaction il va choper des adresses IPv4 et des adresses IPv6 sauf que le firewall gère les règles différemment entre ces deux protocoles.
Pour ça, ils fournissent un ptit outil nommé **nft46**.
Sauf qu'il est pas dispo pour OpenWRT.
Et bon pendant cinq minutes j'ai envisagé de l'utiliser mais pas eu envie de crosscompiler et tout (c'est pas aussi simple que du Go).
Mais du coup, j'ai fait mon propre helper en Go !
Donc la façon dont je vois le truc : **reaction** donne une ip, l'helper se débrouille pour déterminer le type d'IP.
En fonction de ça, il lance une commande **nft** pour ajouter l'ip dans le bon set.
C'est tout.
{{}}
package main
import (
_ "embed"
"flag"
"fmt"
"net"
"os"
"os/exec"
"strings"
"syscall"
)
//go:embed nft-ban.go
var source string
var viewsource bool = false
func main() {
if len(os.Args) != 2 {
fmt.Println("nft-ban: Donnez une adresse IP ou -source pour voir les sources")
os.Exit(1)
}
flag.BoolVar(&viewsource, "source", false, "View source")
flag.Parse()
if viewsource {
fmt.Print(source)
os.Exit(0)
}
commande := []string{"add", "element", "inet", "filter"}
address := os.Args[1]
switch {
case IsIPv4(address):
commande = append(commande, "blackholev4", "{", address, "timeout", "2d", "}")
case IsIPv6(address):
commande = append(commande, "blackholev6", "{", address+"/64", "timeout", "2d", "}")
default:
fmt.Println(address, " n'est pas une adresse IP.")
os.Exit(1)
}
env := os.Environ()
binary, lookErr := exec.LookPath("nft")
if lookErr != nil {
panic(lookErr)
}
execErr := syscall.Exec(binary, commande, env)
if execErr != nil {
fmt.Println("\n", commande)
panic(execErr)
}
}
func IsIPv4(str string) bool {
ip := net.ParseIP(str)
return ip != nil && strings.Contains(str, ".")
}
func IsIPv6(str string) bool {
ip := net.ParseIP(str)
return ip != nil && strings.Contains(str, ":")
}
{{}}
À compiler avec CGO_ENABLED=0 GOARCH=arm GOARM=7 go build et à pousser sur le routeur.
Bon c'est des morceaux rapiécés de droite et de gauche mais ça fonctionne et ça se cross-compile pour le routeur !
Et le pire c'est que ça fonctionne.
C'est pas strictement identique à **nft46** et pas du tout flexible (tout du moins pas sans recompiler) mais pour mon usage perso ça va le faire.
Reaction itself
Bon j'ai le firewall prêt, le helper également, reste plus qu'à bidouiller **reaction**.
Bon on installe ça avec apk add reaction@testing et ensuite on édite le fichier de conf.
{{}}
---
definitions:
- &turris_ban [ "ssh", "root@fayawool" ,"nft-ban" ,""]
- &vps_ban [ "ssh", "root@vps" ,"nft-ban" ,""]
- &tonib_notif [ "tonib" , "[REACTION] BAN " ]
patterns:
ip:
#regex plus simple mais moins correcte
#regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})'
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:
- 127.0.0.1
- ::1
- 10.0.0.254
streams:
ssh:
cmd: [ 'tail', '-Fn0', '/var/log/auth.log' ]
filters:
failedlogin:
regex:
- 'authentication failure;.*rhost='
- 'Failed password for .* from port .*'
- 'Invalid user .* port .*'
- 'Timeout before authentication .* port .*'
- 'Connection reset by authenticating user .* port .*'
retry: 3
retryperiod: 6h
actions:
turris_ban:
cmd: *turris_ban
vps_ban:
cmd: *vps_ban
web-bots:
cmd: [ 'tail', '-Fn0', '/var/log/nginx/lord.re.access.log' ]
filters:
aiBots:
regex:
#Les IP qui se prennent une erreur 402 de la part de nginx (les gaveurs d'IA).
- '^ .*402 160*'
actions:
ban:
cmd: *turris_ban
vps_ban:
cmd: *vps_ban
mail-postfix:
cmd: [ 'tail', '-Fn0', '/var/log/172.16.1.254/postfix.log' ]
filters:
dnsbl:
regex:
- 'dnsblog.* addr listed by*'
retry: 4
retryperiod: 1h
actions:
ban:
cmd: *turris_ban
vps_ban:
cmd: *vps_ban
mail-dovecot:
cmd: [ 'tail', '-Fn0', '/var/log/172.16.1.254/dovecot.log' ]
filters:
failedlogins:
regex:
- 'imap-login: Login aborted: Too many invalid commands.* rip=.*'
retry: 3
retryperiod: 1h
actions:
ban:
cmd: *turris_ban
vps_ban:
cmd: *vps_ban
{{}}
Voilà c'est une config un peu simple que j'étofferais probablement avec le temps pour protéger plus de services.
C'est pas graph !
Bon c'est cool mais ça manque de courbes et de points qui bougent !
Du coup j'ai eu envie de nourrir mon ptit **grafana** et son **influxdb**.
Pour ça, un simple petit script à éxecuter de temps à autre via cron.
{{}}
#! /bin/sh
blv4=$(nft list set inet filter blackholev4 | grep -c expires)
blv6=$(nft list set inet filter blackholev6 | grep -c expires)
wire_protocol=$( printf "reaction_stats,host=FayaWool blv4=%si,blv6=%si" "$blv4" "$blv6" )
curl -i -XPOST -u login:pass "https://l.influx.db/write?db=perso" --data-binary "$wire_protocol"
{{}}
Restera plus qu'a se faire un ptit panel pour voir l'évolution.
C'est tout ?!
Bha oui.
Je vous avais dit que c'était facile.
Au final j'oscille autour des 350 IPv4 blacklistées et autour des 30 IPv6.
Pour les IPv6 j'hésite d'ailleurs à réduire un peu le masque (probablement à 56 au lieu de 64).
Mais bon, au vu de ce que je constate, il n'y a que facebook qui vient me faire chier et je pourrai les bloquer une bonne fois pour toutes avec un /29.
Ça faisait vraiment très longtemps que je m'étais pas replongé dans le firewall et tout.
C'était cool de se remettre un peu dedans, couplé avec un peu de grafana et du Go, je me suis bien amusé !
Liens
[1] Reaction (https://framagit.org/ppom/reaction)
[2] la page des sets (https://wiki.nftables.org/wiki-nftables/index.php/Sets)
------------------------------------
------------------------------------
[28/06/2025] - #linux #réseau #adminsys
------------------------------------
[>> Suivant >>] ⏭ DeepStar Six
[<< Précédent <<] ⏮ Across 110th Street
Source