Ubuntu 24.04 : UFW + Docker — sécuriser les conteneurs sans casser Compose (cas n°40)

Cet article vous a aidé ?

Vous avez activé UFW. Vous avez refusé tout le trafic entrant. Vous vous êtes senti responsable. Puis vous avez lancé docker compose up
et — surprise — votre conteneur est joignable depuis Internet malgré tout. Pas “peut-être”. Pas “seulement depuis le LAN”.
Joignable. Depuis. Partout.

C’est l’une de ces réalités du réseau Linux qui revient sans cesse parce que les valeurs par défaut sont optimisées pour « ça marche »
plutôt que pour « c’est sûr ». La bonne nouvelle : vous pouvez verrouiller ça sur Ubuntu 24.04 sans casser Docker Compose,
sans remplacer UFW et sans transformer votre hôte en projet expérimental de pare-feu.

Le modèle mental : pourquoi UFW et Docker s’opposent

UFW est une interface. Il écrit des règles dans le pare-feu du noyau (sur Ubuntu 24.04 il s’agit généralement de nftables en dessous,
mais de nombreux outils parlent encore la « sémantique iptables »). Docker est aussi une interface. Il écrit des règles de pare-feu pour que
le réseau des conteneurs « fonctionne simplement » : NAT pour la sortie, publication de ports pour l’entrée et isolation entre ponts.

Le conflit survient parce que Docker insère des règles dans des endroits que UFW ne contrôle pas, et avec des priorités qui battent votre
posture haute-niveau « refuser l’entrée ». Quand vous publiez un port (-p 8080:80 ou Compose ports:),
Docker programme des règles DNAT et de filtrage pour que les paquets soient transférés vers le conteneur. Ces paquets peuvent ne jamais atteindre
la règle que vous pensiez bloquante. UFW dit « refuser » ; Docker dit « j’ai promis que ce port fonctionnerait » ; Docker gagne.

La conclusion pratique : vous ne « réglez » pas ce problème en ajoutant plus d’instructions UFW allow et deny. Vous le réglez en
contrôlant le chemin spécifique que Docker utilise pour le trafic transféré. Cela signifie : comprendre le chemin FORWARD,
comprendre les chaînes Docker (en particulier DOCKER-USER) et décider ce qui doit être joignable depuis où.

Une vérité sèche : le pare-feu n’est pas « entrant vs sortant ». C’est la direction du trafic plus les décisions de routage. Les conteneurs
ne sont pas des processus locaux ; ils se trouvent derrière un routeur virtuel. Donc votre politique « entrante » n’est pas nécessairement
appliquée au trafic qui leur est transféré.

Une citation pour rester honnête, issue du monde de la fiabilité : « L’espoir n’est pas une stratégie. » — Gene Kranz.

Blague n°1 : le réseau Docker, c’est comme une carte magnétique d’hôtel : pratique jusqu’au moment où vous réalisez qu’elle ouvre aussi la porte « réservé au personnel ».

Faits et contexte historique utiles pour argumenter

  • UFW date de l’ère iptables et pense encore souvent en ces termes, même lorsque nftables est le backend.
  • Docker s’est historiquement appuyé sur iptables pour implémenter le NAT et la publication de ports ; cette hypothèse de conception persiste selon les distributions.
  • Netfilter Linux possède plusieurs crochets (PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING). Le « refuser l’entrée » d’UFW cible surtout INPUT, pas FORWARD.
  • Docker publie des ports avec DNAT ; les paquets destinés à l’IP de l’hôte peuvent être réécrits vers une IP de conteneur avant même que la chaîne INPUT d’UFW n’ait son mot à dire.
  • La chaîne DOCKER-USER existe précisément pour que les opérateurs puissent insérer des règles de filtrage qui s’appliquent avant les règles acceptées par Docker.
  • Les politiques par défaut d’UFW ont évolué au fil des années, mais le schéma classique reste : refuser par défaut sur INPUT, autoriser established/related, gérer des allow spécifiques.
  • Ubuntu est passé à nftables comme backend préféré ; les commandes « iptables » peuvent être des wrappers (iptables-nft) générant des règles nft.
  • « iptables=false » pour Docker n’est pas une solution gratuite ; désactiver la gestion des règles Docker casse des comportements réseau courants à moins que vous ne les remplaciez vous-même.
  • Le trafic conteneur-à-conteneur sur le même pont n’est pas « sortant » ; il s’agit de trafic L2/L3 interne à l’hôte et peut contourner une intention naïve du pare-feu à moins d’être filtré.

Mode d’action pour un diagnostic rapide

Vous voulez le chemin le plus rapide de « le port est exposé » à « je sais exactement quelle chaîne l’a autorisé ». Voici l’ordre qui
trouve le goulot rapidement.

