Aller au contenu

Le pare-feu de l'hôte

À la fin, vous saurez

  • Expliquer le trajet d'un paquet à travers les crochets de Netfilter et le rôle du suivi de connexion dans un filtrage à état
  • Identifier l'outil qui pilote réellement le pare-feu d'une machine (ufw, nftables, iptables-nft ou iptables-legacy) et lire l'ensemble des règles actives
  • Écrire une politique d'entrée en refus par défaut avec ufw sur Ubuntu et avec nftables sur Debian, IPv4 et IPv6 compris
  • Modifier un pare-feu à distance avec un retour arrière automatique, et vérifier une configuration avant de l'appliquer
  • Diagnostiquer un flux bloqué à partir des compteurs, des journaux du noyau et de la trace nftables
  • Répartir le filtrage entre groupe de sécurité Scaleway, Network ACL et pare-feu de l'hôte, et repérer les contournements par Docker

Prérequis

Testé avec debian 13 iptables 1.8.10 / 1.8.11 nftables 1.0.9 / 1.1.3 noyau 6.8 / 6.12 ubuntu 24.04 ufw 0.36.2 , vérifié le 7 octobre 2026

Pourquoi

En faisant l'état des lieux de sig-app-1 (leçon 1), vous avez relevé dans ss -tulpn : Gunicorn sur 172.16.8.11:8000, SSH sur toutes les interfaces, et un docker-proxy sur le port 8080, reliquat d'un conteneur Adminer que Camille utilisait pour consulter la base. sudo ufw status répond Status: inactive. Sur sig-outils, sudo nft list ruleset ne renvoie rien.

