Vous avez mis en place un pare-feu nftables propre sur Debian 13. Vous le testez. Vous êtes rassuré. Puis vous installez Docker et — mystérieusement — des ports s’ouvrent, le transfert se comporte différemment et des paquets prennent des routes que vous n’avez pas autorisées.
C’est l’une de ces contrariétés opérationnelles qui peuvent devenir un incident de sécurité si vous continuez à les traiter comme « juste du réseau ». Docker a ses préférences. nftables a ses préférences. Votre équipe conformité a ses préférences. Seule l’une d’elles vous paie le salaire.
Le modèle mental : qui possède le pare-feu ?
Sur Debian 13, nftables est l’interface pare-feu de première classe, mais le noyau expose toujours des hooks netfilter que plusieurs outils peuvent programmer. Docker, par défaut, programme aussi ces hooks. Selon le packaging et la configuration, il peut le faire via iptables-nft (syntaxe iptables, backend nft) pendant que vous écrivez des règles nft natives. Mêmes hooks, outils différents, état partagé. C’est ainsi que vous vous retrouvez avec un pare-feu « qui fonctionne » et qui se comporte comme un spectacle d’improvisation.
La plus grande erreur est de penser en termes de « mon fichier de règles du pare-feu ». Le système ne se soucie pas de votre fichier. Le système se soucie de l’ensemble de règles actif dans le noyau. Docker modifie dynamiquement cet ensemble actif. Si vous voulez de la prévisibilité, vous devez décider :
- Soit laisser Docker gérer la NAT et le forwarding de base, et vous le contrôlez via les points d’étranglement appropriés (en particulier le hook
DOCKER-USER), - Soit désactiver l’ingérence de Docker dans le pare-feu et prendre l’entière responsabilité de la NAT/du forwarding/des ports publiés vous-même.
Le mode moitié-moitié est là où les équipes de sécurité vont pleurer.
Une citation collée sur plus d’un laptop d’astreinte :
Espérer n’est pas une stratégie.
— Général Gordon R. Sullivan
Neuf faits qui vous empêcheront de deviner
- nftables a remplacé iptables en tant que « successeur » il y a des années, mais iptables reste largement utilisé comme interface de compatibilité, souvent mappé en nft en coulisses.
- Docker utilisait historiquement iptables directement pour configurer la NAT et la publication de ports. Cet héritage reste visible aujourd’hui, même lorsque nftables est votre interface choisie.
- Le backend
iptables-nftsignifie que des commandes iptables peuvent créer des règles nftables. C’est pratique, mais cela signifie aussi que deux syntaxes différentes écrivent dans un même ensemble de règles. - Les « ports publiés » de Docker ne sont pas que des sockets d’écoute ; ce sont typiquement des règles DNAT plus des modifications de filtrage. Un conteneur peut être atteignable même si rien n’est lié sur l’hôte comme vous l’attendez.
- La chaîne
DOCKER-USERexiste spécifiquement pour permettre aux opérateurs d’appliquer une politique avant les propres règles d’acceptation de Docker. Si vous ne l’utilisez pas, vous laissez du contrôle (et de l’argent) sur la table. - NAT et filtrage sont des tables différentes. Bloquer dans
filtersans comprendrenatpeut conduire à « c’est bloqué mais ça fonctionne quand même » ou « c’est ouvert mais non joignable ». - Le forwarding dépend de
net.ipv4.ip_forwardet des sysctls associés ; Docker peut activer des comportements que votre durcissement de base avait désactivés, et inversement. - Les pare-feux sont évalués dans l’ordre des hooks. Si Docker insère des règles avant les vôtres (ou à une priorité plus haute), vos drops soigneusement conçus peuvent ne jamais s’exécuter.
- Les valeurs par défaut de Debian peuvent être trompeusement silencieuses : vous pouvez avoir « nftables installé » mais « service nftables inactif », et pourtant avoir des règles actives insérées par d’autres composants.
Blague n°1 : Les pare-feux sont comme la politique de bureau — tout va bien jusqu’à ce que quelqu’un change silencieusement la chaîne de commandement.
Ce qui se passe réellement sur Debian 13 quand Docker démarre
1) Docker crée des bridges et des namespaces
Docker crée typiquement un bridge Linux (souvent docker0) et un ou plusieurs bridges définis par l’utilisateur. Les conteneurs résident dans des network namespaces avec des paires veth connectées à ces bridges. Cette partie est simple.
2) Docker programme netfilter pour que ça « marche simplement »
« Marche simplement » signifie :
- Les conteneurs peuvent atteindre Internet via du masquerading (SNAT) en sortie.
- Les ports publiés sur l’hôte sont redirigés (DNAT) vers les IP des conteneurs.
- Le forwarding entre les interfaces de l’hôte et le bridge des conteneurs est accepté.
L’implémentation exacte dépend de si Docker utilise la compatibilité iptables et si le système exécute iptables legacy ou iptables avec backend nft. Sur Debian 13, vous devez supposer le backend nft sauf si vous avez explicitement forcé le legacy.
3) Vous voyez des chaînes nftables que vous n’avez pas écrites
Les artefacts typiques incluent des chaînes comme DOCKER, DOCKER-USER, et parfois des règles dans nat pour DNAT/MASQUERADE. Les noms exacts peuvent varier ; le schéma reste le même. Si cela vous surprend, vous n’êtes pas seul. Mais « surpris » n’est pas un état acceptable en production.
4) Le « surprenant » n’est généralement pas que Docker soit malveillant
Docker optimise l’expérience développeur, tandis que vous optimisez des limites de sécurité prévisibles. Ces objectifs ne sont pas ennemis, mais exigent une conception explicite. Vous ne gagnez pas en espérant que Docker s’arrête.
Mode opératoire de diagnostic rapide (premier / second / troisième)
Lorsqu’un port semble ouvert de façon inattendue, ou que le trafic des conteneurs contourne votre politique prévue, ne commencez pas par réécrire les règles. Commencez par la visibilité. Puis décidez qui doit posséder quoi.
Premier : confirmer ce qui est actif (nft ruleset + vue iptables)
- Dump l’ensemble de règles nft actif et recherchez les chaînes Docker et les points de saut.
- Vérifiez les règles iptables telles que vues par la couche de compatibilité (cela peut révéler comment Docker a inséré des règles).
Second : tracer le chemin du paquet pour le symptôme spécifique
- Est-ce entrant vers un port publié ? C’est DNAT + filter/forward.
- Est-ce de l’egress de conteneur ? C’est masquerade + forward + sysctls.
- Est-ce conteneur-à-conteneur ? C’est le filtrage du bridge et les équivalents de la chaîne
FORWARD.
Troisième : décider du modèle et l’appliquer
- Si Docker gère iptables/nft : verrouillez la politique dans
DOCKER-USER, gardez vos règles nft compatibles, évitez les hooks conflictuels. - Si vous gérez tout : désactivez iptables de Docker, implémentez vos propres règles nat/filter pour les bridges et les ports publiés, et assumez la charge opérationnelle.
La plupart des équipes de production devraient commencer par « Docker gère la NAT mais nous possédons la politique via DOCKER-USER » sauf si elles ont des exigences strictes de segmentation réseau et le personnel pour la maintenir.
Tâches pratiques (commandes, sorties, décisions)
Ce sont des tâches de terrain. Exécutez-les sur l’hôte. Lisez la sortie. Prenez une décision. Répétez jusqu’à ce que le système devienne ennuyeux.
Task 1 — Confirmer l’état de Docker et nftables
cr0x@server:~$ systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-30 09:14:22 UTC; 2h 1min ago
Docs: https://docs.docker.com
Main PID: 1423 (dockerd)
Tasks: 22
Memory: 154.2M
CPU: 1min 12.553s
CGroup: /system.slice/docker.service
└─1423 /usr/bin/dockerd -H fd://
Ce que cela signifie : Docker est actif et peut injecter des règles en ce moment.
Décision : Supposez que le pare-feu n’est pas « statique ». Inspectez les règles actives, pas seulement les fichiers de configuration.
cr0x@server:~$ systemctl status nftables --no-pager
○ nftables.service - nftables
Loaded: loaded (/lib/systemd/system/nftables.service; enabled; preset: enabled)
Active: inactive (dead)
Ce que cela signifie : nftables peut ne pas charger votre baseline au démarrage, mais le noyau peut toujours avoir des règles insérées par d’autres composants.
Décision : Si vous attendez une baseline au démarrage, corrigez l’état du service plus tard. D’abord, inspectez l’ensemble de règles en direct.
Task 2 — Dumper l’ensemble de règles nft actif (la vérité)
cr0x@server:~$ sudo nft list ruleset | sed -n '1,120p'
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept
tcp dport 22 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
jump DOCKER-USER
jump DOCKER-FORWARD
}
chain output {
type filter hook output priority filter; policy accept;
}
chain DOCKER-USER {
return
}
chain DOCKER-FORWARD {
iif "docker0" accept
oif "docker0" accept
}
}
Ce que cela signifie : Des chaînes liées à Docker existent dans votre table inet filter, et forward saute vers elles. Même si vous ne les avez pas créées.
Décision : Si vous voulez le contrôle politique tout en gardant Docker fonctionnel, DOCKER-USER est votre point d’application.
Task 3 — Vérifier la table nat nft pour DNAT/MASQUERADE
cr0x@server:~$ sudo nft list table ip nat
table ip nat {
chain PREROUTING {
type nat hook prerouting priority dstnat; policy accept;
iifname != "docker0" tcp dport 8080 dnat to 172.18.0.5:80
}
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
oifname != "docker0" ip saddr 172.18.0.0/16 masquerade
}
}
Ce que cela signifie : Le port 8080 sur l’hôte est DNATé vers un conteneur, et l’egress des conteneurs est masqué.
Décision : Si vous voyez des règles DNAT non autorisées, trouvez quels conteneurs ont publié des ports et décidez si la publication doit être autorisée du tout.
Task 4 — Identifier les ports publiés du point de vue de Docker
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
web-01 nginx:alpine 0.0.0.0:8080->80/tcp
redis-01 redis:7 6379/tcp
Ce que cela signifie : web-01 est publié sur toutes les interfaces hôte sur le port 8080. Redis n’est pas publié (accessible uniquement depuis le conteneur).
Décision : Si « 0.0.0.0 » n’est pas acceptable, contraignez à 127.0.0.1:8080:80 ou à une IP d’interface spécifique, ou bloquez dans DOCKER-USER.
Task 5 — Confirmer quel backend iptables est actif
cr0x@server:~$ sudo update-alternatives --display iptables | sed -n '1,40p'
iptables - auto mode
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
link iptables is /usr/sbin/iptables
/usr/sbin/iptables-nft - priority 20
/usr/sbin/iptables-legacy - priority 10
Ce que cela signifie : Les commandes iptables se mappent en règles nft (backend nft). La programmation iptables de Docker aboutira dans nftables.
Décision : Ne mélangez pas « iptables-legacy » avec des règles nft natives à moins d’aimer déboguer des univers parallèles.
Task 6 — Voir les règles iptables comme Docker les voit (backend nft)
cr0x@server:~$ sudo iptables -S | sed -n '1,80p'
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-USER
-N DOCKER-ISOLATION-STAGE-1
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A DOCKER-USER -j RETURN
Ce que cela signifie : Docker a inséré sa plomberie standard. La présence de DOCKER-USER est votre chance d’appliquer la politique.
Décision : Si vous laissez Docker gérer les règles, mettez vos drops/accepts dans DOCKER-USER (ou l’équivalent nft si vous êtes entièrement natif).
Task 7 — Prouver que l’hôte écoute réellement vs être DNATé
cr0x@server:~$ sudo ss -lntp | awk 'NR==1 || $4 ~ /:8080$/'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=2331,fd=4))
Ce que cela signifie : Un processus (souvent docker-proxy, selon les paramètres) est lié au port 8080 ; dans d’autres configurations, vous pourriez ne voir aucun écouteur parce que seul le DNAT pur est utilisé.
Décision : Si vous pensiez « pas d’écouteur hôte signifie fermé », corrigez cette hypothèse. Il faut inspecter aussi nat/filter.
Task 8 — Inspecter les sysctls qui contrôlent le forwarding et le filtrage bridge
cr0x@server:~$ sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
Ce que cela signifie : Le forwarding est activé et le trafic bridge passe par les hooks netfilter. C’est typique pour Docker, mais pas toujours souhaitable dans des environnements stricts.
Décision : Si le trafic des conteneurs ne doit jamais transiter entre réseaux, envisagez de désactiver le forwarding globalement (mais comprenez que le réseau Docker changera) ou isolez via des règles.
Task 9 — Trouver quelle interface reçoit réellement le trafic « surprise »
cr0x@server:~$ ip -brief addr
lo UNKNOWN 127.0.0.1/8 ::1/128
ens3 UP 203.0.113.10/24 2001:db8:10::10/64
docker0 DOWN 172.17.0.1/16
br-2a1d3c4e5f6a UP 172.18.0.1/16
Ce que cela signifie : Votre interface publique est ens3 ; des réseaux Docker existent sur br-....
Décision : Pour la politique entrante, raisonnez toujours depuis l’interface d’ingress (souvent ens3) à travers nat PREROUTING puis forward/filter.
Task 10 — Confirmer quel conteneur possède la cible DNAT
cr0x@server:~$ docker inspect -f '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' web-01
/web-01 172.18.0.5
Ce que cela signifie : L’IP cible du DNAT correspond à web-01.
Décision : Si ce conteneur ne doit pas être exposé sur Internet, corrigez la configuration de publication et/ou bloquez dans DOCKER-USER.
Task 11 — Ajouter une porte politique explicite dans DOCKER-USER (bloquer l’entrée sauf approuvé)
cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -p tcp --dport 8080 -s 198.51.100.0/24 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -i ens3 -p tcp --dport 8080 -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i ens3 -p tcp -s 198.51.100.0/24 --dport 8080 -j ACCEPT
-A DOCKER-USER -i ens3 -p tcp --dport 8080 -j DROP
-A DOCKER-USER -j RETURN
Ce que cela signifie : L’entrée vers le port publié 8080 depuis l’interface Internet est maintenant restreinte à un CIDR autorisé, sinon elle est dropée, avant les propres règles d’acceptation de Docker.
Décision : Si cela résout votre désalignement de politique sans casser le réseau des conteneurs, vous avez validé le modèle « Docker gère la plomberie, nous appliquons la politique ».
Task 12 — Rendre les règles persistantes (et cesser de compter sur des modifications manuelles)
cr0x@server:~$ sudo install -d -m 0755 /etc/iptables
cr0x@server:~$ sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
cr0x@server:~$ sudo head -n 20 /etc/iptables/rules.v4
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -i ens3 -p tcp -s 198.51.100.0/24 --dport 8080 -j ACCEPT
-A DOCKER-USER -i ens3 -p tcp --dport 8080 -j DROP
-A DOCKER-USER -j RETURN
COMMIT
Ce que cela signifie : Vous avez maintenant un enregistrement durable des règles de politique appliquées. Que vous restauriez avec une unité systemd ou un autre mécanisme est votre choix ; l’important est que « ça marchait mardi » n’est pas une stratégie de configuration.
Décision : Si la norme de votre organisation est nftables natif, traduisez cela en syntaxe nft et chargez via le service nftables. Si la norme est iptables-nft, gardez la cohérence et automatisez la restauration au démarrage.
Task 13 — Si vous voulez que Docker cesse d’injecter des règles entièrement (mode haute cérémonie)
cr0x@server:~$ sudo install -d -m 0755 /etc/docker
cr0x@server:~$ sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"iptables": false,
"ip-forward": false
}
EOF
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES PORTS
web-01 0.0.0.0:8080->80/tcp
Ce que cela signifie : Docker affiche toujours les ports publiés, mais il n’installera plus les règles netfilter pour les rendre joignables. Les choses vont casser jusqu’à ce que vous fournissiez vos propres règles NAT/forward/filter et les sysctls appropriés.
Décision : Choisissez ce mode uniquement si vous êtes prêt à implémenter et assumer la NAT et le forwarding correctement pour chaque réseau Docker que vous créez.
Task 14 — Vérifier un chemin de paquet spécifique avec des compteurs (visibilité nft native)
cr0x@server:~$ sudo nft -a list chain inet filter forward
table inet filter {
chain forward { # handle 8
type filter hook forward priority filter; policy drop;
jump DOCKER-USER # handle 21
jump DOCKER-FORWARD # handle 22
}
}
Ce que cela signifie : Vous pouvez référencer des handles de règles, ajouter des compteurs et observer les hits. C’est ainsi que vous cessez de débattre du « devrait » et commencez à mesurer le « est ».
Décision : Si vous ne pouvez pas expliquer quelle chaîne prend en charge votre trafic, ne changez pas encore la politique — instrumentez d’abord.
Blague n°2 : Si vous vous sentez inutile, souvenez-vous qu’il existe une demande de changement de pare-feu marquée « urgent » sans IP source et sans port.
Deux conceptions sensées (et une qui en a l’air)
Conception A (recommandée pour la plupart des équipes) : Docker gère la plomberie, vous appliquez la politique dans DOCKER-USER
Vous laissez Docker créer DNAT/MASQUERADE et garder les conteneurs joignables comme prévu. Ensuite vous ajoutez des règles explicites allow/deny dans DOCKER-USER basées sur :
- Interface d’ingress (publique vs privée)
- Port de destination (services publiés)
- CIDR source (réseaux d’administration, plages VPN, réseaux partenaires)
Pourquoi cela fonctionne : la génération de règles par Docker est complexe et dynamique (containeurs démarrent/s’arrêtent, réseaux apparaissent/disparaissent). Votre politique est comparativement stable. Placez la partie stable là où elle sera toujours évaluée tôt.
À éviter : saupoudrer des règles drop dans des chaînes aléatoires en espérant qu’elles surpassent les accept de Docker. Ce n’est pas de l’ingénierie ; c’est du feeling.
Conception B (pour environnements stricts) : désactiver la programmation pare-feu de Docker et tout gérer dans nftables
Si vous avez des contraintes réglementaires ou si vous construisez une plateforme où Docker est « juste une charge de travail de plus », vous pouvez désactiver l’iptables de Docker. Ensuite :
- Vous activez le forwarding de façon sélective.
- Vous créez des règles nft nat pour chaque sous-réseau bridge Docker.
- Vous autorisez explicitement le forwarding pour les ports publiés et l’egress souhaité.
C’est viable, mais c’est du travail. Vous avez besoin de tests, d’automatisation et de quelqu’un d’astreinte capable de raisonner sur le flux de paquets à 03:00.
Conception C (a l’air sensée mais ne l’est pas) : mélanger des règles nft natives avec des corrections iptables ad hoc
Le mode d’échec : vous « corrigez » un problème avec une commande iptables à la va-vite, puis plus tard vous « corrigez » un autre problème en syntaxe nft, puis Docker se met à jour et réécrit des pièces, et maintenant l’ensemble de règles est un gâteau en couches de regrets.
Choisissez un plan de contrôle unique pour la politique écrite par des humains : soit nft natif avec une configuration Docker contrôlée, soit iptables-nft avec une utilisation contrôlée des chaînes. La cohérence vaut mieux que l’ingéniosité.
Trois mini-récits d’entreprise (anonymisés, plausibles, techniquement exacts)
Incident : la mauvaise hypothèse (« On l’a bloqué dans INPUT, donc il ne peut pas être atteint »)
Une société SaaS de taille moyenne a déplacé un ensemble d’outils internes sur une flotte de VM basées sur Debian. Baseline de sécurité : nftables avec une politique INPUT par défaut drop, SSH uniquement depuis le VPN corporatif, et tout le reste fermé. Le déploiement semblait propre.
Puis un développeur a déployé une UI d’administration conteneurisée et l’a publiée avec -p 8443:443 pour « juste tester ». Personne n’a remarqué car aucun service hôte n’écoutait sur 8443 de la manière habituelle, et la chaîne INPUT est restée intacte : drop par défaut, pas d’autorisation 8443. Tout le monde s’est détendu.
Une semaine plus tard, un scan externe a signalé le port 8443 comme ouvert. L’ingénieur d’astreinte a fait la chose normale : a vérifié ss -lntp, a vu un écouteur lié à Docker, a haussé les épaules et ajouté un drop dans INPUT. Le scan montrait toujours ouvert. L’ambiance est passée de « normal » à « on est hantés ».
Cause racine : le trafic était DNATé en PREROUTING et traversait FORWARD, pas INPUT. Les règles d’acceptation de Docker sur le forward (ou votre forward permissif) l’autorisaient.
Correction : appliquez la politique dans DOCKER-USER (drop entrant vers les ports publiés sauf depuis les plages VPN) et changez la publication du conteneur pour ne lier que l’interface VPN. Action post-mortem : cessez de traiter INPUT comme la seule frontière pare-feu sur les hôtes conteneurs.
Optimisation qui s’est retournée : « Désactiver docker-proxy et compter sur nft pur »
Une équipe plateforme voulait des gains de performance et une meilleure observabilité. Ils ont désactivé docker-proxy (une astuce courante) et standardisé sur nftables. Ils s’attendaient à moins de processus, moins de pièces mobiles et une exposition de port plus simple.
En staging, tout semblait plus rapide et « plus natif noyau ». Puis la production a rencontré un bug particulier : certains ports publiés étaient joignables depuis certains réseaux mais pas d’autres, et les health checks ont commencé à flapper seulement pour les clients IPv6. Les ingénieurs ont passé des jours à débattre si c’était le comportement du load balancer, des limites conntrack, ou une mise à jour noyau cassée.
Le vrai problème était la dérive de politique : les règles nft de l’équipe supposaient que les sockets d’écoute représenteraient l’exposition, mais avec le proxy désactivé l’exposition était surtout du DNAT. Leur monitoring vérifiait ss pour les écouteurs et manquait les règles qui ouvraient les chemins. Leur politique IPv6 était incomplète : le comportement NAT et filter était incohérent entre les tables ip et inet.
Correction : considérez la publication de ports comme une fonctionnalité pare-feu, pas comme une fonctionnalité de processus. Surveillez les deltas de l’ensemble de règles nft (ou au moins les comptes de chaînes et règles sélectionnées), appliquez la politique via DOCKER-USER, et concevez explicitement le comportement IPv6 (soit le supporter de bout en bout, soit le désactiver intentionnellement).
L’optimisation n’était pas « mauvaise ». C’était une complexité non prise en charge. C’est ainsi que des « améliorations de performance » deviennent des « incidents de disponibilité ».
Pratique ennuyeuse mais correcte qui a sauvé la mise : dumps de règles dans les tickets d’incident
Une grande entreprise avec un processus de changement conservateur avait une règle simple : chaque ticket d’incident lié au pare-feu doit inclure des pièces jointes de nft list ruleset, iptables -S, et la liste des ports docker ps prise au moment de l’impact. Les ingénieurs râlaient parce que ça semblait bureaucratique.
Puis une intégration d’appliance fournisseur a commencé à échouer de façon intermittente. Le trafic depuis une plage IP partenaire atteignait parfois un endpoint conteneurisé et parfois tombait dans un trou noir. L’équipe applicative accusait le partenaire. Le partenaire accusait l’entreprise. Tout le monde allait planifier une réunion (la manière traditionnelle de résoudre une perte de paquets).
L’astreint a suivi la règle ennuyeuse et a attaché les dumps. Un relecteur a remarqué que l’ensemble de règles actif changeait après un redeploy de conteneur : un nouveau bridge défini par l’utilisateur est apparu, et Docker a inséré des règles de masquerade correspondantes. Les règles custom de l’entreprise faisaient référence à des noms d’interface qui changeaient avec le bridge. La politique était correcte en intention mais fragile dans l’implémentation.
Parce qu’ils avaient des dumps « avant/après », ils n’ont pas eu besoin de devinettes. Ils ont réécrit la politique pour matcher sur des plages d’adresses et utiliser DOCKER-USER pour la restriction d’ingress plutôt que de se baser sur des noms de bridge éphémères. C’était fixé le jour même sans traîner dix personnes dans un événement calendrier.
Pratique ennuyeuse. Issue correcte. C’est le travail.
Erreurs courantes : symptômes → cause racine → correction
1) « Le port est ouvert même si INPUT est en drop »
Symptômes : Un scan externe montre un port publié joignable ; votre chaîne nft input n’a pas de règle allow pour ce port.
Cause racine : Le trafic est DNATé en PREROUTING et traverse FORWARD, pas INPUT. Les règles d’acceptation de Docker sur le forward (ou votre forward permissif) l’autorisent.
Correction : Appliquez les restrictions d’ingress dans DOCKER-USER (préféré) ou dans le hook forward avant que Docker n’accepte. Vérifiez les règles nat PREROUTING pour le port.
2) « J’ai bloqué un conteneur, mais il atteint toujours Internet »
Symptômes : Vous ajoutez des drops dans une chaîne input de l’hôte ; l’egress du conteneur fonctionne toujours.
Cause racine : L’egress du conteneur est du trafic forwardé ; INPUT est hors de propos. De plus, l’egress est souvent masqué, masquant l’IP du conteneur à moins de matcher sur interfaces/sous-réseaux.
Correction : Bloquez dans le chemin forward basé sur le sous-réseau source (plages bridge Docker) ou l’IP du conteneur, ou matchez sur iifname pour les bridges Docker. Préférez des réseaux contrôlés par application.
3) « Après avoir activé le service nftables, le réseau Docker a cassé »
Symptômes : Les conteneurs ne peuvent plus sortir ; les ports publiés ne répondent plus après un reboot.
Cause racine : Le service nftables charge une baseline qui flush ou écrase des chaînes attendues par Docker, ou définit la policy forward sur drop sans fournir les points de saut Docker attendus.
Correction : Décidez du modèle de propriété. Si Docker gère les règles, ne videz pas les tables dont Docker dépend ; incorporez les sauts nécessaires ou gardez la politique dans DOCKER-USER. Si vous possédez tout, désactivez iptables de Docker et implémentez des règles nat/forward équivalentes.
4) « Les règles semblent correctes dans nft, mais iptables montre autre chose »
Symptômes : Le dump nft ne correspond pas à ce qu’un outil basé sur iptables affirme, ou vice versa.
Cause racine : Mélange du legacy iptables avec le backend nft, ou les deux actifs de manière confuse, ou présomption qu’un outil montre tout.
Correction : Standardisez sur iptables-nft si vous devez utiliser la syntaxe iptables. Évitez le legacy. Vérifiez toujours avec nft list ruleset car c’est l’état visible du noyau.
5) « IPv6 se comporte différemment (ou contourne des restrictions) »
Symptômes : Les restrictions IPv4 fonctionnent ; les clients IPv6 peuvent encore se connecter, ou l’inverse.
Cause racine : Les règles n’ont été appliquées qu’aux tables ip (v4), pas inet ; ou Docker/hôte a IPv6 activé avec des chaînes et politiques différentes.
Correction : Utilisez table inet pour les règles de filtrage quand vous voulez la parité, et décidez explicitement si Docker IPv6 doit être activé. Testez les deux piles, n’assumez rien.
6) « Un reboot a tout changé »
Symptômes : Le comportement du pare-feu diffère après redémarrage ; des ports s’ouvrent/se ferment de façon inattendue.
Cause racine : Pas d’ordre de démarrage déterministe : nftables charge après Docker ou l’inverse, et l’un vide/écrase l’autre ; ou des règles ajoutées manuellement n’ont jamais été persistées.
Correction : Rendez l’ordre de démarrage explicite avec des dépendances systemd, persistez correctement les règles, et incluez des vérifications dans la gestion de configuration.
Listes de vérification / plan étape par étape
Checklist A — Vous voulez que Docker continue de fonctionner, mais vous voulez une sécurité prévisible
- Choisissez un seul plan de contrôle de la politique : choisissez soit nftables natif avec des chaînes stables, soit iptables-nft pour l’insertion de politique. Ne improvisez pas.
- Confirmez le backend : assurez-vous que
iptablespointe versiptables-nftet non legacy. - Inventoriez l’exposition : listez les ports publiés via
docker pset faites correspondre au nat nft. - Appliquez la politique d’ingress dans DOCKER-USER : autorisez uniquement les sources prévues ; droppez le reste.
- Gérez IPv6 délibérément : miroir de la politique en utilisant les tables inet, ou désactivez IPv6 pour Docker si votre environnement ne le supporte pas en toute sécurité.
- Rendez les changements persistants : stockez les sources de règles dans la gestion de configuration et assurez-vous qu’elles s’appliquent au démarrage.
- Vérifiez avec des compteurs : ajoutez des compteurs aux règles clés et validez les hits pendant les tests.
- Rédigez une note opérateur : « Les ports publiés sont contrôlés par DOCKER-USER ; n’ouvrez pas de ports dans INPUT en espérant que cela fonctionne. »
Checklist B — Vous voulez que Docker cesse d’injecter des règles (propriété complète)
- Configurez daemon.json de Docker : désactivez
iptableset décidez deip-forward. - Définissez un plan d’adressage : fixez les sous-réseaux bridge Docker pour que les règles ne courent pas après l’aléatoire.
- Écrivez des règles nft nat : MASQUERADE pour l’egress, DNAT pour chaque port publié autorisé.
- Écrivez des règles nft filter : règles forward pour les flux établis, autorisez seulement l’ingress nécessaire vers les conteneurs.
- Testez le comportement au redémarrage : redémarrez Docker et reboot l’hôte ; assurez-vous que les règles restent cohérentes.
- Mettez à jour les runbooks : « Publier un port dans Docker ne fait rien à moins que des règles pare-feu ne soient ajoutées. »
- Automatisez la vérification : CI ou une vérification au démarrage qui affirme que les chaînes clés existent et que les politiques sont correctes.
Checklist C — Politique minimale qui évite les surprises (une bonne base de départ)
- Drop par défaut sur INPUT de l’hôte.
- Drop par défaut sur FORWARD sauf si vous autorisez explicitement le forwarding Docker.
- ct state established,related accept explicite en début.
- Politique explicite dans
DOCKER-USERpour les ports publiés entrants : autoriser uniquement depuis des CIDR de confiance. - Contraindre les ports publiés à des interfaces spécifiques quand c’est possible (lier à une IP VPN, pas à 0.0.0.0).
FAQ
1) Pourquoi Docker touche-t-il mon pare-feu ?
Parce que les conteneurs ont besoin de NAT et de forwarding pour être utiles par défaut, et les ports publiés nécessitent des règles DNAT. Docker optimise pour « installer et exécuter », pas pour « votre modèle de conformité ». Si vous voulez un comportement différent, vous devez le configurer.
2) J’utilise nftables. Pourquoi je vois des chaînes iptables comme DOCKER-USER ?
Sur Debian 13, iptables utilise typiquement le backend nft (iptables-nft). Docker programme la sémantique iptables, qui devient des objets nftables en coulisses. Différente syntaxe, même ensemble de règles noyau.
3) DOCKER-USER est-il le bon endroit pour mettre mes règles de sécurité ?
Si vous laissez Docker gérer la plomberie iptables/nft, oui. Docker saute vers DOCKER-USER tôt dans le forwarding, précisément pour que les opérateurs puissent appliquer la politique avant les accept de Docker. Utilisez-le.
4) Pourquoi bloquer dans la chaîne nft input n’a-t-il pas stoppé l’accès à un port publié ?
Parce que le paquet peut ne pas traverser INPUT. Avec DNAT, le noyau peut traiter le trafic entrant comme du trafic forwardé vers une autre interface (le bridge Docker), donc FORWARD est la porte.
5) Dois-je désactiver la gestion iptables de Docker ?
Seulement si vous avez une raison forte et la maturité opérationnelle pour posséder la NAT/le forwarding de bout en bout. Sinon, laissez la plomberie de Docker et appliquez la politique au point d’étranglement prévu.
6) Comment empêcher les développeurs de publier accidentellement des services sur Internet ?
Combinez des contrôles : appliquez un drop par défaut dans DOCKER-USER pour l’ingress depuis l’interface publique, et exigez la publication uniquement sur loopback ou une interface privée/VPN. Ajoutez des contrôles CI pour les fichiers Compose et une surveillance runtime des ports publiés.
7) Qu’en est-il de Docker rootless ?
Rootless change le modèle réseau et réduit souvent la manipulation directe de netfilter, mais introduit d’autres composants (slirp4netns, user namespaces) et différents compromis de performance/fonctionnalités. Il peut aider à ce que « Docker ne réécrive pas les règles du pare-feu », mais ce n’est pas un remplaçant universel prêt pour la production.
8) Pourquoi des règles disparaissent après reboot ?
Généralement ordre de démarrage et persistance. Docker peut ajouter des règles au démarrage ; le service nftables peut ensuite les vider ou les remplacer ; ou vous avez ajouté des règles manuellement sans les sauvegarder. Corrigez en rendant le modèle de propriété explicite et en assurant que le bon service charge les bonnes règles au bon moment.
9) Puis-je tout gérer dans un seul ensemble de règles nftables et continuer à utiliser Docker normalement ?
Oui, mais vous devez vous aligner sur la manière dont Docker attend que les chaînes et hooks existent, ou désactiver la gestion des règles par Docker et reproduire vous-même le comportement nat/forward requis. L’objectif « ensemble de règles unique » est correct ; la croyance « les surprises s’arrêtent automatiquement » ne l’est pas.
Conclusion : prochaines étapes que vous pouvez faire aujourd’hui
Si Docker a surpris votre pare-feu nftables sur Debian 13, la correction n’est pas héroïque. C’est de la clarté. Décidez qui possède la programmation netfilter, puis appliquez la politique dans le bon hook avec les bons outils.
- Dumper l’ensemble de règles en direct (
nft list ruleset) et identifier les chaînes Docker et les points de saut. - Inventorier les ports publiés (
docker ps) et les faire correspondre aux règles nat DNAT. - Mettre en place une porte politique dans
DOCKER-USERpour contraindre l’accès entrant aux ports publiés. - Rendre cela persistant et tester les reboots afin que « ça a marché une fois » devienne « ça marche toujours ».
- Documenter le chemin du paquet pour votre équipe : INPUT n’est pas la seule porte.
Votre pare-feu doit être ennuyeux. Docker ne le rendra pas ennuyeux pour vous. C’est votre travail.