1) Confirmez ce qui est réellement exposé (ne faites pas confiance aux fichiers Compose)

  • Vérifiez les ports publiés du point de vue de Docker (docker ps).
  • Vérifiez les sockets en écoute sur l’hôte (ss -ltnp).
  • Probe depuis un système distant ou un deuxième namespace réseau NIC si vous en avez un.

2) Identifiez le chemin du paquet

  • Si c’est un port publié, il passe probablement par DNAT dans nat PREROUTING puis par le filtrage FORWARD.
  • Si c’est networking host (network_mode: host), ça passe par le filtrage INPUT comme tout démon.

3) Inspectez la règle gagnante

  • Vérifiez DOCKER-USER en premier (votre point de contrôle).
  • Puis inspectez les chaînes propres à Docker (DOCKER, DOCKER-FORWARD).
  • Puis vérifiez la politique de forwarding d’UFW et si UFW voit même le paquet.

4) Corrigez avec le plus petit changement qui rend la posture de sécurité vraie

  • Si vous avez seulement besoin d’être « joignable depuis le LAN », filtrez par subnet source dans DOCKER-USER.
  • Si vous avez seulement besoin d’être « joignable depuis un conteneur reverse proxy », arrêtez de publier le port de l’application et utilisez des réseaux internes.
  • Si vous devez être « joignable seulement sur localhost », liez les ports publiés à 127.0.0.1.

État cible : que signifie « verrouillé »

« Conteneurs verrouillés » est vague. En pratique, choisissez un de ces états cibles raisonnables et implémentez-le explicitement :

  1. Par défaut : rien n’est publié. Les conteneurs communiquent via des réseaux Compose privés. Seul un reverse proxy
    (ou un service passerelle) publie 80/443.
  2. Publication sélective. Quelques ports sont publiés, mais seulement vers des réseaux sources spécifiques
    (VPN d’entreprise, plages IP du bureau) et jamais vers Internet entier sauf si le service doit être public.
  3. Modèle localhost pour dev sur hôtes de prod (oui, parfois c’est nécessaire) : publier sur 127.0.0.1 et
    exiger un tunnel SSH, VPN ou un accès depuis l’hôte.
  4. Isolation stricte entre ponts Docker. Le trafic inter-conteneurs est autorisé uniquement là où vous le déclarez.

Ce qu’il faut éviter : « refuser par défaut l’entrée » sur UFW puis laisser Docker publier des ports librement. C’est une contradiction de politique avec un résultat prévisible.

Tâches pratiques (commandes, sorties, décisions)

Voici les tâches que j’exécute réellement sur Ubuntu 24.04 quand je diagnostique ou renforce UFW + Docker. Chacune inclut
la commande, ce que signifie une sortie typique, et la décision à prendre.

Task 1: Verify UFW state and default policies

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

Signification : « routed » contrôle le forwarding (FORWARD). Si c’est disabled, UFW peut ne pas surveiller le trafic transféré
comme vous le supposez.

Décision : Si des conteneurs sont exposés, vous devez aborder le comportement de forwarding (généralement via DOCKER-USER),
pas seulement les règles INPUT.

Task 2: Confirm Docker is managing iptables/nft rules

cr0x@server:~$ sudo docker info --format '{{json .SecurityOptions}}'
["name=apparmor","name=seccomp,profile=builtin","name=cgroupns"]

Signification : Cela ne montre pas directement iptables, mais confirme un environnement Docker normal. Ensuite vérifiez
la config du démon.

Décision : Inspectez /etc/docker/daemon.json avant de supposer quoi que ce soit sur le comportement firewall de Docker.

Task 3: Check Docker daemon iptables setting

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "iptables": true,
  "ip-forward": true
}

Signification : Docker va programmer des règles de pare-feu. C’est la situation commune/par défaut.

Décision : Gardez-le. Le désactiver est la manière dont les gens réinventent accidentellement le NAT à 2 h du matin.

Task 4: List published ports as Docker sees them

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES          PORTS
web            0.0.0.0:8080->80/tcp
db             5432/tcp
prometheus     127.0.0.1:9090->9090/tcp

Signification : 0.0.0.0:8080 est accessible depuis le monde (sous réserve du pare-feu). 127.0.0.1:9090
est accessible localement seulement.

Décision : Si un service n’a pas besoin d’ingress public, supprimez le port publié ou liez-le à localhost.

Task 5: Verify host listening sockets (reality check)

cr0x@server:~$ sudo ss -ltnp | head -n 12
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=2214,fd=4))
LISTEN 0      4096   127.0.0.1:9090     0.0.0.0:*     users:(("docker-proxy",pid=2311,fd=4))
LISTEN 0      4096   0.0.0.0:22         0.0.0.0:*     users:(("sshd",pid=1042,fd=3))