Aucune des trois machines ne filtre quoi que ce soit. On pourrait s'en accommoder, puisque le groupe de sécurité Scaleway est strict. C'est là le malentendu : chez Scaleway, le groupe de sécurité ne filtre que le trafic public. Le trafic du réseau privé pn-signalements arrive sur les instances sans aucun contrôle. Une seule machine compromise dans ce réseau (une instance de test oubliée, un prestataire à qui l'on a ouvert un accès) peut joindre Adminer, tenter des mots de passe SSH sans limite, ou écrire dans le port de réception des journaux de sig-outils.

Le pare-feu de l'hôte est la dernière ligne de défense, et la seule qui voie tout ce qui arrive sur la machine, quelle que soit l'interface. Cette leçon le met en place sur les deux familles de machines dont vous avez hérité, montre comment le modifier sans vous enfermer dehors, et ce que d'autres programmes (Docker en tête) y font dans votre dos.

Les concepts

Netfilter : le filtrage est dans le noyau

Sous Linux, le filtrage n'est pas fait par un programme en arrière-plan mais par le noyau, dans un sous-système appelé Netfilter. ufw, iptables, nft ou firewalld ne filtrent rien : ils écrivent des règles dans le noyau, puis se terminent. C'est pourquoi systemctl status nftables affiche active (exited) : le service a chargé les règles au démarrage, il n'y a plus rien à surveiller.

Netfilter place des crochets (hooks, des points d'interception) sur le chemin des paquets. Toute règle est rattachée à l'un d'eux :

                          paquet entrant
                                |
                           [prerouting]
                                |
                       décision de routage
                      /                   \
          pour cette machine         pour une autre machine
                  |                          |
               [input]                   [forward]
                  |                          |
         processus locaux                    |
      (sshd, Gunicorn, rsyslog)              |
                  |                          |
              [output]                       |
                   \                        /
                    ----- [postrouting] ----
                                |
                          paquet sortant
  • prerouting voit tout ce qui arrive, avant la décision de routage ; c'est là que se fait la traduction d'adresse de destination (DNAT), celle de Docker pour ses ports publiés.
  • input voit les paquets destinés à un processus local : le crochet principal d'un pare-feu d'hôte.
  • forward voit ce qui traverse la machine quand elle route (net.ipv4.ip_forward=1, leçon 3). Un serveur d'application ne route rien ; un hôte Docker, si.
  • output voit ce qu'émettent les processus locaux : c'est là que se fait le filtrage en sortie.
  • postrouting voit tout ce qui sort, et accueille la traduction d'adresse source.

Le suivi de connexion, ou ce que veut dire « à état »

Un pare-feu sans état juge chaque paquet isolément : pour laisser entrer SSH, il faut aussi autoriser les réponses vers des ports clients imprévisibles, et pour qu'une requête DNS aboutisse, accepter tout ce qui vient du port 53, ce qu'un attaquant peut forger.

Netfilter tient un suivi de connexion (connection tracking, conntrack) : une table où chaque flux est identifié par son quintuplet et porte un état que les règles peuvent tester :

ÉtatSignification
newpremier paquet d'un flux inconnu
establishedle flux a déjà vu du trafic dans les deux sens
relatedflux lié à un flux existant, typiquement une erreur ICMP (« paquet trop gros ») concernant une connexion en cours
invalidpaquet incohérent (un ACK TCP pour une connexion inconnue)
untrackedpaquet exclu du suivi

Tout pare-feu d'hôte bien écrit commence donc ainsi : accepter established et related, jeter invalid, puis ne décider que des paquets new. Les réponses aux connexions ouvertes par la machine passent seules ; on n'écrit de règles que pour ce que l'on accepte de recevoir. UDP est suivi aussi : le noyau simule une connexion à partir du quintuplet et d'un délai d'expiration.

iptables, nftables

Pendant vingt ans, on a piloté Netfilter avec iptables : des tables fixes (filter, nat, mangle, raw), des chaînes prédéfinies (INPUT, FORWARD, OUTPUT), et un outil par famille (iptables pour IPv4, ip6tables pour IPv6). Une politique s'écrivait deux fois.

nftables, entré dans le noyau 3.13 en 2014, remplace ce moteur : un seul outil, nft, aucune table prédéfinie, et une famille inet qui couvre IPv4 et IPv6 dans la même table. Debian l'utilise par défaut depuis Debian 10, d'après son wiki ; Red Hat depuis RHEL 8.

La transition passe par une astuce : la commande iptables des distributions actuelles est iptables-nft, qui accepte l'ancienne syntaxe mais écrit des règles nftables. L'ancien moteur reste disponible sous le nom iptables-legacy, et update-alternatives choisit entre les deux. Sur Ubuntu 24.04 et Debian 13, c'est iptables-nft. Conséquence : ce qu'écrivent ufw, Docker ou fail2ban via iptables-nft apparaît dans nft list ruleset, alors que les règles d'iptables-legacy vivent dans un autre moteur, invisible pour nft.

Les outils frontaux

  • ufw (Uncomplicated Firewall), sur Ubuntu : installé sur Ubuntu Server mais inactif par défaut. Il pilote iptables et raisonne en règles simples (« autoriser le port 8000 depuis ce réseau »).
  • nftables natif, sur Debian : un fichier /etc/nftables.conf chargé au démarrage par nftables.service.
  • firewalld, sur Red Hat, Fedora ou SUSE : un démon qui raisonne en zones (un niveau de confiance associé à des interfaces ou des sources) et en services, sépare configuration courante et permanente, et s'appuie sur nftables. Exemple : firewall-cmd --permanent --zone=internal --add-service=ssh, puis firewall-cmd --reload.

Règle d'or : un seul outil par machine. ufw plus un fichier nftables écrit à la main, ce sont deux politiques qui se combinent d'une manière peu intuitive (voir Sous le capot).

Qui filtre quoi chez Scaleway

FiltreOùCe qu'il voitÀ état
Groupe de sécuritéhyperviseurle trafic public de l'instance seulementoui pour les groupes créés par vous, non pour le groupe automatique de la zone
Network ACLrouteur du VPCle trafic entre deux réseaux privésnon
Pare-feu de l'hôtenoyau de l'instancetout, toutes interfacesoui

La documentation Scaleway est explicite : les groupes de sécurité « only allow the filtering of public traffic », et pour le trafic privé il faut « configure a firewall directly on your Instance ». Entre sig-app-1 et sig-outils, dans le même réseau privé, seul le pare-feu de l'hôte intervient.

Les filtres extérieurs ont un avantage : une machine compromise avec les droits root peut vider son propre pare-feu, pas le groupe de sécurité. Les deux se complètent, c'est la défense en profondeur.

En pratique

Les sorties de cette section sont construites d'après le code source et la documentation des outils, pas capturées sur les machines de Signalements : le format est fidèle, les valeurs illustratives.

1. Savoir qui pilote le pare-feu

$ sudo iptables -V
$ update-alternatives --display iptables
$ sudo ufw status verbose
$ systemctl is-enabled nftables ufw firewalld docker 2>/dev/null
$ sudo nft list ruleset
  • iptables -V affiche le moteur entre parenthèses : iptables v1.8.10 (nf_tables) ou (legacy).
  • update-alternatives --display iptables dit vers quel programme pointe iptables.
  • systemctl is-enabled révèle les outils qui chargeront des règles au prochain démarrage.
  • nft list ruleset affiche tout le moteur nftables. Les tables créées par iptables-nft y portent l'avertissement # Warning: table ip filter is managed by iptables-nft, do not touch! : on les modifie avec l'outil qui les a créées, jamais avec nft.

Si iptables -L commence par # Warning: iptables-legacy tables present, use iptables-legacy to see them (texte tiré du code source d'iptables), un vieux script a écrit dans l'ancien moteur : inventoriez-le avec sudo iptables-legacy-save avant toute chose.

2. ufw sur sig-app-1 et sig-app-2

Besoins de sig-app-1 : SSH depuis le réseau privé (on y arrive par le bastion de la passerelle publique) et le port 8000 pour le répartiteur de charge, qui a une adresse dans pn-signalements. Tout le reste est refusé en entrée ; la sortie reste libre pour l'instant.

Le filet de sécurité d'abord. Vous êtes en SSH ; une règle fausse et vous perdez la main. Programmez la désactivation d'ufw dans cinq minutes, avant de toucher à quoi que ce soit :

$ sudo systemd-run --on-active=5min --unit=retour-pare-feu /usr/sbin/ufw disable
Running timer as unit: retour-pare-feu.timer
Will run service as unit: retour-pare-feu.service

systemd-run crée un minuteur temporaire qui se déclenchera cinq minutes après sa création (--on-active=, délai relatif ; minuteurs en leçon 10) et le service qui exécutera ufw disable. systemctl list-timers retour-pare-feu.timer affiche le temps restant.

La politique et les règles :

$ sudo ufw default deny incoming
Default incoming policy changed to 'deny'
(be sure to update your rules accordingly)
$ sudo ufw default allow outgoing
$ sudo ufw limit from 172.16.8.0/22 to any port 22 proto tcp comment 'SSH depuis pn-signalements'
Rules updated
$ sudo ufw allow from 172.16.8.0/22 to any port 8000 proto tcp comment 'Gunicorn, répartiteur'
Rules updated
  • deny jette sans réponse (DROP) ; reject renverrait un refus explicite. Ne rien répondre ralentit les balayages de ports.
  • limit autorise SSH mais refuse les connexions d'une adresse qui en ouvre 6 ou plus en 30 secondes. Le code d'ufw le traduit par le module recent d'iptables (--seconds 30 --hitcount 6) suivi d'un REJECT.
  • from 172.16.8.0/22 : une source IPv4, donc une règle IPv4 seulement. En IPv6, rien n'est autorisé, tout est refusé.
  • proto tcp : sans lui, ufw ouvre TCP et UDP.
  • comment : stocké avec la règle et affiché par status. Dans deux ans, il dira pourquoi la règle existe.

Tant qu'ufw est inactif, ces commandes écrivent seulement dans /etc/ufw/user.rules, d'où Rules updated. Relisez avec sudo ufw show added, puis :

$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

enable charge les règles et écrit ENABLED=yes dans /etc/ufw/ufw.conf, pour que ufw.service les recharge à chaque démarrage.

Tester depuis une nouvelle session. Votre session actuelle survivra à presque toute erreur : sa connexion est established, acceptée avant toute autre règle. Le vrai test est une seconde connexion SSH, plus curl -s http://172.16.8.11:8000/sante depuis sig-outils. Si tout répond : sudo systemctl stop retour-pare-feu.timer.

Lire l'état :

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     LIMIT IN    172.16.8.0/22              # SSH depuis pn-signalements
8000/tcp                   ALLOW IN    172.16.8.0/22              # Gunicorn, répartiteur

disabled (routed) signifie que la machine ne route pas. New profiles: skip concerne les profils d'application de /etc/ufw/applications.d, inutilisés ici. Une règle ouverte à tous (ufw allow 443/tcp) apparaîtrait deux fois, avec une ligne 443/tcp (v6).

sudo ufw status numbered numérote les règles pour ufw delete 2 ou ufw insert 1 .... La page ufw(8) le rappelle : « the first match wins ». Pour bloquer une adresse du réseau privé malgré l'autorisation générale, la règle doit donc venir avant :

$ sudo ufw insert 1 deny from 172.16.8.40 comment 'instance de test compromise, ticket 412'

Même procédure sur sig-app-2, idéalement écrite une seule fois dans l'outil d'automatisation (cours Ansible : les fondamentaux) pour que les deux machines ne divergent jamais.

3. Ce qu'ufw a réellement écrit

Les fichiers d'ufw, d'après ufw-framework(8) :

FichierRôle
/etc/default/ufwpolitiques par défaut, IPV6=yes
/etc/ufw/ufw.confENABLED, LOGLEVEL
/etc/ufw/before.rules, before6.rulesrègles évaluées avant les vôtres
/etc/ufw/user.rules, user6.rulesvos règles, écrites par la commande ufw : ne pas éditer
/etc/ufw/after.rules, after6.rulesrègles évaluées après les vôtres

before.rules contient la moitié de la politique, sans que ufw status en dise un mot : tout accepter sur lo ; accepter RELATED,ESTABLISHED ; jeter INVALID ; accepter quatre types ICMP (destination-unreachable, time-exceeded, parameter-problem, echo-request) et les réponses DHCP ; puis mDNS et UPnP en multidiffusion, hérités d'un usage sur poste de travail. before6.rules fait de même pour IPv6, avec la liste de messages ICMPv6 de la RFC 4890. C'est là que l'on ajoute ce qu'ufw ne sait pas exprimer en ligne de commande, puis sudo ufw reload. Pour voir vos règles telles que le noyau les applique : sudo iptables -S ufw-user-input.

4. nftables natif sur sig-outils

Sur Debian 13, le paquet nftables (sudo apt install nftables s'il manque) est construit avec dh_installsystemd --no-enable --no-start : le service n'est ni activé ni démarré, et le fichier livré ne contient que trois chaînes vides en politique accept. Un Debian fraîchement installé ne filtre rien.

Besoins de sig-outils : SSH depuis le réseau privé, et le port 6514 (syslog sur TLS) seulement depuis sig-app-1 et sig-app-2, pour la réception des journaux de la leçon 12. Le fichier complet, à écrire avec sudoedit /etc/nftables.conf :

#!/usr/sbin/nft -f
# Pare-feu de sig-outils (Debian 13). Source de vérité : dépôt de configuration.

flush ruleset

define RESEAU_PRIVE = 172.16.8.0/22
define APPLIS = { 172.16.8.11, 172.16.8.12 }

table inet filtre {

    set ssh_temporaire {
        type ipv4_addr
        flags timeout
    }

    set ssh_debit {
        type ipv4_addr
        flags dynamic
        timeout 60s
    }

    chain entree {
        type filter hook input priority filter; policy drop;

        iif "lo" accept
        ct state vmap { established : accept, related : accept, invalid : drop }

        icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept
        icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request } accept
        icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } ip6 hoplimit 255 accept

        udp sport 67 udp dport 68 accept

        tcp dport 22 ct state new ip saddr $RESEAU_PRIVE update @ssh_debit { ip saddr limit rate 6/minute } accept
        tcp dport 22 ip saddr @ssh_temporaire accept
        tcp dport 6514 ip saddr $APPLIS accept comment "rsyslog TLS, leçon 12"
        udp dport 123 ip saddr $APPLIS accept comment "NTP servi au réseau privé, leçon 9"

        limit rate 10/minute burst 20 packets log prefix "nft-entree-refus: " level info
        counter drop
    }

    chain transit {
        type filter hook forward priority filter; policy drop;
    }

    chain sortie {
        type filter hook output priority filter; policy accept;
    }
}

De haut en bas :

  • nft -f lit le fichier d'un seul bloc, comme une transaction : une ligne fausse, et rien n'est appliqué ; l'ancien jeu de règles reste en place.
  • flush ruleset vide tout le moteur avant de charger le reste, tables des autres programmes comprises. Le fichier devient idempotent, mais c'est un piège si Docker tourne (voir Pièges courants).
  • define : des variables ; $APPLIS est un ensemble anonyme.
  • table inet filtre : famille inet, donc IPv4 et IPv6. Le nom est libre.
  • set ssh_temporaire : un ensemble nommé, modifiable à chaud ; flags timeout autorise des éléments qui expirent seuls.
  • set ssh_debit : un ensemble dynamique, rempli par les règles elles-mêmes, avec un compteur de débit par adresse (méthode de la page Meters du wiki nftables).
  • type filter hook input priority filter; policy drop; fait de la chaîne une chaîne de base, rattachée au crochet input. priority filter vaut 0 : plusieurs chaînes peuvent partager un crochet, la priorité la plus basse passe d'abord. policy drop jette ce qui atteint la fin.
  • iif "lo" accept : sans elle, les programmes qui se parlent par 127.0.0.1 cessent de fonctionner.
  • ct state vmap : une table de verdicts consultée en une fois, équivalente à ct state established,related accept suivi de ct state invalid drop.
  • ICMP et ICMPv6 : voir Sous le capot. ip6 hoplimit 255 vérifie, comme le demande la RFC 4890, que les messages de découverte des voisins n'ont traversé aucun routeur.
  • SSH : un paquet new vers le port 22 depuis le réseau privé met à jour l'entrée de sa source dans @ssh_debit ; sous 6 par minute, la règle accepte, au-delà le paquet descend jusqu'à la règle finale.
  • comment "..." est conservé dans le noyau et affiché par nft list ruleset, contrairement aux #.
  • La règle finale journalise (10 lignes par minute au plus) puis jette. La politique suffirait à refuser ; la règle ajoute la trace et un compteur, comme le demande la recommandation R6 de la note de l'ANSSI sur les politiques de filtrage : une « règle explicite d'interdiction finale journalisée ».
  • transit : sig-outils ne route rien. sortie : libre pour l'instant.

Vérifier, sécuriser, appliquer :

$ { echo 'flush ruleset'; sudo nft list ruleset; } | sudo tee /root/pare-feu-avant.nft > /dev/null
$ sudo nft -c -f /etc/nftables.conf
$ sudo systemd-run --on-active=5min --unit=retour-pare-feu /usr/sbin/nft -f /root/pare-feu-avant.nft
$ sudo nft -f /etc/nftables.conf
$ sudo systemctl enable nftables
  1. Sauvegarde de l'état courant dans un fichier rechargeable (nft list ruleset produit une syntaxe que nft -f relit).
  2. nft -c (check) valide le fichier sans rien appliquer ; en cas d'erreur, il indique la ligne et les colonnes fautives.
  3. Le minuteur de retour arrière.
  4. L'application, atomique.
  5. enable, sans quoi le pare-feu disparaît au redémarrage. L'unité Debian, rattachée à sysinit.target, s'exécute avant network-pre.target : les règles sont en place avant la première interface configurée, ce qui ferme la fenêtre d'exposition au démarrage dont parle la recommandation R12 de l'ANSSI.

Testez par une nouvelle session SSH et un message de test depuis sig-app-1, puis sudo systemctl stop retour-pare-feu.timer.

Modifier à chaud. nft -a affiche l'identifiant de chaque règle (# handle 12) :

$ sudo nft -a list chain inet filtre entree
$ sudo nft insert rule inet filtre entree position 12 ip saddr 172.16.8.40 drop
$ sudo nft delete rule inet filtre entree handle 27
$ sudo nft add element inet filtre ssh_temporaire { 172.16.8.50 timeout 2h }

insert ... position 12 place la règle avant celle d'identifiant 12 ; add rule l'ajouterait après la dernière, derrière la règle finale, où elle ne servirait à rien. add element ... timeout 2h ouvre SSH à un prestataire pour deux heures, sans risque d'oubli. Toute modification à chaud est perdue au redémarrage ou au prochain systemctl reload nftables : ce qui doit durer va dans le fichier, et dans le dépôt.

5. Journaliser et diagnostiquer

L'instruction log écrit dans le tampon du noyau, que journald recueille. Son niveau par défaut est warn ; on l'a abaissé à info pour ne pas déclencher les alertes sur les avertissements.

$ journalctl -k --grep 'nft-entree-refus' --since '-1h'

Sortie typique (interface et valeurs à titre d'exemple) :

oct. 07 10:42:13 sig-outils kernel: nft-entree-refus: IN=ens5 OUT= MAC=02:00:00:aa:bb:cc:02:00:00:dd:ee:ff:08:00 SRC=172.16.8.40 DST=172.16.8.20 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=4242 DF PROTO=TCP SPT=51544 DPT=6514 WINDOW=64240 RES=0x00 SYN URGP=0

IN= l'interface d'arrivée, SRC=/DST= les adresses, SPT=/DPT= les ports, SYN un début de connexion. Une machine absente de $APPLIS tente de joindre le port des journaux : configuration oubliée ou exploration du réseau.

Les compteurs disent si une règle est seulement atteinte : nft list chain inet filtre entree affiche counter packets 1312 bytes 78720 sur les règles qui en ont un. Pour les cas tordus, la trace montre le parcours d'un paquet, règle par règle, toutes tables confondues (ufw et Docker compris) :

$ sudo nft add table inet trace
$ sudo nft add chain inet trace marquage '{ type filter hook prerouting priority -350; }'
$ sudo nft add rule inet trace marquage ip saddr 172.16.8.11 tcp dport 6514 meta nftrace set 1
$ sudo nft monitor trace
$ sudo nft delete table inet trace

Avec ufw, sudo ufw logging low|medium|high|full règle le volume. D'après ufw(8), low (le défaut) journalise les paquets bloqués par la politique, avec limitation de débit ; medium y ajoute les paquets autorisés hors politique par défaut, les paquets invalides et toutes les nouvelles connexions ; high et full retirent progressivement la limitation. Les lignes portent les préfixes [UFW BLOCK], [UFW ALLOW] ou [UFW LIMIT BLOCK] ; sur Ubuntu, rsyslog les copie dans /var/log/ufw.log. Restez en low en production.

6. Filtrer en sortie

Une politique d'entrée stricte n'empêche pas un programme compromis d'envoyer des données ni de télécharger la suite de son attaque. Les besoins sortants de sig-outils sont connus : DNS (53), temps (123/udp, leçon 9), sig-db (5432, pour l'export), Object Storage et dépôts APT (443), service de métadonnées Scaleway (169.254.42.42, port 80). La chaîne sortie devient :

    chain sortie {
        type filter hook output priority filter; policy drop;

        oif "lo" accept
        ct state vmap { established : accept, related : accept, invalid : drop }
        icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept
        icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert, mld-listener-report, mld2-listener-report } accept
        udp sport 68 udp dport 67 accept

        meta l4proto { udp, tcp } th dport 53 accept
        udp dport 123 accept
        tcp dport 4460 accept comment "NTS-KE, leçon 9"
        ip daddr $RESEAU_PRIVE tcp dport 5432 accept comment "sig-db"
        tcp dport 443 accept comment "Object Storage, APT"
        ip daddr 169.254.42.42 tcp dport 80 accept comment "métadonnées Scaleway"

        limit rate 10/minute log prefix "nft-sortie-refus: " level info
        counter drop
    }

