Les ensembles IP revisités
Il existe déjà une réponse mentionnant les ensembles IP. Cependant, elle est plutôt unidimensionnelle dans la mesure où elle se concentre sur les gains de performance par rapport aux règles classiques et sur le fait que les ensembles IP atténuent le problème que l'on rencontre avec de nombreuses adresses IP individuelles qui ne peuvent pas être facilement exprimées sous forme de sous-réseau dans la notation CIDR.
Notation utilisée ci-dessous
Pour ipset
Je vais utiliser la notation lue par ipset restore
et écrit par ipset save
.
En conséquence, pour iptables
(y ip6tables
), je vais utiliser la notation telle que lue par iptables-restore
et écrit par iptables-save
. Cela permet de raccourcir la notation et de mettre en évidence les possibilités d'utilisation d'IPv4 uniquement (préfixe -4
) ou IPv6 uniquement (préfixe -6
).
Dans certains exemples, nous détournerons le flux de paquets vers une autre chaîne. La chaîne est supposée exister à ce moment-là, donc les lignes pour créer les chaînes ne sont pas produites (le nom de la table n'est pas mentionné, ni les commandes COMMIT
-à la fin).
Jeux IP avancés
Les ensembles IP peuvent faire beaucoup plus que ce qui a été mentionné dans le document l'autre réponse et vous devez absolument lire la documentation sur le jeu d'IP ( ipset(8)
) ainsi que iptables-extensions(8)
en plus de cette brève entrée ici.
Par exemple, je me concentrerai principalement sur trois types d'ensembles : hash:ip
, hash:net
y list:set
mais il y en a d'autres et ils ont tous des cas d'utilisation valables.
Par exemple, vous pouvez également faire correspondre les numéros de port, et pas seulement les adresses IP .
Sauvegarde et restauration des jeux d'adresses IP comme avec iptables-save
y iptables-restore
Vous pouvez créer des déclarations d'ensembles d'IP en vrac et les importer en les introduisant dans le fichier ipset restore
. Si vous voulez rendre votre commande plus résistante aux entrées déjà existantes, utilisez ipset -exist restore
.
Si vos règles se trouvent dans un fichier appelé default.set
que vous utiliseriez :
ipset -exist restore < default.set
Un tel fichier peut contenir des entrées pour create
et à add
des entrées en leur sein. Mais en général, la plupart des commandes de la ligne de commande semblent avoir une version correspondante dans les fichiers. Exemple (création d'un ensemble de serveurs DNS) :
create dns4 hash:ip family inet
create dns6 hash:ip family inet6
# Google DNS servers
add dns4 8.8.8.8
add dns4 8.8.4.4
add dns6 2001:4860:4860::8888
add dns6 2001:4860:4860::8844
Ici, un ensemble est créé pour IPv4 ( dns4
) et un pour IPv6 ( dns6
).
Délais d'attente sur les postes IP
Les délais d'attente dans les ensembles IP peuvent être définis par défaut par ensemble et aussi par entrée. Ceci est très utile pour les scénarios où vous voulez bloquer quelqu'un temporairement (par exemple, pour le balayage de port ou la tentative de forçage brutal de votre serveur SSH).
La façon dont cela fonctionne est la suivante (par défaut lors de la création des ensembles d'IP) :
create ssh_loggedon4 hash:ip family inet timeout 5400
create ssh_loggedon6 hash:ip family inet6 timeout 5400
create ssh_dynblock4 hash:ip family inet timeout 1800
create ssh_dynblock6 hash:ip family inet6 timeout 1800
Nous reviendrons plus loin sur ces ensembles particuliers et sur les raisons pour lesquelles ils sont fixés de la sorte.
Si vous voulez définir votre délai d'attente pour une adresse IP particulière, vous pouvez simplement dire :
add ssh_dynblock4 1.2.3.4 timeout 7200
Pour bloquer l'IP 1.2.3.4 pendant deux heures au lieu de la demi-heure (fixée) par défaut.
Si vous deviez regarder ça avec ipset save ssh_dynblock4
après un court moment, vous verrez quelque chose du genre :
create ssh_dynblock4 hash:ip family inet hashsize 1024 maxelem 65536 timeout 1800
add ssh_dynblock4 1.2.3.4 timeout 6954
Avertissements sur les délais d'attente
- Les délais d'attente sont une caractéristique de tout poste donné. Si le poste n'a pas été créé avec prise en charge du délai d'attente, vous recevrez une erreur (par ex.
Kernel error received: Unknown error -1
).
- Les délais sont donnés en secondes. Utilisez les expressions arithmétiques de Bash pour passer des minutes aux secondes, par exemple. Par exemple :
sudo ipset add ssh_dynblock4 1.2.3.4 timeout $((120*60))
Vérifier si une entrée existe dans un jeu d'IP donné
A l'intérieur de vos scripts, il peut être utile de voir si une entrée existe déjà. Ceci peut être réalisé avec ipset test
qui renvoie un zéro si l'entrée existe et un non-zéro sinon. Ainsi, les vérifications habituelles peuvent être appliquées dans un script :
if ipset test dns4 8.8.8.8; then
echo "Google DNS is in the set"
fi
Cependant, dans de nombreux cas, vous préférerez utiliser la fonction -exist
passer à ipset
afin de lui demander de ne pas se plaindre des entrées existantes.
Remplissage de jeux d'adresses IP à partir de iptables
règles
C'est, à mon avis, l'une des caractéristiques les plus intéressantes des ensembles IP. Non seulement vous pouvez comparer les entrées d'un jeu d'adresses IP, mais vous pouvez également ajouter de nouvelles entrées à un jeu d'adresses IP existant.
Par exemple dans cette réponse à la question que vous vous posez :
-A INPUT -p tcp -i eth0 -m state --state NEW --dport 22 -m recent --update --seconds 15 -j DROP
-A INPUT -p tcp -i eth0 -m state --state NEW --dport 22 -m recent --set -j ACCEPT
... dans le but de limiter le nombre de tentatives de connexion à SSH (port TCP 22). Le module utilisé recent
conserve la trace des dernières tentatives de connexion. Au lieu de la state
je préfère le module conntrack
cependant.
# Say on your input chain of the filter table you have
-A INPUT -i eth+ -p tcp --dport ssh -j SSH
# Then inside the SSH chain you can
# 1. create an entry in the recent list on new connections
-A SSH -m conntrack --ctstate NEW -m recent --set --name tarpit
# 2. check whether 3 connection attempts were made within 2 minutes
# and if so add or update an entry in the ssh_dynblock4 IP set
-4 -A SSH -m conntrack --ctstate NEW -m recent --rcheck --seconds 120 --hitcount 3 --name tarpit -j SET --add-set ssh_dynblock4 src --exist
-6 -A SSH -m conntrack --ctstate NEW -m recent --rcheck --seconds 120 --hitcount 3 --name tarpit -j SET --add-set ssh_dynblock6 src --exist
# 3. last but not least reject the packets if the source IP is in our
# IP set
-4 -A SSH -m set --match-set ssh_dynblock4 src -j REJECT
-6 -A SSH -m set --match-set ssh_dynblock6 src -j REJECT
Dans ce cas, je redirige le flux vers le fichier SSH
de telle sorte que je ne doive pas me répéter avec des -p tcp --dport ssh
pour chaque règle.
Je le répète :
-
-m set
fait iptables
conscient que nous utilisons des commutateurs de la set
(qui gère les ensembles IP)
-
--match-set ssh_dynblock4 src
dit à iptables
pour correspondre à la source ( src
) par rapport à l'ensemble nommé ( ssh_dynblock4
)
- cela correspond à
sudo ipset test ssh_dynblock4 $IP
(où $IP
contient l'adresse IP source du paquet)
-
-j SET --add-set ssh_dynblock4 src --exist
ajoute ou met à jour le source ( src
) du paquet dans l'ensemble IP ssh_dynblock4
. Si une entrée existe ( --exist
), il sera simplement mis à jour.
- cela correspond à
sudo ipset -exist add ssh_dynblock4 $IP
(où $IP
contient l'adresse IP source du paquet)
Si vous vouliez faire correspondre l'adresse de la cible/destination à la place, vous utiliseriez dst
au lieu de src
. Consultez le manuel pour plus d'options.
Ensembles d'ensembles
Les ensembles IP peuvent contenir d'autres ensembles. Si vous avez suivi l'article jusqu'ici, vous vous êtes sans doute demandé s'il était possible de combiner des ensembles. Et bien sûr, c'est possible. Pour les ensembles IP ci-dessus, nous pouvons créer deux ensembles conjoints ssh_dynblock
y ssh_loggedon
respectivement pour contenir les ensembles IPv4-only et IPv6-only :
create ssh_loggedon4 hash:ip family inet timeout 5400
create ssh_loggedon6 hash:ip family inet6 timeout 5400
create ssh_dynblock4 hash:ip family inet timeout 1800
create ssh_dynblock6 hash:ip family inet6 timeout 1800
# Sets of sets
create ssh_loggedon list:set
create ssh_dynblock list:set
# Populate the sets of sets
add ssh_loggedon ssh_loggedon4
add ssh_loggedon ssh_loggedon6
add ssh_dynblock ssh_dynblock4
add ssh_dynblock ssh_dynblock6
Et la prochaine question qui devrait surgir dans votre esprit est de savoir si cela nous permet de match et de manipuler les ensembles IP de manière agnostique par rapport à la version IP.
Et la réponse à cette question est sans appel : OUI ! (hélas, cela n'était pas documenté explicitement la dernière fois que j'ai vérifié)
Par conséquent, les règles de la section précédente peuvent être réécrites comme suit :
-A INPUT -i eth+ -p tcp --dport ssh -j SSH
-A SSH -m conntrack --ctstate NEW -m recent --set --name tarpit
-A SSH -m conntrack --ctstate NEW -m recent --rcheck --seconds 120 --hitcount 3 --name tarpit -j SET --add-set ssh_dynblock src --exist
-A SSH -m set --match-set ssh_dynblock src -j REJECT
ce qui est beaucoup plus concis. Et oui, cette méthode est éprouvée et fonctionne à merveille.
Tout mettre en place : Défense SSH par force brute
Sur mes serveurs, j'ai un script exécuté comme une cron
qui prend un tas de noms d'hôtes et les résout en adresses IP, puis les introduit dans le jeu d'IP pour les "hôtes de confiance". L'idée est que les hôtes de confiance reçoivent plus de tentatives de connexion au serveur et ne sont pas nécessairement bloqués aussi longtemps que les autres.
À l'inverse, j'ai bloqué la connexion de pays entiers à mon serveur SSH, à l'exception (potentielle) des hôtes de confiance (c'est-à-dire que l'ordre des règles compte).
Cependant, cela reste un exercice pour le lecteur. Ici, j'aimerais ajouter une solution soignée qui utilisera les ensembles contenus dans l'application ssh_loggedon
défini pour permettre aux tentatives de connexion ultérieures d'être transmises et de ne pas être soumises au même examen que les autres paquets.
Il est important de se rappeler que les délais par défaut de 90 minutes pour les services de l ssh_loggedon
et 30 minutes pour ssh_dynblock
en regardant les éléments suivants iptables
règles :
-A INPUT -i eth+ -p tcp --dport ssh -j SSH
-A SSH -m set --match-set ssh_loggedon src -j ACCEPT
-A SSH -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A SSH -m conntrack --ctstate NEW -m recent --set --name tarpit
-A SSH -m conntrack --ctstate NEW -m recent --rcheck --seconds 120 --hitcount 3 --name tarpit -j SET --add-set ssh_dynblock src --exist
-A SSH -m set --match-set ssh_dynblock src -j REJECT
A présent, vous devez vous demander comment l'adresse IP de connexion se retrouve dans le fichier ssh_loggedon
sous-ensembles. Lisez donc ce qui suit...
Bonus : ajout de l'IP avec laquelle vous vous connectez lors de la connexion SSH
Si vous avez expérimenté sshrc
et amis, vous aurez appris ses défauts. Mais PAM vient à la rescousse. Un module nommé pam_exec.so
nous permet d'invoquer un script pendant la connexion SSH à un moment où nous savons que l'utilisateur est admis.
Sur /etc/pam.d/sshd
sous le pam_env
y pam_selinux
ajoutez la ligne suivante :
session optional pam_exec.so stdout /path/to/your/script
et assurez-vous que votre version du script ( /path/to/your/script
ci-dessus) existe et est exécutable.
PAM utilise des variables d'environnement pour communiquer ce qui se passe, vous pouvez donc utiliser un simple script comme celui-ci :
#!/bin/bash
# When called via pam_exec.so ...
SETNAME=ssh_loggedon
if [[ "$PAM_TYPE" == "open_session" ]] && [[ -n "$PAM_RHOST" ]]; then
[[ "x$PAM_RHOST" != "x${PAM_RHOST//:/}" ]] && SETNAME="${SETNAME}6" || SETNAME="${SETNAME}4"
ipset -exist add $SETNAME "$PAM_RHOST"
fi
Malheureusement, le ipset
ne semble pas avoir l'intelligence intégrée de netfilter. Nous devons donc faire la distinction entre les ensembles IPv4 et IPv6 lorsque nous ajoutons notre entrée. Sinon, ipset
nous supposerons que nous voulons ajouter un autre set à l'ensemble des ensembles, au lieu de l'IP. Et bien sûr, il est peu probable qu'il y ait un ensemble portant le nom d'un IP :)
Nous vérifions donc :
dans l'adresse IP et ajoutez 6
au nom de l'ensemble dans ce cas et 4
autrement.
La fin.