Signification : Docker-proxy (ou le NAT noyau) accepte des connexions sur l’hôte. S’il est lié sur 0.0.0.0,
votre pare-feu doit être correct sinon vous êtes exposé.

Décision : Considérez les liaisons 0.0.0.0 comme « publiques jusqu’à preuve du contraire ».

Task 6: Identify which firewall backend you’re effectively using

cr0x@server:~$ sudo update-alternatives --display iptables | sed -n '1,12p'
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
  slave iptables-restore is /usr/sbin/iptables-restore
  slave iptables-save is /usr/sbin/iptables-save

Signification : Vous utilisez iptables-nft en compatibilité, ce qui est correct. L’important est la cohérence.

Décision : Évitez de mélanger des règles nft brutes qui entreraient en conflit avec iptables-nft à moins d’avoir la main totale sur l’ensemble.

Task 7: Inspect the DOCKER-USER chain (your policy hook)

cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN

Signification : Aucune restriction n’est appliquée avant les règles acceptées par Docker.

Décision : C’est ici que vous ajoutez « refuser par défaut, autoriser ce qui est nécessaire » pour les ports publiés.

Task 8: Inspect FORWARD policy and Docker forwarding chains

cr0x@server:~$ sudo iptables -S FORWARD
-P FORWARD DROP
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-FORWARD

Signification : DROP par défaut est une bonne chose, mais le saut vers DOCKER-FORWARD signifie que Docker peut encore autoriser des flux spécifiques.

Décision : Faites respecter vos contraintes dans DOCKER-USER, avant que DOCKER-FORWARD n’accepte le trafic.

Task 9: List Docker-created NAT rules for published ports

cr0x@server:~$ sudo iptables -t nat -S DOCKER | head -n 20
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.18.0.3:80
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9090 -j DNAT --to-destination 172.18.0.5:9090

Signification : DNAT réécrit le trafic destiné au port 8080 de l’hôte vers le conteneur. Voilà pourquoi les règles INPUT
ne racontent pas toute l’histoire.

Décision : Si vous voulez bloquer l’accès public, bloquez-le dans filter FORWARD/DOCKER-USER selon l’IP source.

Task 10: Add a “default deny for Docker published ports” baseline in DOCKER-USER

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -i lo -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 3 -s 10.0.0.0/8 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 4 -s 192.168.0.0/16 -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -i lo -j ACCEPT
-A DOCKER-USER -s 10.0.0.0/8 -j ACCEPT
-A DOCKER-USER -s 192.168.0.0/16 -j ACCEPT
-A DOCKER-USER -j RETURN
-A DOCKER-USER -j DROP

Signification : La sortie exemple montre un piège courant : Docker (ou des règles précédentes) peut déjà inclure RETURN.
Si RETURN apparaît avant DROP, votre DROP ne s’exécutera pas.

Décision : Assurez-vous que le DROP est avant tout RETURN inconditionnel, ou réécrivez proprement le contenu de la chaîne.
En pratique, vous voulez : autoriser established, autoriser sources de confiance, puis drop, puis return (ou juste drop).

Task 11: Cleanly rewrite DOCKER-USER to avoid rule order surprises

cr0x@server:~$ sudo iptables -F DOCKER-USER
cr0x@server:~$ sudo iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -s 10.10.0.0/16 -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -s 192.168.50.0/24 -j ACCEPT
cr0x@server:~$ sudo iptables -A DOCKER-USER -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -s 10.10.0.0/16 -j ACCEPT
-A DOCKER-USER -s 192.168.50.0/24 -j ACCEPT
-A DOCKER-USER -j DROP

Signification : Déterministe : si vous ne venez pas des sous-réseaux autorisés et que vous n’êtes pas established, vous êtes drop avant que Docker n’accepte quoi que ce soit.

Décision : Utilisez ceci comme baseline sur des hôtes qui ne doivent pas publier des services arbitraires vers Internet public.

Task 12: Verify UFW isn’t silently allowing routed traffic

cr0x@server:~$ sudo grep -nE 'DEFAULT_FORWARD_POLICY|IPV6' /etc/default/ufw
7:DEFAULT_FORWARD_POLICY="DROP"
18:IPV6=yes

Signification : Forward par défaut est DROP. Bien. IPv6 est activé ; si vous l’ignorez, vous pouvez « sécuriser » IPv4 et quand même fuir via IPv6.

Décision : Si vous utilisez IPv6 (probable), dupliquez la politique pour ip6tables/nft.

Task 13: Check UFW’s “before” rules for Docker interaction