th dport désigne le port de destination quel que soit le transport. Le 443 ne peut pas être restreint par adresse : celles de s3.fr-par.scw.cloud changent sans préavis, et un filtrage par nom demande un mandataire sortant. Même imparfait, ce filtrage ferme les canaux les plus simples (SSH ou SMTP sortants, ports non standard, rebond vers d'autres machines du réseau privé). Avant de passer en drop, laissez tourner une semaine une version en accept qui ne fait que journaliser : c'est là que l'on découvre le besoin oublié (un dépôt tiers en HTTP, un webhook de notification).

Sous le capot

Accepter n'est pas définitif, jeter l'est. Plusieurs chaînes de base peuvent partager un crochet : votre table inet filtre, la table ip filter d'iptables-nft (ufw, Docker), une table de trace. Le noyau les parcourt par priorité croissante, et la page Configuring chains du wiki nftables pose la règle : un accept « isn't necessarily final », le paquet continue vers la chaîne suivante ; un drop prend effet immédiatement. Avec ufw et une table nftables écrite à la main, un paquet doit donc être accepté par les deux. Ajouter une autorisation dans l'un ne sert à rien si l'autre refuse.

Les priorités. filter vaut 0 ; les autres alias documentés sont raw (-300), mangle (-150), dstnat (-100), security (50) et srcnat (100). Le suivi de connexion intervient à -200 : une chaîne raw voit les paquets avant qu'ils aient un état.

Pourquoi les ensembles passent à l'échelle. Une chaîne s'évalue règle après règle : 500 règles ip saddr X drop, c'est jusqu'à 500 comparaisons par paquet. Un ensemble est une structure de données du noyau (table de hachage, ou arbre pour les intervalles) : ip saddr @bloques drop reste une seule recherche, et l'ensemble se modifie sans recharger les règles.

L'atomicité. nft -f applique un lot de modifications en une transaction. Une suite de commandes iptables -A, au contraire, traverse des états intermédiaires, parfois fermés, parfois ouverts. Raison de plus pour décrire un pare-feu dans un fichier complet.

La table de suivi. Le nombre d'entrées et le maximum se lisent dans /proc/sys/net/netfilter/nf_conntrack_count et nf_conntrack_max ; sudo conntrack -L -p tcp --dport 8000 (paquet conntrack) liste les connexions du répartiteur vers Gunicorn. Table pleine : le noyau jette les nouveaux flux et écrit nf_conntrack: table full, dropping packet.

Pourquoi ICMP et ICMPv6 ne se bloquent pas. La découverte du MTU du chemin repose sur les messages « paquet trop gros » ; les jeter crée un trou noir où les petites requêtes passent et les grosses réponses se figent (leçon ICMP, ping, traceroute et la MTU). En IPv6, il n'y a pas d'ARP : la résolution des adresses de liaison passe par la découverte des voisins (NDP), en ICMPv6. Le suivi de connexion ne rattache pas ces messages à un flux, il les marque untracked : ils traversent la règle ct state vmap sans verdict et doivent être acceptés explicitement. C'est la liste de la RFC 4890, section 4.4, que reprend before6.rules d'ufw.

Docker et la table nat. Publier un port (docker run -p 8080:8080) ajoute une règle DNAT au crochet prerouting : un paquet vers 172.16.8.11:8080 voit sa destination réécrite vers le conteneur (172.17.0.2:8080) avant la décision de routage. Il n'est donc plus destiné à la machine et prend forward, pas input. La documentation Docker le dit : le trafic est détourné « before it reaches the INPUT and OUTPUT chains that ufw uses ». Docker place en outre ses sauts (DOCKER-USER, puis DOCKER-FORWARD) en tête de la chaîne FORWARD, avant ceux d'ufw. ufw status affiche deny, et le port 8080 est ouvert à tout le réseau privé.

Pièges courants

Se couper l'accès SSH. ufw default deny incoming && ufw enable sans règle SSH, ou avec la mauvaise plage. La session en cours survit (elle est established), on se déconnecte satisfait, et l'on ne revient plus. Remèdes : le minuteur systemd-run --on-active= posé avant (ou at now + 5 minutes si le paquet at est installé), le test par une nouvelle session, et en dernier recours la console de l'instance dans l'interface Scaleway, qui ne dépend pas du réseau (leçon 5).

Docker qui contourne ufw. ufw status n'autorise rien sur 8080, et curl http://172.16.8.11:8080 répond depuis sig-outils. Remèdes : publier sur la boucle locale (-p 127.0.0.1:8080:8080, puis un tunnel SSH) ; ou filtrer dans DOCKER-USER, que Docker décrit comme « a placeholder for user-defined rules that will be processed before rules in the DOCKER-FORWARD and DOCKER chains ». Désactiver la gestion du pare-feu par Docker ("iptables": false dans /etc/docker/daemon.json) casse le réseau des conteneurs. Le moteur nftables de Docker, apparu avec Docker 29.0, est expérimental d'après sa documentation et n'a pas de chaîne DOCKER-USER.

flush ruleset sur un hôte Docker. Recharger /etc/nftables.conf vide aussi les tables de Docker : les conteneurs perdent leurs ports et leur accès sortant jusqu'au prochain systemctl restart docker. Même effet avec systemctl stop nftables, dont l'unité Debian exécute nft flush ruleset. Sur un tel hôte, remplacez flush ruleset par destroy table inet filtre (nftables 1.0.8 et noyau 6.3 ou plus récents), qui ne supprime que votre table. Et ne déclarez pas de chaîne forward en drop : le accept de Docker n'est pas définitif, votre drop l'est.

IPv6 oublié. Avec iptables seul, la politique IPv4 est soignée et ip6tables vide : tout passe en IPv6. Or les réseaux privés Scaleway reçoivent un préfixe IPv6 par défaut. Vérifiez ip -6 addr, et couvrez les deux familles (inet en nftables, IPV6=yes pour ufw).

L'ordre des règles. Première correspondance gagnante chez ufw ; add rule derrière la règle finale en nftables. Utilisez ufw insert et nft insert rule ... position, relisez avec ufw status numbered ou nft -a list ruleset.

Une règle à chaud qui disparaît. Elle n'existait que dans le noyau. Le fichier fait foi.

Les erreurs de nft. Error: syntax error, unexpected ..., suivi de la ligne et d'un soulignement ^^^^ : souvent un point-virgule manquant après priority filter ou policy drop. Error: Could not process rule: No such file or directory : table, chaîne ou ensemble inexistant (filter au lieu de filtre, table ip interrogée en inet). Error: Could not process rule: Device or resource busy : suppression d'une chaîne ou d'un ensemble encore référencé.

Deux moteurs en même temps. Un vieux script appelle iptables-legacy, ufw utilise iptables-nft : nft list ruleset ne montre qu'une moitié de la politique. Choisissez un moteur, convertissez (iptables-legacy-save puis iptables-restore-translate) et videz l'autre.

Sécurité

  • Le refus par défaut. La note de l'ANSSI parle de « modèle de sécurité positif » : tout ce qui n'a pas été autorisé est interdit, et la dernière règle le rend explicite et journalisé.
  • Des sources précises. ufw allow 22 ouvre SSH au monde. L'ANSSI demande des règles « définies précisément, en particulier au niveau de leurs adresses sources et de leurs services ».
  • limit n'authentifie rien. Il gêne la force brute, il ne remplace ni les clés ni PasswordAuthentication no (leçon 14, et le cours SSH).
  • root peut tout défaire. Un attaquant administrateur vide le pare-feu en une commande ; d'où l'intérêt des filtres extérieurs, et de journaux de refus qui quittent la machine en temps réel (leçon 12).
  • Le filtrage en sortie ralentit l'exfiltration. Pour une machine qui manipule les signalements des citoyens (photos, adresses), c'est une mesure à faire valoir au titre de la sécurité du traitement (article 32 du RGPD). Les journaux de refus, qui contiennent des adresses IP, suivent la politique de conservation de la leçon 12.
  • Revoir la politique. Les règles temporaires deviennent permanentes par oubli ; l'ANSSI recommande une revue au moins annuelle (R15). Les ensembles avec timeout et les commentaires sur chaque règle rendent la revue possible.

En production

  • La politique est du code, versionnée, relue et déployée par l'outil d'automatisation, minuteur de retour arrière compris. Une modification manuelle sur une seule instance crée une dérive de configuration qui se révélera le jour où le répartiteur basculera sur l'autre.
  • Le déploiement est progressif : sig-app-2 d'abord, retirée du répartiteur le temps de la vérification, puis sig-app-1.
  • La précision est un arbitrage. Restreindre le port 8000 à l'adresse du répartiteur est plus strict que le /22, mais cette adresse, attribuée par l'IPAM, peut changer si le répartiteur est recréé. Réservez-la dans l'IPAM, ou gardez le réseau entier et documentez l'écart.
  • On surveille l'état du service, le nombre de règles chargées (un pare-feu vidé par un flush raté passe inaperçu sans cette mesure), le volume des refus et, sur les machines chargées, le remplissage de la table de suivi.
  • Sur Kapsule, kube-proxy ou Cilium écrivent des milliers de règles Netfilter ou eBPF, et le filtrage entre applications se déclare par des NetworkPolicy (cours Kubernetes : le réseau). On ne gère pas le pare-feu d'un nœud à la main, mais les mécanismes sont ceux de cette leçon.

Exercices

1. Lire une politique (niveau 100). Sur sig-app-2, sudo ufw status verbose affiche :

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
8000/tcp                   ALLOW IN    172.16.8.0/22
22/tcp (v6)                ALLOW IN    Anywhere (v6)

Qui peut se connecter en SSH ? Qu'est-ce qui diffère de sig-app-1, et comment le corrigez-vous ?

Solution

SSH est ouvert à toute adresse, IPv4 et IPv6, sans limitation de débit : la règle vient de ufw allow 22/tcp, qui crée une règle par famille. Gunicorn n'est joignable que depuis le réseau privé. Correction, sous la protection d'un minuteur de retour arrière :

$ sudo ufw limit from 172.16.8.0/22 to any port 22 proto tcp comment 'SSH depuis pn-signalements'
$ sudo ufw delete allow 22/tcp

La nouvelle règle avant la suppression de l'ancienne, pour ne jamais passer par un état sans SSH. delete allow 22/tcp supprime les deux variantes. Testez par une nouvelle session.

2. Un accès temporaire (niveau 200). Un auditeur doit se connecter en SSH à sig-outils depuis 172.16.8.50 pendant trois heures. Écrivez la commande. Faut-il nettoyer ensuite ? Que devient l'accès si la machine redémarre entre-temps, et une session ouverte est-elle coupée à l'expiration ?

Solution

sudo nft add element inet filtre ssh_temporaire { 172.16.8.50 timeout 3h }. L'ensemble a flags timeout : l'élément expire seul, aucun nettoyage. Après un redémarrage, l'ensemble est recréé vide depuis le fichier : l'accès disparaît, ce qui est souhaitable pour un accès temporaire. Une session ouverte n'est pas coupée à l'expiration, puisqu'elle est established ; pour l'interrompre, il faut supprimer son entrée de suivi (sudo conntrack -D -s 172.16.8.50).

3. Le conteneur oublié (niveau 200). ufw sur sig-app-1 n'autorise que 22 et 8000 depuis le réseau privé, pourtant curl -s http://172.16.8.11:8080 depuis sig-outils affiche la page de connexion d'Adminer. Décrivez le chemin du paquet et proposez deux corrections.

Solution

Le conteneur a été lancé avec -p 8080:8080, sur toutes les adresses. La règle DNAT de Docker, au crochet prerouting, réécrit la destination vers l'adresse du conteneur avant la décision de routage : le paquet prend forward, où les sauts de Docker précèdent ceux d'ufw et acceptent le trafic vers les ports publiés. Les règles d'ufw, sur input, ne le voient jamais.

  1. Arrêter le conteneur s'il ne sert plus, ou le republier sur la boucle locale (-p 127.0.0.1:8080:8080) et y accéder par ssh -L 8080:127.0.0.1:8080 sig-app-1.
  2. Filtrer dans DOCKER-USER : sudo iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport 8080 ! -s 172.16.8.20 -j DROP. Le module conntrack teste le port d'origine, puisque dans forward le port vu est déjà celui du conteneur. La règle doit être rechargée après chaque redémarrage de Docker (unité systemd ordonnée après docker.service).

La première est la meilleure : un outil d'administration de base n'a pas sa place en permanence sur un serveur de production.

4. IPv6 qui meurt au bout d'une minute (niveau 300). Un collègue ajoute, juste après la ligne ct state vmap de sig-outils, la règle meta l4proto ipv6-icmp drop, « pour réduire la surface d'attaque ». Tout fonctionne, puis une à deux minutes plus tard plus aucune destination IPv6 hors du lien n'est joignable, alors que l'IPv4 va bien. Expliquez le délai, pourquoi les règles ICMPv6 placées plus bas ne sauvent rien, et proposez une politique qui réponde à l'inquiétude du collègue.

Solution

L'adresse de liaison de la passerelle IPv6 s'obtient par la découverte des voisins (nd-neighbor-solicit et nd-neighbor-advert), dont le résultat est mis en cache. Tant que l'entrée est valide, le trafic passe. Quand elle doit être revérifiée, les annonces de réponse sont jetées en entrée : l'entrée passe de REACHABLE à STALE, PROBE puis FAILED (ip -6 neigh), et tout le trafic routé s'arrête. Les annonces de routeur sont jetées aussi ; la route par défaut finira par expirer.

Les acceptations placées plus bas sont inutiles : drop est définitif. Les erreurs liées à une connexion établie passent encore (elles sont related), mais pas NDP, que le suivi marque untracked.

Politique conforme à la RFC 4890 : retirer le drop, accepter les erreurs (destination-unreachable, packet-too-big, time-exceeded, parameter-problem), echo-request avec une limite de débit si l'on craint le ping en masse (icmpv6 type echo-request limit rate 10/second accept), et les messages NDP avec ip6 hoplimit 255 : un attaquant distant ne peut pas les injecter à travers un routeur, ce qui répond à l'inquiétude. Tout autre type tombe dans la politique drop.

Récapitulatif

  • Le filtrage est fait par Netfilter, dans le noyau ; ufw, nft, iptables et firewalld y écrivent des règles. Crochets : prerouting, input, forward, output, postrouting.
  • Le suivi de connexion permet le filtrage à état : accepter established,related, jeter invalid, ne décider que des flux new.
  • La commande iptables actuelle est iptables-nft : ses règles apparaissent dans nft list ruleset. Un seul moteur, un seul outil par machine.
  • ufw sur Ubuntu : default deny incoming, sources précises et commentaires, limit pour SSH ; la moitié de la politique est dans before.rules.
  • nftables sur Debian : table inet, politique drop, nft -c -f avant d'appliquer, systemctl enable nftables (désactivé par défaut), ensembles pour les listes et les accès temporaires.
  • ICMP et ICMPv6 ne se bloquent pas en bloc : découverte du MTU du chemin et découverte des voisins en dépendent.
  • Chez Scaleway, le groupe de sécurité ne voit que le trafic public ; le trafic privé ne rencontre que le pare-feu de l'hôte.
  • Docker publie ses ports par DNAT et passe par forward, hors de portée d'ufw.
  • Toute modification à distance : retour arrière automatique (systemd-run --on-active=) et test par une nouvelle connexion.
  • Une règle finale journalisée, des compteurs et nft monitor trace expliquent un refus ; le filtrage en sortie borne ce que peut faire une machine compromise.

Pour aller plus loin

  • La page nft(8) et le wiki nftables, en particulier Netfilter hooks, Configuring chains et Sets.
  • La note technique de l'ANSSI Définition d'une politique de filtrage réseau d'un pare-feu : quinze recommandations courtes, presque toutes applicables à un pare-feu d'hôte.
  • La RFC 4890, pour la liste raisonnée des messages ICMPv6 à laisser passer.
  • La documentation Docker sur le filtrage des paquets, et la leçon Relier et exposer : les réseaux Docker.
  • La leçon Le réseau : réseaux privés, groupes de sécurité, répartiteurs de charge, pour les filtres côté Scaleway.
  • Le cours Le réseau sous Linux, pour la traduction d'adresses, le routage et les espaces de noms réseau.
  • La leçon suivante : L'heure et sa synchronisation.
+20 XP Carte du ciel →Mon cosmonaute →

Sources