L’objet de ce TP est de découvrir et mettre en œuvre des règles de filtrage Netfilter au travers de l’utilitaire iptables sur GNU/Linux.
iptables est une solution complète de pare-feu pour GNU/Linux, disponible depuis la version 2.4 du noyau. Il permet de faire du firewalling à états (stateful), de la translation de port et d'adresse (NAT/PAT), du filtrage au niveau 2 et beaucoup d'autres choses, comme la modification des paquets à la volée.
iptables est fiable et dispose de très nombreuses options qui permettent de faire du filtrage très fin.
Le module qui fournit au noyau Linux les fonctions de pare-feu, de partage de connexions internet (NAT) et d'historisation du trafic réseau s'appelle Netfilter. Le rôle d’iptables est de permettre à un administrateur de configurer Netfilter.
Iptables ne concerne que IPv4 ; un outil similaire existe pour gérer les flux IPv6 : ip6tables, présent par défaut dans les distributions récentes.
Nous allons tout d’abord créer une VM GNU/Linux Debian en mode core (pas de GUI), avec les caractéristiques suivantes : 1 CPU, 2048 Mo RAM, 1 HDD 20 GB, 1 interface réseau bridgée sur la carte physique connectée à Internet.
Quelques rappels sur l’installation d’une Debian core depuis un support d’installation ISO « netinst » :
Pour des informations détaillées sur cette installation, consultez le tutoriel complet disponible ici.
Une fois l'OS installé et la VM redémarrée, on procède à quelques réglages et contrôles d'usage. Les commandes suivantes seront réalisées en mode console, connecté avec l'utilisateur root.
On vérifie la connectivité à Internet :
ping 1.1.1.1
et que la résolution de noms (DNS) est fonctionnelle :
ping debian.org
Mise à jour des dépôts et des paquets :
apt update
apt upgrade
On installe les outils dont nous aurons besoin :
apt install wget ssh sudo
On repère le nom de l'interface réseau et l'adresse ip :
ip -c address
Dans l'exemple ci-dessus nous avons :
On ajoute l'utilisateur standard (pascal dans l'exemple) au groupe des sudoers :
adduser pascal sudo
Puis on peut libérer la console :
exit
La VM étant connectée en mode "bridge", on peut utiliser notre terminal préféré pour nous connecter dessus en ssh :
ssh pascal@192.168.68.112
Par exemple avec MobaXterm :
Première connexion à un nouveau serveur : on l'ajoute dans la liste des serveurs connus (known hosts).
Découvrons désormais le fonctionnement de l’utilitaire iptables.
Vérifions d'abord que iptables est installé :
apt list iptables
Si le paquet est effectivement installé, la mention [installé] doit s’afficher en fin de ligne de réponse. S’il n’est pas présent, il suffit de l’installer, par un simple
sudo apt install iptables
l’option -L
(ou --list
) permet d’afficher l’état du pare-feu. Sans autre option, elle affiche les règles de la table filter :
sudo iptables -L
L’option -t
(ou --table
) permet de sélectionner la table. Par défaut, c’est la table filter qui est affichée. La commande précédente est donc identique à celle-ci :
sudo iptables -t filter -L
Les trois chaînes de la tables filter doivent apparaître : INPUT, FORWARD et OUTPUT, avec notamment leur règle par défaut (ACCEPT) :
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Les chaînes des tables nat et mangle seront affichées avec ces options :
sudo iptables -t nat -L
sudo iptables -t mangle -L
La table nat
intègre les chaînes PREROUTING, INPUT, OUTPUT et POSTROUTING
La table mangle
intègre toutes les chaînes PREROUTING, INPUT, FORWARD, OUTPUT et POSTROUTING
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
Pour afficher seulement les règles d’une certaine chaîne, on peut la fournir en argument, par exemple pour la chaine INPUT
(attention à la casse) :
sudo iptables -t filter -L INPUT
Chain INPUT (policy ACCEPT)
target prot opt source destination
En pratique, on utilise la combinaison d’options suivante pour afficher l’état du pare-feu :
-v
(ou --verbose
) permet d’afficher plus de détails (mode verbeux)-n
(ou --numeric
) peut s’avérer pratique dans la mesure où elle permet un formatage numérique de l’affichage pour les adresses IP et les ports.Ce qui donne (en ajoutant les deux options -v
et -n
en mode condensé, avec la table filter
par défaut) :
sudo iptables -L -vn
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Dans certains cas, on ajoutera l’option
--line-numbers
pour ajouter des numéros de ligne au début de chaque règle, ce qui permet d’identifier sa position dans la chaîne.
L’option -P
(ou --policy
) permet de définir la politique par défaut. Pour commencer, nous allons bloquer (DROP
) les connexions entrantes (chaîne INPUT
) :
sudo iptables -P INPUT DROP
Notre connexion ssh est une connexion entrante, nous avons donc perdu le contact ! Voilà un exemple d'une triste expérience de manipulation de pare-feu (couper la branche sur laquelle on était assis). Dans la pratique, il faut être très vigilants sur les actions de ce genre.
Depuis la console de notre VM (heureusement elle n'est pas trop loin), nous allons rétablir l'accès SSH par une règle spécifique (connexion en root) :
iptables -A INPUT -p tcp -i enp0s3 --dport 22 -j ACCEPT
Cette règle est établie dans la table filter
(par défaut), sur la chaîne INPUT
, elle concerne le protocole tcp
, port 22
, sur l'interface enp0s3
; les paquets filtrés (concernés) par cette règle sont dirigés (-j
pour jump) vers la cible ACCEPT
.
Nous pouvons maintenant rétablir la connexion SSH depuis notre terminal :
ssh pascal@192.168.68.112
et visualiser la règles en action :
sudo iptables -L -vn
Chain INPUT (policy DROP 35 packets, 11127 bytes)
pkts bytes target prot opt in out source destination
89 5000 ACCEPT 6 -- enp0s3 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Mais notre machine est par ailleurs injoignable depuis l’extérieur (en IPv4). Elle devient également injoignable pour elle-même :
ping -4 localhost
On va autoriser tous les paquets entrants sur la boucle locale (interface lo
) :
sudo iptables -A INPUT -i lo -j ACCEPT
-A
(ou --append
) permet d’ajouter une règle à la fin de la chaîne sélectionnée.-i
(ou --in-interface
) permet de spécifier l’interface réseau par laquelle un paquet a été reçu.-j
(ou --jump
) spécifie la cible de la règle, c’est-à-dire ce qu’il faut faire si le paquet correspond à la règle (de base on a : ACCEPT
, DROP
, REJECT
; mais on peut définir d’autres cibles peronnalisées.)On obtient donc ceci (on ne veut afficher que la chaîne INPUT) :
sudo iptables -L INPUT -vn
Chain INPUT (policy DROP 74 packets, 16098 bytes)
pkts bytes target prot opt in out source destination
119 6536 ACCEPT 6 -- enp0s3 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
0 0 ACCEPT 0 -- lo * 0.0.0.0/0 0.0.0.0/0
Ceci rétablit donc le ping (icmp) vers la machine elle-même (loopback)
ping -4 localhost
On peut finalement décider d’autoriser uniquement les pings en provenance de l’extérieur. Pour ce faire, on pourrait par exemple autoriser le protocole icmp
en utilisant l’option -p
(ou --protocol
) :
sudo iptables -A INPUT -p icmp -j ACCEPT
Cependant, ceci est peut-être trop permissif. En effet, le protocole icmp
comprend toute une "panoplie" de types de paquets. La commande suivante permet de les afficher :
sudo iptables -p icmp -h | less
Nous n’allons autoriser que ce qu’il faut pour que la machine soit "pingable". Supprimons d’abord la règle précédente. Pour ce faire, dans un premier temps, on affiche les règles avec l’option --line-numbers
:
sudo iptables -L INPUT --line-numbers
Résultat :
Chain INPUT (policy DROP)
num target prot opt source destination
1 ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
2 ACCEPT all -- anywhere anywhere
3 ACCEPT icmp -- anywhere anywhere
Ensuite, on supprime la règle n°2 de la chaine INPUT avec l’option -D
(ou --delete
) :
sudo iptables -D INPUT 2
sudo iptables -L INPUT --line-numbers
La liste devient :
Chain INPUT (policy DROP)
num target prot opt source destination
1 ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
2 ACCEPT icmp -- anywhere anywhere
Il nous faut encore supprimer une règle, la seconde :
sudo iptables -D INPUT 2
Désormais il ne reste que notre règle SSH (que l'on va conserver ) :
sudo iptables -L INPUT -vn --line-numbers
Chain INPUT (policy DROP 117 packets, 28622 bytes)
num pkts bytes target prot opt in out source destination
1 489 31061 ACCEPT 6 -- enp0s3 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
Il nous reste ensuite à autoriser quatre types de paquets icmp
nécessaires pour que le ping fonctionne correctement.
Les commandes pour autoriser ces types de paquets seront celles-ci :
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
sudo iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
sudo iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
sudo iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
Le ping standard fonctionne :
ping -4 localhost
Et nous pouvons "monitorer" les quantités de paquets passés par les différentes règles (avec la très intéressante commande watch
):
sudo watch iptables -L INPUT -vn --line-numbers
Toutes les 2,0s: iptables -L INPUT -vn --line-numbers pminfo: Tue Jun 25 20:00:47 2024
Chain INPUT (policy DROP 327 packets, 54364 bytes)
num pkts bytes target prot opt in out source destination
1 1555 94090 ACCEPT 6 -- enp0s3 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
2 71 5964 ACCEPT 1 -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8
3 71 5964 ACCEPT 1 -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 0
4 0 0 ACCEPT 1 -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 11
5 0 0 ACCEPT 1 -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 3
Avant d’aller plus loin dans la définition des règles de filtrage, nous allons voir comment gérer (arrêter, démarrer) le pare-feu. Tel que nous l’utilisons, iptables n’est pas un service (daemon) qu’on peut démarrer et arrêter. C’est donc un peu plus compliqué d’arrêter le filtrage des paquets dans l’état actuel des choses.
Comme nous l’avons vu un peu plus haut, l’option -P
(ou --policy
) se charge de définir la politique par défaut. Puisque nous souhaitons arrêter le pare-feu, cela équivaut à définir partout une politique par défaut ACCEPT
.
Sur toutes les chaînes de la table filter
:
sudo iptables -t filter -P INPUT ACCEPT
sudo iptables -t filter -P OUTPUT ACCEPT
sudo iptables -t filter -P FORWARD ACCEPT
Sur toutes les chaînes de la table nat
:
sudo iptables -t nat -P PREROUTING ACCEPT
sudo iptables -t nat -P INPUT ACCEPT
sudo iptables -t nat -P OUTPUT ACCEPT
sudo iptables -t nat -P POSTROUTING ACCEPT
Et enfin sur toutes les chaînes de la table mangle
:
sudo iptables -t mangle -P PREROUTING ACCEPT
sudo iptables -t mangle -P INPUT ACCEPT
sudo iptables -t mangle -P FORWARD ACCEPT
sudo iptables -t mangle -P OUTPUT ACCEPT
sudo iptables -t mangle -P POSTROUTING ACCEPT
Ensuite, il faut remettre à zéro tous les compteurs de paquets et d’octets dans toutes les chaînes. Pour ce faire, on utilise l’option -Z
(ou --zero
) :
sudo iptables -t filter -Z
sudo iptables -t nat -Z
sudo iptables -t mangle -Z
Il ne reste plus qu’à supprimer toutes les règles et toutes les chaînes.
-F
(ou --flush
) se charge de supprimer les chaînes d’une table.-X
(ou --delete-chain
) supprime les chaînes personnalisées définies par l’utilisateur.Pour supprimer tous les jeux de règles sur toutes les tables, on appelle donc la série de commandes suivante :
sudo iptables -t filter -F
sudo iptables -t filter -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X
Maintenant, le pare-feu est réinitialisé, regardons par exemple la table filter
:
sudo iptables -t filter -L -vn --line-numbers
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Toutes ces règles de filtrage sont sans aucun doute fastidieuses à saisir une par une. Pour cette raison, il est utile de consigner toutes ces commandes dans un script avant d’aller plus loin.
Placez-vous dans /usr/local/sbin/ :
cd /usr/local/sbin
Editez le fichier firewall.sh
:
sudo nano firewall.sh
Voici le contenu du script à compléter (là où il y a des _
), en s’inspirant de tout ce qui a été évoqué plus haut :
#!________
# Créateur :
# date :
#
# variable désignant le chemin absolu du binaire iptables
IPT=/usr/sbin/__________
# Tout accepter
$IPT -t filter -P ______ ________
$IPT -t ______ -P ______ ________
$IPT -t ______ -P ______ ________
$IPT -t nat -P ______ ________
$IPT -t ___ -P ______ ________
$IPT -t ___ -P ______ ________
$IPT -t ___ -P ______ ________
$IPT -t mangle -P ______ ________
$IPT -t ______ -P ______ ________
$IPT -t ______ -P ______ ________
$IPT -t ______ -P ______ ________
$IPT -t ______ -P ______ ________
# Remettre les compteurs à zéro
$IPT -t filter -Z
$IPT -t nat -Z
$IPT -t mangle -Z
# Supprimer toutes les règles actives et les chaînes personnalisées
$IPT -t filter -F
$IPT -t filter -X
$IPT -t nat -F
$IPT -t nat -X
$IPT -t mangle -F
$IPT -t mangle -X
# Politique par défaut (DROP sur INPUT et FORWARD, ACCEPT sur OUTPUT)
$IPT -P ______ ____
$IPT -P ______ ____
$IPT -P ______ ______
# autoriser ce qui provient de l’interface locale (lo)
$IPT -A _____ -i __ -j ______
# Autoriser le ping restreint
$IPT -A _____ -p _____ --icmp-type _______________ -j ______
$IPT -A _____ -p _____ --icmp-type _______________ -j ______
$IPT -A _____ -p _____ --icmp-type _______________ -j ______
$IPT -A _____ -p _____ --icmp-type _______________________ -j ______
# Autoriser la connexion SSH depuis une seule IP
$IPT -A INPUT -p tcp -i ______ --dport __ -s ___.___.___.___ -j ACCEPT
Dans un premier temps, le script prend soin de supprimer toutes les règles existantes.
Notez que la politique par défaut consiste à rejeter (DROP) les paquets entrants (INPUT) et relayés (FORWARD) et à accepter (ACCEPT) les paquets sortants. C’est une politique courante par défaut pour les firewalls.
Pour l’instant, la machine ne peut pas faire grand-chose, si ce n’est se connecter à elle-même et accepter le ping depuis l’extérieur, ainsi que l'accès SSH.
Pour plus de sécurité, l’accès SSH sera limité à une certaine IP source avec l’option -s
(ou --source
) ; remplacer ip_source
par l’adresse IPv4 effective de la machine autorisée à se connecter (probablement votre PC hôte).
Définissons maintenant les permissions adéquates sur le fichier firewall.sh
pour le rendre exécutable:
sudo chmod u+x firewall.sh
Et lançons le script :
sudo /usr/local/sbin/firewall.sh
Puis affichons les règles du pare-feu :
sudo iptables -L -vn
Vérifions depuis une machine extérieure si la machine se comporte bien comme prévu. Normalement, on devrait pouvoir envoyer un ping. En revanche, il ne sera pas possible de se connecter via SSH, sauf depuis l'adresse IP autorisée.
Il est possible aussi de vérifier en scannant les ports accessibles avec l'outil nmap
(comparer avec et sans pare-feu)
À présent, redémarrons la machine et affichons à nouveau les règles du pare-feu.
sudo reboot
Nous constaterons qu’elles ne sont plus en vigueur.
La prochaine étape consiste à rendre les règles de filtrage persistantes. Le moyen le plus simple et le plus sûr consiste ici à utiliser le paquet iptables-persistent
:
sudo apt install iptables-persistent
NB : lors de l’installation il vous est demandé si vous voulez sauvegarder les règles actuelles, répondez NON pour IPv4 et IPv6
Les règles de pare-feu sont enregistrées et chargées au démarrage dans les fichiers :
Pour rendre persistantes les règles de notre script, il faut tout d’abord les activer en lançant notre script :
sudo /usr/local/sbin/firewall.sh
Puis sauvegarder les règles actives vers le fichier de configuration :
su -
iptables-save > /etc/iptables/rules.v4
exit
Observons le nouveau fichier de configuration :
sudo nano /etc/iptables/rules.v4
On constate que le fichier généré a une structure plus élaborée que notre script initial.
Redémarrer la machine et vérifier que les règles sont bien présentes.
Notre pare-feu est désormais plus confortable à manier. Nous avons désormais un script pour le modifier, le démarrer ou l’arrêter, et une méthode simple pour rendre la configuration persistante après un redémarrage. Nous allons ajouter quelques règles de filtrage pour disposer d’un pare-feu opérationnel.
Même si notre pare-feu autorise par défaut les connexions sortantes, une requête sur internet de notre machine ne va pas pas aboutir. Essayons par exemple de charger une page web :
wget google.fr
La requête va se figer rien que sur la résolution du nom (requête DNS).
En effet, une connexion sortante appelle (en général) des réponses en retour, ce sont précisément ces réponses - entrantes - qui vont être bloquées par nos règles actuelles. Nous allons donc ajouter une règle qui autorise les paquets provenant de connexions déjà établies :
sudo iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT
Essayons de nouveau :
wget google.fr
Le résultat est plus satisfaisant :
Résolution de google.fr (google.fr)… 142.250.178.131, 2a00:1450:4007:819::2003
Connexion à google.fr (google.fr)|142.250.178.131|:80… connecté.
requête HTTP transmise, en attente de la réponse… 301 Moved Permanently
Emplacement : http://www.google.fr/ [suivant]
--2024-MM-JJ hh:mm:ss-- http://www.google.fr/
Résolution de www.google.fr (www.google.fr)… 172.217.20.163, 2a00:1450:4007:81a::2003
Connexion à www.google.fr (www.google.fr)|172.217.20.163|:80… connecté.
requête HTTP transmise, en attente de la réponse… 200 OK
Taille : non indiqué [text/html]
Sauvegarde en : « index.html.1 »
index.html.1 [ <=> ] 19,40K --.-KB/s ds 0s
2024-MM-JJ hh:mm:ss (118 MB/s) - « index.html.1 » sauvegardé [19868]
Ajoutons cette règle dans notre script, ainsi qu’une commande supplémentaire pour sa persistance (via une variable IPTS placée au début du script) :
...
IPT=/usr/sbin/__________
IPTS=/usr/sbin/iptables-save
...
...
# Accepter les connexions établies
$IPT -A INPUT -m state --state ESTABLISHED -j ACCEPT
# Autoriser le ping restreint
$IPT -A …
$IPT -A …
$IPT -A …
$IPT -A …
# Enregistrer la configuration
$IPTS > /etc/_______/_______.v4
Vérifiez quels sont les sockets réseau ouverts sur la machine :
ss -ntaul
Essayez de vous connecter en SSH depuis plusieurs machines ; vérifier que seule dont l’IP est autorisée peut se connecter.
On reprend cette commande qui permet de monitorer iptables :
sudo watch iptables -L -vn
Regardez l’évolution des différents compteurs selon les commandes effectuées (ping, ssh, http, …) depuis des machines externes.