cr0x@server:~$ sudo sed -n '1,140p' /etc/ufw/before.rules | sed -n '1,40p'
#
# rules.before
#
# Rules that should be run before the ufw command line added rules. Custom
# rules should be added to one of these chains:
#   ufw-before-input
#   ufw-before-output
#   ufw-before-forward
#

Signification : UFW s’attend à ce que vous ajoutiez les politiques de chemin forward dans ufw-before-forward si nécessaire.

Décision : Préférez DOCKER-USER pour les contraintes spécifiques à Docker. Utilisez ufw-before-forward pour une politique de routage plus large.

Task 14: Confirm Docker networks and bridge interfaces

cr0x@server:~$ docker network ls
NETWORK ID     NAME              DRIVER    SCOPE
a1b2c3d4e5f6   bridge            bridge    local
b2c3d4e5f6a1   myapp_default     bridge    local
c3d4e5f6a1b2   host              host      local
d4e5f6a1b2c3   none              null      local

Signification : Chaque pont défini par l’utilisateur (comme myapp_default) peut avoir sa propre interface et comportement d’isolation.

Décision : Si vous essayez de restreindre le trafic est-ouest, réfléchissez par pont, pas seulement docker0.

Task 15: Map a published port to a specific container IP (for precise rules)

cr0x@server:~$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web
172.18.0.3

Signification : Vous pouvez écrire une règle DOCKER-USER qui autorise seulement le trafic vers ce conteneur/port (utile pour des exceptions).

Décision : Préférez des politiques basées sur le sous-réseau ; les IP par conteneur changent. N’utilisez des IP statiques que si nécessaire.

Task 16: Test from an untrusted source and watch counters

cr0x@server:~$ sudo iptables -L DOCKER-USER -v -n
Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination
   40  2400 ACCEPT     all  --  *      *       10.10.0.0/16         0.0.0.0/0
   12   720 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0

Signification : Les compteurs augmentent. Vos règles voient réellement du trafic. C’est la meilleure sensation avec des pare-feux.

Décision : Si les compteurs ne bougent pas, vous filtrez la mauvaise chaîne/crochet (commun avec le host networking).

Task 17: Persist rules across reboot (don’t rely on memory)

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y iptables-persistent
Reading package lists... Done
Building dependency tree... Done
Suggested packages:
  firewalld
The following NEW packages will be installed:
  iptables-persistent netfilter-persistent
Setting up iptables-persistent ...
Saving current rules to /etc/iptables/rules.v4...
Saving current rules to /etc/iptables/rules.v6...

Signification : Vos règles v4/v6 actuelles sont sauvegardées et restaurées au démarrage.

Décision : Si vous utilisez des règles DOCKER-USER, persistez-les. Sinon, le prochain reboot « réparera » votre pare-feu vers l’état non sécurisé.

Task 18: Validate IPv6 exposure (the quiet footgun)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}' | sed -n '1,5p'
NAMES          PORTS
web            :::8080->80/tcp
prometheus     127.0.0.1:9090->9090/tcp

Signification : :::8080 est IPv6-any. Si vous verrouillez seulement IPv4, vous êtes toujours joignable via IPv6.

Décision : Dupliquez la politique DOCKER-USER dans ip6tables (ou assurez-vous que nft couvre les deux familles).

Blague n°2 : IPv6, c’est comme la clé de secours sous le paillasson — tout le monde oublie qu’elle existe jusqu’à ce que la mauvaise personne la trouve.

Patrons Compose qui ne sabotent pas votre pare-feu

1) Ne publiez pas ce dont vous n’avez pas besoin

La règle de pare-feu la plus propre est celle dont vous n’avez pas besoin parce que le port n’est tout simplement pas exposé. Compose
facilite la publication de tout pendant le développement puis l’oubli.

Préférez expose: (interne) à ports: (publié). Oui, « expose » sert surtout de documentation et de comportement réseau
interne Docker, mais l’idée clé est : les services internes doivent être joignables uniquement par les conteneurs pairs.

2) Liez les ports hôtes à localhost pour les interfaces d’administration

Si vous avez besoin de Grafana/Prometheus/panneaux d’administration sur un serveur, liez-les à 127.0.0.1 et accédez-y via un tunnel SSH ou un VPN.
Cela évite de jouer au casse-tête avec des exceptions de pare-feu.

Dans Compose :
ports: ["127.0.0.1:9090:9090"].
C’est aussi simple. Ce n’est pas « sécurité par l’obscurité » ; c’est refuser tout simplement de router le trafic.

3) Utilisez un reverse proxy comme seul bord exposé

Le meilleur modèle de production : publier 80/443 pour un conteneur reverse proxy (ou un démon hôte), et garder tout le reste
sur des réseaux Docker internes. Alors vos règles de pare-feu peuvent être ennuyeuses.

Ajoutez un réseau « internal: true » pour les backends. Cela indique à Docker de ne pas fournir de connectivité externe via ce réseau.
Cela ne résoudra pas tous les cas, mais vous oriente vers une topologie saine.

4) Évitez network_mode: host sauf si vous le voulez vraiment

Le host networking contourne la plupart du réseau virtuel Docker et fait du conteneur un processus de l’hôte.
Cela change les crochets de pare-feu qui s’appliquent. Cela rend aussi les collisions de ports plus amusantes.

Utilisez-le pour des agents de monitoring sensibles à la performance ou des outils réseau spécialisés quand vous avez une raison. Sinon,
c’est un raccourci qui devient une responsabilité lors d’un incident.

5) Déclarez des réseaux séparés pour « bord public » et « backend privé »

Des réseaux séparés vous donnent une séparation raisonnée. Un réseau attaché au proxy et à l’app,
un autre attaché seulement aux backends. Vous pouvez aussi restreindre le routage inter-réseaux au niveau du pare-feu si nécessaire.

La chaîne DOCKER-USER : votre levier

La documentation Docker a introduit DOCKER-USER parce que les opérateurs avaient besoin d’un point d’insertion stable que Docker ne réécrirait pas.
La chaîne est appelée depuis FORWARD tôt (selon la version), et c’est l’endroit approprié pour faire respecter « quelles sources peuvent
atteindre les ports publiés des conteneurs ».

Pensez-y comme la couche « politique » au-dessus de la couche « plomberie » de Docker. Docker met en place la plomberie pour que les paquets puissent atteindre les conteneurs.
Vous appliquez la politique pour que seuls les paquets désirés le fassent réellement.

Que mettre dans DOCKER-USER

  • Autoriser established/related en premier (le trafic de retour doit fonctionner).
  • Autoriser depuis des subnets sources de confiance (VPN, bureau, plage bastion).
  • Éventuellement autoriser des services publics spécifiques (par ex. 80/443 vers le proxy uniquement).
  • Dropper tout le reste qui est transféré vers les réseaux Docker.

Que ne pas mettre dans DOCKER-USER

  • Des règles par IP de conteneur sauf si vous avez épinglé les IP et accepté la dette opérationnelle.
  • Des règles qui supposent que seul docker0 existe (Compose crée plusieurs ponts).
  • Des règles qui dropent sans autoriser established d’abord (vous casserez les chemins de retour sortant).

Schémas d’intégration UFW qui fonctionnent sur Ubuntu 24.04

Il y a deux grandes stratégies qui n’aboutissent pas à des pleurs :

  1. Conserver UFW pour les services hôtes ; appliquer l’ingress conteneur dans DOCKER-USER. C’est ma recommandation par défaut.
    UFW reste l’outil convivial pour SSH, node exporters, etc. DOCKER-USER devient le « périmètre conteneur ».
  2. Placer plus de politique dans la chaîne forward d’UFW (ufw-before-forward et voisins). Cela peut marcher, mais vous êtes maintenant
    en train de déboguer les interactions entre chaînes gérées par UFW et chaînes gérées par Docker. C’est faisable ; ce n’est juste pas le chemin le plus rapide.

Ma posture de production opinionnée

Gardez le refus par défaut sur les entrées d’UFW. Autorisez SSH seulement depuis des sources de confiance (VPN ou bastion). Publiez seulement 80/443 publiquement.
Tout le reste soit :

  • n’est pas publié du tout (réseaux Docker internes), ou
  • est publié sur 127.0.0.1, ou
  • est autorisé seulement depuis des sous-réseaux privés via DOCKER-USER.

IPv6 : décidez, puis appliquez

Sur Ubuntu 24.04, IPv6 n’est pas un cas marginal. Si votre serveur a un AAAA, il est joignable. Si Docker publie sur ::,
vous avez besoin d’une stratégie v6. Cette stratégie peut être « désactiver IPv6 partout » (difficile, parfois correct) ou « le filtrer correctement »
(plus courant dans les environnements modernes).

Trois mini-récits d’entreprise depuis le terrain

Incident causé par une mauvaise hypothèse : « refuser l’entrée signifie refuser l’entrée »

Une entreprise SaaS de taille moyenne a déplacé quelques outils internes sur un nouveau cluster de VM Ubuntu 24.04. L’équipe plateforme
avait une norme : UFW activé, refuser par défaut l’entrée, autoriser SSH depuis la plage VPN. Ils ont utilisé Docker Compose pour déployer
un tableau de bord interne plus une base de données et une stack métriques. Ça semblait propre.

L’hypothèse erronée était discrète et classique : ils supposaient que le « refuser l’entrée » d’UFW bloquerait tout ce qui est joignable via
l’IP de l’hôte, y compris les ports des conteneurs. Lors d’un scan externe de routine (pas même un pentest dédié), quelqu’un a remarqué
que la page de login du tableau de bord était joignable sur un port élevé. Ce n’était pas censé être public. Et le service n’était pas à jour.

La première réponse fut « mais UFW est actif ; ça ne peut pas être exposé ». La seconde réponse fut de regarder docker ps et
de voir 0.0.0.0:PORT->CONTAINER. La troisième réponse fut inconfortable : ils avaient construit une posture de sécurité
à partir d’une abstraction d’interface plutôt que de la réalité du flux de paquets.

La correction n’a pas été héroïque. Ils ont arrêté de publier le port du tableau de bord, l’ont mis derrière le reverse proxy existant,
et ont appliqué une allowlist DOCKER-USER pour les quelques services d’administration qui avaient réellement besoin d’un accès direct depuis le subnet VPN.
La leçon retenue : « entrant » et « transféré » ne sont pas la même chose, et Docker vit dans le monde du forwarding.

Optimisation qui a mal tourné : désactiver la gestion iptables de Docker

Une autre organisation avait une revue sécurité qui n’aimait pas « des applications modifiant les règles du pare-feu ». C’est un instinct raisonnable.
L’équipe a décidé de mettre "iptables": false dans la config du démon Docker et de gérer tout via UFW uniquement.
Ils ont fait cela en staging, vu les conteneurs démarrer, et appelé ça une victoire.

Le premier retour de bâton fut subtil : la connectivité sortante depuis les conteneurs est devenue aléatoire de façons difficiles à corréler.
Certaines images tiraient lentement, des webhooks expiraient, et DNS échouait de manière intermittente selon le nœud. Ce n’était pas « down » ; c’était « bizarre ». Le bizarre coûte cher.

Le second retour de bâton fut opérationnel : chaque projet Compose nécessitait désormais des règles NAT et de forwarding personnalisées. Les développeurs
ne savaient plus quels ports étaient routés où parce que ce n’était plus exprimé dans le Compose file. Les règles du pare-feu sont devenues de la connaissance tribale.
Les changements prenaient plus de temps et la réponse aux incidents était plus lente.

Ils ont fini par revenir en arrière. Docker a repris la gestion d’iptables/nft. L’équipe sécurité a obtenu ce qu’elle voulait réellement — le contrôle de la politique —
en imposant des restrictions dans DOCKER-USER et en utilisant un template standard pour les subnets autorisés et les ports de bord public. « Ne laissez pas Docker toucher iptables »
sonnait propre. En pratique, ça avait remplacé un mécanisme commun par un mécanisme sur-mesure. Ce n’est pas de la sécurité ; c’est de la dette avec un badge.

Pratique ennuyeuse mais correcte qui a sauvé la mise : persistance des règles et test de reboot

Une entreprise régulée effectuait des fenêtres de maintenance trimestrielles où les hôtes étaient rebootés, les noyaux mis à jour, et la suite habituelle de changements déployée.
Une équipe avait l’habitude : après tout changement de pare-feu, ils faisaient (1) persister les règles, (2) rebooter un nœud canari, et (3) vérifier l’exposition depuis l’extérieur du subnet. Ennuyeux. Répétitif. Terriblement correct.

Lors d’une fenêtre, ils ont mis à jour Docker et rafraîchi les politiques UFW. Tout avait l’air OK — jusqu’au reboot du canari.
Leur vérification externe montrait un port de service ouvert qui devait être VPN-only. L’équipe n’a pas paniqué ; elle a suivi sa checklist et a remarqué que la chaîne DOCKER-USER était présente mais vide après le reboot. Les règles n’avaient pas persisté.

Parce qu’ils l’ont attrapé sur un canari, l’impact a été faible : ils ont réinstallé l’outil de persistance, sauvegardé les règles v4 et v6,
et validé à nouveau. Le reste du parc a suivi la baseline corrigée. Aucun incident. Aucun email client. Pas de réunion « comment cela a-t-il passé la revue ? ».

La morale n’est pas glamour : si vous ne testez pas un reboot, vous n’avez pas une configuration. Vous avez une humeur.

Erreurs courantes : symptômes → cause → correctif

1) « UFW est activé mais le port du conteneur est quand même joignable »

Symptôme : Des clients distants atteignent host:8080 même avec refuser par défaut l’entrée.

Cause : Le trafic est DNATé et transféré ; la politique INPUT d’UFW ne s’applique pas. Les règles Docker l’autorisent.

Correctif : Ajoutez une allowlist + policy drop dans DOCKER-USER, ou arrêtez de publier le port.

2) « J’ai ajouté DOCKER-USER DROP mais rien n’a changé »

Symptôme : Les compteurs ne bougent pas ; le port reste ouvert.

Cause : Vous utilisez le host networking, ou votre règle DROP est après un RETURN inconditionnel, ou vous ne filtrez que l’IPv4.

Correctif : Vérifiez iptables -S DOCKER-USER pour l’ordre des règles ; vérifiez network_mode: host ; dupliquez les règles en ip6tables.

3) « Après reboot, tout est exposé à nouveau »

Symptôme : La politique fonctionne jusqu’à un redémarrage.

Cause : Les règles DOCKER-USER n’ont pas été persistées ; seul l’état runtime a changé.

Correctif : Utilisez iptables-persistent ou une unité systemd qui restaure les règles avant le démarrage de Docker.

4) « Seuls certains utilisateurs peuvent se connecter ; d’autres expirent »

Symptôme : Les utilisateurs VPN fonctionnent, les utilisateurs bureau non (ou inversement).

Cause : L’allowlist basée sur la source n’inclut pas tous les sous-réseaux clients réels ; le NAT fait apparaître une source différente.

Correctif : Validez l’IP source du client sur le serveur (tcpdump/conntrack), puis étendez l’allowlist délibérément.

5) « Le trafic inter-conteneurs est bloqué de façon inattendue »

Symptôme : L’app ne peut pas joindre la BDD bien qu’elles soient dans le même projet Compose.

Cause : DROP trop large dans DOCKER-USER sans exceptions pour le trafic du pont interne.

Correctif : Autorisez established/related, et si vous droppez par défaut, ajoutez des allows explicites pour les sous-réseaux internes ou les interfaces avant le drop.

6) « IPv4 est verrouillé, mais les scanners voient quand même le port ouvert »

Symptôme : Un scan externe montre des ports ouverts malgré les règles IPv4.

Cause : Exposition IPv6 (:::) avec politique ip6tables/nft manquante.

Correctif : Implémentez une politique IPv6 équivalente, ou désactivez volontairement IPv6 (hôte + Docker) et vérifiez.

7) « Docker Compose updates cassent mon pare-feu »

Symptôme : Après compose up, les règles changent et l’accès varie.

Cause : Vos contraintes sont dans des chaînes gérées par Docker plutôt que dans DOCKER-USER, ou vous dépendez de noms d’interface instables.

Correctif : Conservez la politique dans DOCKER-USER et utilisez des critères de correspondance stables (subnets source, ports destination, état conntrack).

Checklists / plan pas-à-pas

Plan A (recommandé) : ne publier que les ports de bord, restreindre tout le reste

  1. Inventaire des ports exposés : lancez docker ps et ss -ltnp ; listez tout ce qui est lié à 0.0.0.0 ou :::.
  2. Supprimez les publications accidentelles : retirez ports: des services internes ; utilisez des réseaux internes.
  3. Liez les outils d’administration à localhost : utilisez 127.0.0.1:PORT:PORT dans Compose quand approprié.
  4. Décidez vos plages sources de confiance : subnets VPN, subnets bureau, IP bastion. Notez-les.
  5. Appliquez dans DOCKER-USER : autorisez established/related, autorisez les plages de confiance, droppez le reste.
  6. Dupliquez pour IPv6 : ajoutez les règles ip6tables équivalentes ou assurez-vous que nft couvre les deux familles.
  7. Persistez les règles : installez l’outil de persistance et validez un reboot.
  8. Testez depuis l’extérieur : validez depuis un réseau non fiable et depuis un réseau de confiance.
  9. Surveillez les compteurs : vérifiez les compteurs DOCKER-USER pendant les tests ; confirmez que vos règles sont réellement sur le chemin.

Plan B : Politique centrée UFW (à choisir si vous aimez tracer les chaînes)

  1. Définissez la politique forward d’UFW sur DROP (déjà commun) et assurez-vous que le filtrage routé est activé comme souhaité.
  2. Ajoutez des autorisations forward explicites pour les ponts Docker et les services publiés dans ufw-before-forward.
  3. Vérifiez que Docker n’insère pas de règles accept qui contournent votre intention (vous en reviendrez probablement à DOCKER-USER).

Plan C : « Rien n’est jamais publié » (pour plateformes internes)

  1. Imposez une vérification CI qui rejette les fichiers Compose contenant ports: sauf approbation.
  2. Exigez l’ingress via une couche reverse proxy standardisée et découverte de services interne.
  3. Droppez le trafic transféré vers les ponts Docker depuis les sources non fiables universellement.

FAQ

1) Pourquoi UFW ne bloque-t-il pas par défaut les ports publiés par Docker ?

Parce que le trafic des conteneurs publiés est typiquement transféré après NAT, et la posture « refuser l’entrée » d’UFW gouverne principalement
INPUT. Docker installe des règles de forwarding/NAT pour garantir que la publication de port fonctionne.

2) Dois-je désactiver la gestion iptables de Docker ?

Non, pas comme premier réflexe de sécurité. Cela remplace un mécanisme standard et bien connu par des règles NAT et de forwarding manuelles que vous devez maintenant maintenir.
Utilisez DOCKER-USER pour appliquer la politique pendant que Docker garde la plomberie fonctionnelle.

3) Quelle est la meilleure solution unique qui ne casse pas Compose ?

Ajouter des règles allowlist + drop dans DOCKER-USER, puis retirer les ports: inutiles du Compose. Cela préserve le réseau Docker
tout en évitant l’« exposition surprise sur Internet ».

4) Les règles DOCKER-USER survivent-elles aux redémarrages du daemon Docker ?

Elles survivent typiquement aux redémarrages du daemon Docker parce que la chaîne est destinée à la politique utilisateur, mais elles ne survivront pas
forcément au reboot de l’hôte à moins que vous ne les persistiez. Persistez-les explicitement.

5) Comment restreindre un port publié au LAN seulement ?

Autorisez les subnets LAN dans DOCKER-USER et droppez tout le reste. Alternativement, liez le port publié à une IP d’interface qui n’existe que sur le LAN, mais le filtrage par source est généralement plus clair.

6) Que faire pour des conteneurs qui doivent être publics (comme une app web) ?

Publiez seulement le reverse proxy (80/443) publiquement. Gardez les conteneurs applicatifs non publiés sur des réseaux internes. Si vous devez publier l’application directement,
autorisez seulement ces ports depuis 0.0.0.0/0 et droppez tout le reste.

7) Cela change-t-il quelque chose pour les conteneurs en host-network ?

Oui. Les conteneurs en host-network se comportent comme des processus hôtes ; le trafic atteint INPUT, pas FORWARD. Pour ceux-là, les règles UFW sont le bon point de contrôle, pas DOCKER-USER.

8) Comment savoir si IPv6 expose mes conteneurs ?

Cherchez ::: dans les listings de ports de docker ps ou dans ss -ltnp. Testez ensuite depuis un hôte IPv6-capable et confirmez que votre politique ip6tables/nft correspond à l’intention IPv4.

9) Puis-je tout faire uniquement en nftables ?

Oui, mais si Docker utilise la compatibilité iptables-nft, évitez de gérer des règles conflictuelles. L’approche pragmatique sur Ubuntu est : garder le comportement iptables-nft de Docker
et appliquer la politique dans DOCKER-USER (et l’équivalent v6).

10) Quelle est la manière la plus propre d’éviter des règles par conteneur ?

Ne publiez pas de ports pour les services internes. Utilisez des réseaux Docker internes et un proxy de bord unique. Alors votre pare-feu sera principalement
« autoriser 80/443, autoriser SSH depuis le VPN, dropper le reste », avec DOCKER-USER garantissant que les conteneurs ne contournent pas cela.

Conclusion : prochaines étapes durables

La manière fiable de sécuriser Docker sur Ubuntu 24.04 n’est pas de lutter contre le réseau de Docker. Laissez Docker faire la plomberie. Vous faites
la politique. Mettez votre politique là où elle compte vraiment : sur le chemin transféré, avant les acceptations de Docker, via DOCKER-USER.

Si vous voulez une heure pratique suivante :

  1. Exécutez les tâches d’inventaire : docker ps, ss -ltnp et vérifiez les liaisons IPv6.
  2. Supprimez les ports: accidentels dans Compose ; remplacez par des réseaux internes ou des liaisons localhost.
  3. Implémentez une allowlist DOCKER-USER pour les plages sources de confiance, puis un drop par défaut.
  4. Persistez les règles v4 et v6 et vérifiez avec un reboot sur un hôte canari.
  5. Rédigez votre modèle d’exposition désiré (bord public vs VPN-only vs interne-only) pour que le prochain ingénieur n’introduise pas à nouveau une « exposition surprise sur Internet ».

Vous obtiendrez quelque chose de rare en monde conteneur : une posture de pare-feu qui correspond à ce que vous pensez avoir déployé. Ce n’est pas seulement de la sécurité.
C’est du bon sens opérationnel.

← Précédent
E-mail : les inclusions SPF sont un désordre — comment simplifier sans casser la délivrabilité
Suivant →
Churn des sockets : quand les plateformes deviennent des pièges de mise à niveau

Laisser un commentaire