Le rapport d’incident commence toujours de la même façon : « Aucune modification n’a été faite au réseau. » Puis vous regardez le conteneur et il est connecté à trois réseaux,
dont l’un est plus ou moins public, et il répond joyeusement à des requêtes sur un port dont personne ne se souvient avoir publié.
Les conteneurs multi‑réseaux sont normaux en production : des frontends reliant des backends, des agents joignant à la fois un plane de contrôle et un plane de données, une
surveillance s’étendant sur plusieurs environnements. Ils sont aussi un moyen parfait de fuir du trafic vers le mauvais endroit si vous ne traitez pas le réseau Docker
comme un vrai système de routage et de pare‑feu — parce que c’en est un.
Ce qui se passe réellement avec les conteneurs multi‑réseaux
Un conteneur connecté à plusieurs réseaux Docker est effectivement multi‑homé. Il a plusieurs interfaces, plusieurs routes et parfois plusieurs vues DNS.
Docker installera aussi des règles NAT et de filtrage sur l’hôte qui décident qui peut atteindre quoi. C’est acceptable — jusqu’à ce que vous fassiez des hypothèses
erronées sur les réseaux « internes », les routes par défaut, la publication de ports, ou sur la manière dont Compose « isole » les services.
L’exposition accidentelle a tendance à se produire de quatre façons :
- Ports publiés sur l’hôte et l’hôte est joignable depuis des réseaux auxquels vous n’avez pas pensé (Wi‑Fi, LAN d’entreprise, VPN, peering de VPC cloud, etc.).
-
Pontage de réseaux par conception : un conteneur connecté à la fois au « frontend » et au « backend » devient un point pivot. Si le service se lie à 0.0.0.0,
il écoute sur toutes les interfaces du conteneur. Cela inclut l’interface « erronée ». -
Routage inattendu : la « route par défaut » à l’intérieur du conteneur pointe via le réseau que Docker a décidé d’utiliser comme primaire. Ce ne sera
peut‑être pas celui que vous aviez prévu pour l’egress. -
Dérive du pare‑feu : les règles iptables/nft de Docker ont changé, ont été désactivées, ont été partiellement remplacées par des outils de sécurité sur l’hôte,
ou ont été « optimisées » par quelqu’un qui n’aime pas la complexité (et qui adore aussi les réunions d’incident).
Voici la vérité inconfortable : dans les configurations multi‑réseaux, « ça marche » et « c’est sécurisé » sont orthogonaux. Si vous ne contrôlez pas explicitement les liaisons,
les routes et la politique, vous obtenez le comportement dicté par les paramètres par défaut. Les paramètres par défaut ne sont pas un modèle de menace.
Faits et contexte à connaître
- Le réseau original de Docker était un seul bridge (docker0) avec NAT ; les « bridges définis par l’utilisateur » sont arrivés plus tard pour corriger les bizarreries de DNS/découverte de services et d’isolation.
- La communication inter‑conteneurs se gérait autrefois par un seul drapeau du démon (
--icc) affectant le bridge par défaut ; les réseaux définis par l’utilisateur ont changé la donne. - La publication de ports précède les workflows modernes de Compose et a été conçue pour la commodité des développeurs ; la sécurité en production est quelque chose que vous ajoutez, pas une garantie fournie.
- Docker gérait historiquement iptables directement ; sur des systèmes passés à nftables, la couche de traduction peut créer des surprises dans l’ordre des règles et le débogage.
- Le réseau overlay (Swarm) a introduit des options VXLAN chiffrées, mais le chiffrement ne résout que l’écoute passive, pas l’exposition via des ports mal publiés ou des attachements incorrects.
- Macvlan/ipvlan ont été ajoutés pour répondre aux besoins de « vrai réseau » ; ils contournent aussi les hypothèses confortables que les gens font sur l’isolation du bridge Docker.
- Les réseaux « internal » de Docker bloquent le routage externe mais n’empêchent pas l’accès par des conteneurs attachés à ce réseau, et ils ne nettoient pas les ports publiés.
- Docker rootless change la plomberie ; vous obtenez un réseau en mode utilisateur à la slirp4netns et des caractéristiques de performance et de pare‑feu différentes.
- Les valeurs par défaut de Compose sont pratiques, pas défensives ; le réseau par défaut n’est pas « sûr », il est simplement « présent ».
Une citation à garder sur votre écran, car elle explique la plupart des pannes et des pièges de sécurité en une seule phrase :
L’espoir n’est pas une stratégie.
— idée paraphrasée souvent attribuée aux pratiques de fiabilité/exploitation
Modèle mental : Docker vous construit des routeurs et des pare‑feu
Cessez de penser aux réseaux Docker comme des « étiquettes ». Ce sont des constructions L2/L3 concrètes reposant sur des primitives Linux : bridges, paires veth, namespaces, routes,
état conntrack, NAT et chaînes de filtrage. Quand vous connectez un conteneur à plusieurs réseaux, Docker crée plusieurs interfaces veth dans le netns du conteneur,
et installe typiquement des routes de sorte qu’un de ces réseaux devienne la passerelle par défaut.
Vous devez raisonner sur trois « plans » différents :
- Plan conteneur : quelles interfaces existent dans le conteneur, quelles adresses IP, quelles routes, quels services se lient à quelles adresses.
- Plan hôte : règles iptables/nftables, réglages de bridge, rp_filter, forwarding, et tout autre outil de sécurité hôte.
- Plan réseau amont : la joignabilité de l’hôte (IP publique, VPN, LAN d’entreprise, tables de routage cloud, groupes de sécurité).
Si un seul plan est mal modélisé, vous obtenez « c’était interne » suivi d’une leçon publique d’humilité.
Blague n°1 : Le réseau Docker, c’est comme le Wi‑Fi du bureau — quelqu’un pense toujours que c’est « privé », puis l’imprimante leur prouve le contraire.
Les chemins courants d’exposition (et pourquoi ils surprennent)
1) Les ports publiés se lient plus largement que prévu
-p 8080:8080 publie par défaut sur toutes les interfaces de l’hôte. Si l’hôte est joignable via un VPN, un VPC pairé ou un sous‑réseau d’entreprise, vous venez de publier
là aussi. La publication n’est pas « locale », elle est « sur tout l’hôte ». Le conteneur peut être attaché à dix réseaux ; la publication s’en moque.
La correction est simple et ennuyeuse : liez‑vous à une IP hôte spécifique lorsque vous publiez, et considérez « 0.0.0.0 » comme une odeur suspecte en production.
2) Les réseaux « internal » ne sont pas un champ de force
L’option de réseau --internal de Docker empêche les conteneurs sur ce réseau d’atteindre le monde externe via le comportement de passerelle par défaut de Docker.
Elle n’empêche pas d’autres conteneurs sur le même réseau de les atteindre, et elle ne protège pas magiquement les ports publiés sur l’hôte.
3) Le conteneur multi‑homé écoute sur la mauvaise interface
Les services qui se lient à 0.0.0.0 à l’intérieur du conteneur écoutent sur toutes les interfaces du conteneur. Si vous attachez le conteneur à la fois à
frontend et backend, il peut donc être joignable depuis les deux réseaux, à moins que vous ne fassiez une liaison explicite ou un pare‑feu au niveau du conteneur/hôte.
4) DNS et découverte de services pointent vers la mauvaise IP
Le DNS embarqué de Docker retourne des enregistrements A selon le périmètre réseau du conteneur qui effectue la requête. En scénario multi‑réseau, vous pouvez vous retrouver
avec un nom de service résolvant vers une IP sur un réseau que vous n’aviez pas l’intention d’utiliser pour ce trafic. Cela ressemble à des timeouts intermittents, parce que
parfois vous atteignez le bon chemin, parfois le chemin bloqué.
5) Sélection de route et surprise de la passerelle par défaut
Le premier réseau attaché tend à devenir la route par défaut. Puis quelqu’un attache un réseau « monitoring » plus tard, ou Compose attache les réseaux dans un ordre différent
de celui attendu, et soudain l’egress sort par le mauvais chemin. Cela peut casser des hypothèses d’ACL et faire apparaître des journaux depuis des IP sources inattendues.
6) Les outils de pare‑feu de l’hôte entrent en collision avec les chaînes de Docker
Docker injecte des règles dans iptables. Les outils de durcissement hôte injectent aussi des règles. Les équipes « sécurité » ajoutent parfois un agent qui « gère la politique du pare‑feu »
et qui ne comprend pas les besoins de Docker, si bien qu’il efface ou réordonne les règles. Alors la connectivité casse — ou pire, la mauvaise connectivité fonctionne.
Tâches pratiques : auditer, prouver et décider (commandes + sorties)
Vous ne sécurisez pas le réseau Docker par intuition. Vous le sécurisez en répondant de façon répétée à « qui est joignable depuis où ? » avec des preuves.
Ci‑dessous se trouvent des tâches pratiques à lancer sur un hôte Linux exécutant Docker. Chacune inclut : la commande, ce que signifie la sortie, et la décision à prendre.
Task 1: List containers and their published ports
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
api myco/api:1.9.2 0.0.0.0:8080->8080/tcp
postgres postgres:16 5432/tcp
nginx nginx:1.25 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
Signification : Tout ce qui affiche 0.0.0.0:PORT est exposé sur toutes les interfaces de l’hôte. Les entrées comme 5432/tcp sans mappage
hôte ne sont pas publiées ; elles ne sont joignables que sur les réseaux Docker (sauf si le mode réseau host est utilisé).
Décision : Pour chaque mappage 0.0.0.0, décidez s’il doit être joignable depuis tous les réseaux que touche l’hôte. Sinon, reliez‑le à une IP spécifique ou supprimez la publication.
Task 2: See which networks a container is attached to
cr0x@server:~$ docker inspect api --format '{{json .NetworkSettings.Networks}}'
{"frontend":{"IPAddress":"172.20.0.10"},"backend":{"IPAddress":"172.21.0.10"}}
Signification : Le conteneur est sur deux réseaux. Si le service à l’intérieur se lie à 0.0.0.0, il écoute sur les deux interfaces.
Décision : Si ce conteneur n’a besoin d’accepter du trafic que sur un réseau, détachez‑le de l’autre réseau ou liez le service à l’IP de l’interface prévue.
Task 3: Inspect a Docker network for scope and attached endpoints
cr0x@server:~$ docker network inspect backend --format '{{.Name}} internal={{.Internal}} driver={{.Driver}} subnet={{(index .IPAM.Config 0).Subnet}}'
backend internal=false driver=bridge subnet=172.21.0.0/16
Signification : Il s’agit d’un réseau bridge défini par l’utilisateur, non interne, avec un sous‑réseau privé routable.
Décision : Si les services backend ne doivent jamais sortir vers Internet, envisagez de recréer ce réseau avec --internal et de fournir explicitement tout egress nécessaire via des proxys.
Task 4: Find the container’s PID and enter its network namespace
cr0x@server:~$ docker inspect -f '{{.State.Pid}}' api
23147
cr0x@server:~$ sudo nsenter -t 23147 -n ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.20.0.10/16 brd 172.20.255.255 scope global eth0
44: eth1@if45: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.21.0.10/16 brd 172.21.255.255 scope global eth1
Signification : Deux interfaces, deux sous‑réseaux. Votre service peut être joignable depuis les deux.
Décision : Si une seule interface doit accepter les connexions entrantes, configurez l’application pour qu’elle se lie spécifiquement (par exemple listen 172.20.0.10) ou ajoutez des règles de pare‑feu.
Task 5: Check the container’s routes (who is the default gateway?)
cr0x@server:~$ sudo nsenter -t 23147 -n ip route
default via 172.20.0.1 dev eth0
172.20.0.0/16 dev eth0 proto kernel scope link src 172.20.0.10
172.21.0.0/16 dev eth1 proto kernel scope link src 172.21.0.10
Signification : L’egress sort via frontend (eth0). Si vous attendiez un egress depuis le backend uniquement, vous avez déjà une erreur.
Décision : Décidez quel réseau doit être la route par défaut. Si cela doit être différent, ajustez la façon dont le conteneur est connecté (ordre d’attachement), ou utilisez du routage basé sur la politique dans le conteneur (avancé, fragile).
Task 6: Verify what address/port the process is actually listening on
cr0x@server:~$ sudo nsenter -t 23147 -n ss -lntp
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:(("api",pid=1,fd=7))
Signification : Le service écoute sur toutes les interfaces. En mode multi‑réseaux, ce n’est généralement pas ce que vous voulez.
Décision : Changez le service pour qu’il se lie à l’IP du conteneur voulue, ou pare‑feutez l’interface indésirable. Ne « faites pas confiance » au fait que personne ne pourra atteindre le réseau backend.
Task 7: Confirm which host IPs are actually listening (published ports)
cr0x@server:~$ sudo ss -lntp | grep -E '(:80 |:443 |:8080 )'
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("docker-proxy",pid=1943,fd=4))
LISTEN 0 4096 0.0.0.0:443 0.0.0.0:* users:(("docker-proxy",pid=1951,fd=4))
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=2022,fd=4))
Signification : Des écouteurs existent sur tout l’hôte. Même si vous vouliez « seulement » un accès interne, l’hôte participe maintenant.
Décision : Si l’exposition doit être limitée, publiez comme -p 127.0.0.1:8080:8080 ou sur une adresse LAN/VIP spécifique. Ensuite, placez un vrai proxy devant.
Task 8: Inspect NAT and filter rules that Docker installed
cr0x@server:~$ sudo iptables -t nat -S | sed -n '1,120p'
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.20.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.21.0.0/16 ! -o br-acde1234 -j MASQUERADE
-A DOCKER ! -i br-acde1234 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.20.0.10:8080
Signification : DNAT redirige host:8080 vers le conteneur. Des règles MASQUERADE existent pour les deux sous‑réseaux.
Décision : Si vous comptez sur une politique de pare‑feu, vérifiez qu’elle est mise en œuvre dans la bonne chaîne (souvent DOCKER-USER) et qu’elle correspond à votre intention pour chaque port publié.
Task 9: Check the DOCKER-USER chain (where you should put your policy)
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN
Signification : Aucune politique. Tout ce que Docker autorise est autorisé.
Décision : Ajoutez des règles explicites allow/deny ici pour restreindre qui peut atteindre les ports publiés (sous‑réseaux source, interfaces). Faites‑le avant que les incidents ne vous forcent à agir sous pression.
Task 10: Confirm inter-container connectivity from the “wrong” network
cr0x@server:~$ docker exec -it postgres bash -lc 'nc -vz 172.21.0.10 8080; echo exit_code=$?'
Connection to 172.21.0.10 8080 port [tcp/*] succeeded!
exit_code=0
Signification : Un conteneur censé être backend seulement peut atteindre l’API sur le réseau backend. Cela peut être correct — ou bien votre plan de données touche maintenant votre plan de contrôle.
Décision : Décidez si le réseau backend doit être autorisé à initier du trafic vers ce service. Sinon, appliquez‑le via des règles de pare‑feu ou séparez les responsabilités en services distincts.
Task 11: Validate DNS answers differ across networks (the subtle one)
cr0x@server:~$ docker exec -it nginx sh -lc 'getent hosts api'
172.20.0.10 api
cr0x@server:~$ docker exec -it postgres sh -lc 'getent hosts api'
172.21.0.10 api
Signification : Même nom, IP différente selon le réseau de l’appelant. C’est le comportement attendu — et une cause fréquente de « ça marche depuis X mais pas depuis Y ».
Décision : Si un service doit être atteint uniquement via un réseau, n’attachez pas ce service à l’autre réseau. Le « scoping » DNS n’est pas une frontière de sécurité ; c’est une fonctionnalité de commodité.
Task 12: Show network ordering and the “primary” network in Compose
cr0x@server:~$ docker inspect api --format '{{.Name}} {{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
/api frontend backend
Signification : L’ordre d’attachement est visible, mais pas toujours stable lors de modifications si vous laissez Compose générer automatiquement des réseaux et que vous refactorez.
Décision : Dans Compose, soyez explicite sur les réseaux et envisagez de définir explicitement quel réseau est utilisé pour la route par défaut (en contrôlant l’ordre d’attachement et en minimisant le multi‑homing).
Task 13: Detect containers accidentally using host networking
cr0x@server:~$ docker inspect -f '{{.Name}} network_mode={{.HostConfig.NetworkMode}}' $(docker ps -q)
/api network_mode=default
/postgres network_mode=default
/node-exporter network_mode=host
Signification : Un conteneur contourne l’isolation réseau Docker et partage la pile réseau de l’hôte. Parfois nécessaire, souvent imprudent.
Décision : Si un conteneur utilise network_mode=host, traitez‑le comme un processus hôte. Auditez ses adresses d’écoute et ses règles de pare‑feu comme vous le feriez pour tout démon.
Task 14: Verify if Docker is managing iptables (or not)
cr0x@server:~$ docker info | grep -i iptables
iptables: true
Signification : Docker gère les règles iptables. Si ceci indique false (ou si les règles manquent), la publication de ports et la connectivité se comportent différemment et souvent de manière dangereuse.
Décision : Si votre environnement désactive la gestion iptables de Docker, vous devez implémenter une politique équivalente vous‑même. Ne passez pas à « iptables: false » à la légère, sauf si vous aimez déboguer des trous noirs.
Task 15: Trace a published port from the host to the container
cr0x@server:~$ sudo conntrack -L -p tcp --dport 8080 2>/dev/null | head
tcp 6 431999 ESTABLISHED src=10.10.5.22 dst=10.10.5.10 sport=51432 dport=8080 src=172.20.0.10 dst=10.10.5.22 sport=8080 dport=51432 [ASSURED] mark=0 use=1
Signification : Vous pouvez voir une connexion réelle NATée vers l’IP du conteneur. Cela confirme le chemin du trafic et les adresses source.
Décision : Utilisez‑cela pour valider si les requêtes proviennent des endroits attendus. Si les sources sont « surprenantes », corrigez l’exposition par la liaison de publication ou le pare‑feu.
Task 16: Identify which bridges exist and what subnets they correspond to
cr0x@server:~$ ip -br link | grep -E 'docker0|br-'
docker0 UP 0a:58:0a:f4:00:01 <BROADCAST,MULTICAST,UP,LOWER_UP>
br-acde1234 UP 02:42:8f:11:aa:01 <BROADCAST,MULTICAST,UP,LOWER_UP>
br_bf001122 UP 02:42:6a:77:bb:01 <BROADCAST,MULTICAST,UP,LOWER_UP>
cr0x@server:~$ ip -4 addr show br-acde1234 | sed -n '1,8p'
12: br-acde1234: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.21.0.1/16 brd 172.21.255.255 scope global br-acde1234
Signification : Chaque bridge défini par l’utilisateur correspond à un device bridge Linux avec une IP de passerelle sur l’hôte.
Décision : Si vous auditez l’exposition, ces IP de passerelle et ces sous‑réseaux importent pour le pare‑feu et pour comprendre comment le trafic sort du namespace du conteneur.
Playbook de diagnostic rapide
Quand quelque chose « fuit », « ne peut pas se connecter » ou « se connecte depuis le mauvais endroit », vous avez besoin d’une séquence rapide qui converge. Voici cette séquence.
Traitez‑la comme un runbook d’astreinte : premier/deuxième/troisième, pas de divagations.
Premier : Prouvez si le port est exposé sur l’hôte
- Exécutez
docker pset cherchez les mappages0.0.0.0:. - Exécutez
ss -lntpsur l’hôte et confirmez qu’un écouteur existe (docker‑proxy ou chemin NAT du noyau).
Si c’est publié : supposez que tous les réseaux que touche l’hôte peuvent y accéder jusqu’à preuve du contraire. Votre histoire « interne » est déjà suspecte.
Second : Identifiez les réseaux, IPs et la route par défaut du conteneur
docker inspect CONTAINERpour les réseaux et les IPs.nsenter ... ip routepour voir quel réseau est la passerelle par défaut.nsenter ... ss -lntppour voir si le service écoute sur 0.0.0.0 ou une IP spécifique.
Si ça écoute sur 0.0.0.0 : c’est joignable depuis tous les réseaux attachés sauf si quelque chose le bloque.
Troisième : Vérifiez la politique à l’endroit où vous pouvez l’appliquer de façon fiable
- Vérifiez
iptables -S DOCKER-USER(ou équivalent nft) pour des allow/deny explicites. - Confirmez que Docker gère iptables (
docker info). - Testez depuis un conteneur sur le « mauvais » réseau avec
ncoucurl.
Si DOCKER-USER est vide : vous n’avez pas de politique ; vous avez de l’espoir.
Quatrième : Si le comportement est incohérent, suspectez le DNS et les noms de service multi‑IP
- Exécutez
getent hosts servicedepuis les appelants sur différents réseaux. - Cherchez des réponses différentes menant à des résultats ACL différents.
Trois mini‑histoires d’entreprise du terrain
Mini‑histoire 1 : L’incident causé par une fausse hypothèse
Une entreprise de taille moyenne exécutait une API admin interne dans Docker. « Interne » signifiait « sur un réseau backend dans Compose », donc l’équipe se sentait en sécurité. Ils
avaient aussi publié le port sur l’hôte par commodité, parce que quelques scripts ops s’exécutaient depuis l’hôte et personne ne voulait rejoindre un réseau de conteneur juste pour appeler localhost.
Lors d’un changement réseau, l’hôte a été joint à un sous‑réseau d’entreprise plus large dans le cadre d’une consolidation VPN. Rien dans le fichier Compose n’a changé. Le port
était toujours publié sur 0.0.0.0. Soudain, un outil d’une autre équipe a découvert l’API lors d’un scan de routine. Ce n’était pas malveillant.
C’était juste le genre de scan curieux qui arrive dans les grands réseaux quand des gens tentent d’inventorier les ressources.
L’API admin nécessitait une authentification, mais elle avait aussi un endpoint de debug qui renvoyait des informations de version, des métadonnées de build et une carte des noms d’hôtes internes.
C’était suffisant pour faciliter une campagne d’ingénierie sociale plus tard. L’incident initial n’était pas un vol de données ; c’était une divulgation accidentelle qui
a élargi le rayon d’impact des erreurs futures.
La cause racine du postmortem était ennuyeuse : l’équipe assimilait « pas sur le réseau frontend » à « pas joignable ». Ils n’ont jamais modélisé l’hôte comme un système en réseau.
La publication est une préoccupation de l’hôte, pas du conteneur. La correction a été tout aussi ennuyeuse : ne publier que sur 127.0.0.1 et placer un proxy
authentifié devant, plus des règles DOCKER-USER pour restreindre les ports publiés restants par sous‑réseau source.
Mini‑histoire 2 : L’optimisation qui s’est retournée contre eux
Une autre équipe avait des problèmes de performance avec un service parlant à une base de données via un bridge Docker. Quelqu’un a proposé macvlan pour un « réseau proche du natif »
et une latence moindre. Ils ont créé un réseau macvlan attaché au VLAN de production, donné aux conteneurs des IP à part entière, et ont célébré un petit gain de latence.
Tout le monde aime gratter des millisecondes. C’est la version adulte de collectionner des autocollants.
Puis le retour de bâton : le service était toujours attaché à un bridge interne pour la découverte de services et pour atteindre un sidecar qui n’existait que là. Maintenant le
conteneur avait deux identités réseau : une sur le VLAN prod, une sur le bridge interne. Le service s’est lié à 0.0.0.0 comme d’habitude. Il écoutait donc sur les deux.
Les ACLs de la base de données supposaient que seules certaines plages pouvaient s’y connecter, mais le service pouvait maintenant initier des connexions depuis une nouvelle plage d’IP source.
Du trafic a commencé à emprunter un chemin différent parce que la route par défaut était désormais via macvlan. L’observabilité est devenue rapidement étrange.
Le vrai problème n’était pas macvlan. C’était l’hypothèse que « ajouter un réseau plus rapide » ne change pas l’exposition ni l’identité. Si, ça change. Cela modifie les IP sources,
le routage et les interfaces qui reçoivent le trafic. L’incident qu’ils ont eu n’était pas un « hack », c’était des « échecs d’authentification mystérieux et un comportement de pare‑feu incohérent »,
qui est la manière dont beaucoup de problèmes de sécurité se manifestent avant de devenir des incidents.
La correction finale : arrêter le multi‑homing de ce service. Ils ont déplacé la fonctionnalité du sidecar sur le VLAN prod aussi, ou alternativement déplacé l’app sur un seul
réseau bridge en acceptant le coût de performance. La deuxième correction a été de gouvernance : les nouveaux réseaux nécessitaient une courte revue incluant « sur quelles interfaces
le service va‑t‑il se lier, et quelle sera la route par défaut après le changement ? »
Mini‑histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une plateforme de services financiers avait une règle : chaque hôte avait une politique DOCKER-USER de base, et chaque port publié devait être justifié avec un périmètre source.
Ce n’était pas sexy. C’était une case à cocher dans l’infrastructure‑as‑code. Les ingénieurs se plaignaient, discrètement, comme les ingénieurs se plaignent quand on les empêche
de faire quelque chose d’imprudent en urgence.
Un vendredi, une équipe a déployé un conteneur de dépannage avec une interface web rapide. Quelqu’un a ajouté -p 0.0.0.0:9000:9000 parce qu’il devait y accéder depuis son
laptop. Ils ont oublié de l’enlever. Le conteneur a aussi été attaché à un second réseau pour atteindre des services internes. C’était la recette exacte pour une exposition accidentelle.
Mais la politique de base bloquait l’inbound vers les ports publiés à moins que la source ne provienne d’un petit sous‑réseau jump‑host approuvé. L’interface web était donc joignable
depuis là où elle devait l’être, et pas depuis partout ailleurs. La semaine suivante, un scan d’audit a vu le port ouvert sur l’hôte mais n’a pas pu atteindre le service depuis le réseau général.
Le ticket de sécurité a été ennuyeux, mais ce n’était pas un incident.
La leçon n’était pas « les audits sont utiles ». La leçon était que des valeurs par défaut ennuyeuses battent des nettoyages héroïques. Quand votre posture de sécurité dépend du souvenir
de retirer des flags temporaires, votre posture est « éventuellement compromise ». L’équipe a conservé la politique de base et a ajouté un garde‑fou en CI pour signaler les publications
0.0.0.0 dans les changements Compose pour revue.
Schémas de durcissement qui fonctionnent vraiment
1) Minimiser le multi‑homing. Préférer un réseau par service.
Les conteneurs multi‑réseaux sont parfois nécessaires. Ils sont aussi un multiplicateur de complexité. Si votre service doit parler à deux choses différentes, demandez‑vous :
l’une de celles‑ci peut‑elle être atteinte via un proxy sur un seul réseau ? Pouvez‑vous scinder le service en deux composants — un par réseau — avec une API étroite et auditable entre eux ?
Si vous devez multi‑homer : traitez le conteneur comme un objet adjacent à un routeur. Liezt explicitement, journalisez les IP sources et pare‑feutez.
2) Publier les ports sur des IP hôtes spécifiques, pas sur 0.0.0.0
Le comportement par défaut de publication est une commodité développeur. En production, soyez explicite :
-p 127.0.0.1:...quand le service est seulement pour l’accès local derrière un reverse proxy.-p 10.10.5.10:...quand le service doit se lier à une interface/VIP spécifique.
Ce simple choix élimine des catégories entières d’incidents « joignable depuis le VPN maintenant ».
3) Mettez l’application des règles dans DOCKER-USER (pare‑feu hôte), pas dans la mémoire humaine
Docker recommande DOCKER-USER comme endroit stable pour la politique personnalisée parce qu’elle est évaluée avant les propres règles d’autorisation de Docker.
Une politique de base pourrait ressembler à : allow established, allow des sources spécifiques vers des ports spécifiques, drop le reste.
Adaptez‑la par environnement ; ne faites pas de cargo‑cult.
4) Les réseaux « internal » servent au contrôle d’egress, pas à la segmentation entrante
Utilisez --internal pour empêcher des egress Internet accidentels depuis des couches sensibles. Mais ne l’envisagez pas comme une frontière entrante. Si un conteneur est attaché,
il peut communiquer. Votre frontière repose sur l’appartenance au réseau plus la politique de pare‑feu plus la liaison de l’application.
5) Liez les services à l’interface/IP prévue à l’intérieur du conteneur
Si un service ne doit être joignable que depuis le réseau frontend, liez‑le à l’IP ou à l’interface de ce réseau. C’est sous‑estimé parce que c’est de la « configuration applicative »,
pas de la « configuration Docker », mais c’est l’une des mesures les plus efficaces que vous puissiez prendre.
6) Traitez macvlan/ipvlan comme si vous mettiez des conteneurs directement sur le LAN (parce que c’est le cas)
Avec macvlan, un conteneur devient un pair sur le réseau physique avec sa propre MAC et IP. C’est puissant. C’est aussi un moyen de contourner les hypothèses confortables que les gens
faisaient quand tout vivait derrière docker0.
7) Journalisez et alertez sur les attachements réseau et les publications de ports
L’exposition arrive typiquement par des « petits changements ». Surveillez donc :
- Nouveaux ports publiés
- Conteneurs attachés à des réseaux supplémentaires
- Nouveaux réseaux créés avec des drivers comme macvlan
Blague n°2 : Si votre modèle de sécurité dépend de « personne ne lancera jamais -p 0.0.0.0 », j’ai un réseau bridge à vous vendre.
Docker Compose et multi‑réseaux : habitudes sûres par défaut
Compose facilite les attachements multi‑réseaux. C’est bien, jusqu’à ce que ce soit trop simple. Voici des habitudes qui vous évitent des ennuis.
Soyez explicite sur les réseaux et leur objectif
Nommez les réseaux par fonction, pas par équipe. frontend, service-mesh, db, mgmt valent mieux que net1 et shared.
Si vous ne pouvez pas le nommer, vous ne pourrez probablement pas le défendre.
Privilégiez « le proxy publie, les apps non »
Un schéma courant : seul le reverse proxy publie des ports sur l’hôte. Tout le reste reste sur des réseaux internes. Cela réduit le nombre de ports publiés à auditer.
Ce n’est pas parfait, mais c’est une vraie réduction de la surface d’exposition.
Utilisez des services séparés plutôt qu’un service avec deux réseaux quand c’est possible
Si vous connectez un service à la fois à frontend et à db, il devient le pont entre zones. Parfois c’est correct. Souvent c’est de la paresse.
Préférez une application single‑homée et un client DB single‑homé ou un sidecar si vous avez besoin de fonctionnalité inter‑zones.
Contrôlez les liaisons de publication dans Compose
Les ports Compose supportent les liaisons IP. Utilisez‑les. Si vous ne spécifiez pas l’IP hôte, vous demandez « toutes les interfaces ».
Erreurs fréquentes : symptôme → cause racine → correction
1) Symptôme : « Le service est interne, mais quelqu’un l’a atteint depuis le VPN »
Cause racine : Port publié sur 0.0.0.0 sur un hôte joignable via VPN/routage d’entreprise.
Correction : Liez la publication sur 127.0.0.1 ou sur une IP d’interface spécifique ; ajoutez des règles DOCKER-USER limitant les sources ; placez un proxy authentifié en frontal.
2) Symptôme : « Des conteneurs backend peuvent atteindre un endpoint admin qu’ils ne devraient pas »
Cause racine : Conteneur multi‑homé écoutant sur 0.0.0.0 à l’intérieur, exposant le service sur tous les réseaux attachés.
Correction : Liez l’app à l’interface/IP prévue ; détachez‑la des réseaux inutiles ; ajoutez une politique réseau via le pare‑feu hôte si approprié.
3) Symptôme : « Timeouts intermittents entre services »
Cause racine : Le DNS résout le même nom de service vers des IP différentes selon le réseau de l’appelant ; certains chemins sont bloqués ou asymétriques.
Correction : Réduisez les attachements multi‑réseaux ; utilisez des noms d’hôtes explicites par tier réseau ; vérifiez avec getent hosts depuis chaque appelant.
4) Symptôme : « Le trafic sortant apparaît depuis une nouvelle plage d’IP source »
Cause racine : La route par défaut dans le conteneur a changé à cause de l’ordre d’attachement des réseaux ; l’egress utilise maintenant une interface différente (macvlan, nouveau bridge).
Correction : Contrôlez l’ordre d’attachement et minimisez les réseaux ; forcez l’egress via un proxy ; si nécessaire, implémentez du routage politique avec précaution et testez à chaque redémarrage.
5) Symptôme : « Les ports publiés ont cessé de fonctionner après le durcissement de l’hôte »
Cause racine : L’outil de pare‑feu de l’hôte a réordonné ou vidé les chaînes gérées par Docker ; la politique FORWARD a changé et a cassé le forwarding NAT.
Correction : Alignez la gestion du pare‑feu hôte avec Docker (utilisez DOCKER-USER pour la politique) ; assurez‑vous que le forwarding est autorisé ; validez les règles après les mises à jour des agents.
6) Symptôme : « Conteneur joignable depuis des réseaux qui ne devraient pas voir les sous‑réseaux Docker »
Cause racine : Le routage en amont/peering atteint maintenant des plages RFC1918 utilisées par les bridges Docker ; « privé » n’est pas « isolé ».
Correction : Choisissez des sous‑réseaux Docker non chevauchants avec les plages d’entreprise/VPN/cloud ; restreignez l’inbound au périmètre réseau et sur l’hôte ; évitez d’annoncer les sous‑réseaux Docker en amont.
7) Symptôme : « Le réseau interne autorise toujours l’accès entrant »
Cause racine : Méprise : --internal bloque le routage externe, pas l’accès par des conteneurs attachés ; les ports publiés sont séparés.
Correction : Combinez --internal avec une adhésion stricte au réseau et des règles de pare‑feu ; ne publiez pas de services destinés à être internes seulement.
Checklists / plan étape par étape
Checklist A: Before you attach a container to a second network
- Listez les écouteurs actuels dans le conteneur (
ss -lntp). S’il se lie à 0.0.0.0, supposez qu’il sera joignable sur le nouveau réseau. - Décidez quel réseau doit être la route par défaut (
ip route) et quelle identité d’egress vous voulez. - Confirmez les attentes DNS : les clients résoudront‑ils le nom du service vers la bonne IP sur chaque réseau ?
- Documentez la raison du multi‑homing dans le dépôt à côté du fichier Compose. Si ce n’est pas écrit, ce n’est pas réel.
Checklist B: When exposing a service via -p / Compose ports
- Ne publiez jamais sans une IP hôte explicite en production sauf si vous entendez vraiment « toutes les interfaces ».
- Décidez des plages sources autorisées et implémentez‑les dans DOCKER-USER.
- Validez avec
ss -lntpet un test de connexion depuis une source autorisée et une source non autorisée. - Journalisez les requêtes avec les IP sources ; vous en aurez besoin plus tard.
Checklist C: Baseline host controls that prevent “oops” exposure
- Créez une politique DOCKER-USER de base qui par défaut refuse les ports publiés sauf aux sources approuvées.
- Alertez sur les nouveaux ports publiés et sur les nouveaux attachements réseau (au moins en revue de changement).
- Prévenez les chevauchements de sous‑réseaux : choisissez des sous‑réseaux Docker qui ne rentreront pas en collision avec les plages d’entreprise/VPN/cloud.
- Standardisez sur un gestionnaire de pare‑feu (iptables‑nft vs nft) et testez le comportement Docker après les mises à jour OS.
Step-by-step remediation plan for a discovered accidental exposure
- Confirmez l’exposition :
docker ps,sssur l’hôte, et un test distant depuis le réseau suspecté. - Confinement immédiat : retirez la publication ou reliez‑la sur
127.0.0.1. - Ajoutez des règles restrictives DOCKER-USER si le port doit rester publié.
- Corrigez la cause racine : retirez les réseaux inutiles, liez le service à la bonne interface, et ajoutez des contrôles de régression en CI/revue.
- Validation post‑changement : testez depuis chaque zone réseau et confirmez que la résolution DNS et les routes se comportent comme prévu.
FAQ
1) Si mon conteneur est sur un réseau Docker « internal », est‑il sûr ?
Plus sûr pour l’egress, pas automatiquement sûr pour l’inbound. Tout conteneur sur ce réseau peut toujours l’atteindre. Et les ports publiés sur l’hôte ignorent « internal ».
2) Pourquoi -p 8080:8080 expose‑t‑il plus d’endroits que prévu ?
Parce que cela se lie à toutes les interfaces de l’hôte par défaut. Votre hôte est connecté à plus de réseaux que vous ne vous en souvenez, surtout avec les VPN et le routage cloud.
3) Puis‑je compter sur la séparation réseau Docker comme frontière de sécurité ?
Considérez‑la comme une couche, pas la couche. Les vraies frontières nécessitent une politique explicite (règles DOCKER-USER ou équivalent), des attachements réseau minimaux, et des liaisons applicatives correctes.
4) Comment empêcher un service multi‑réseaux d’écouter sur le « mauvais » réseau ?
Configurez l’application pour qu’elle se lie à l’IP/interface spécifique du conteneur. Si ce n’est pas possible, bloquez l’inbound indésirable au pare‑feu hôte ou repensez l’architecture pour éviter le multi‑homing.
5) Pourquoi le même nom de service résout‑il vers des IP différentes ?
Le DNS embarqué de Docker retourne des réponses scoppées à la requête réseau de l’appelant. C’est pratique pour la découverte de services et une cause fréquente de connectivité confuse.
6) Le macvlan est‑il intrinsèquement dangereux ?
Non. Il est simplement honnête. Il place les conteneurs sur le réseau réel, ce qui signifie que votre posture de sécurité réseau doit aussi être réelle : VLANs, ACLs et audit.
7) Où dois‑je placer les règles de pare‑feu pour que Docker ne les écrase pas ?
Utilisez la chaîne DOCKER-USER pour les configurations basées sur iptables. Elle est conçue pour votre politique et est évaluée avant les règles de Docker.
8) Quelle est la manière la plus rapide de prouver une exposition accidentelle ?
Vérifiez docker ps pour 0.0.0.0:PORT, confirmez avec ss -lntp sur l’hôte, puis testez depuis un autre segment réseau (ou depuis un conteneur sur un réseau différent).
9) Docker rootless change‑t‑il le risque d’exposition ?
Cela change la plomberie et quelques valeurs par défaut, mais cela n’enlève pas le besoin de contrôler les ports publiés et les liaisons multi‑réseaux. Vous avez toujours besoin d’un modèle et d’une politique.
Conclusion : prochaines étapes pratiques
Si vous exécutez des conteneurs multi‑réseaux, acceptez que vous faites du vrai réseau. Ensuite agissez en conséquence.
- Auditez les ports publiés et éliminez les liaisons
0.0.0.0lorsqu’elles ne sont pas strictement nécessaires. - Pour tout conteneur multi‑homé, vérifiez : interfaces, routes et adresses d’écoute. Corrigez les liaisons
0.0.0.0qui n’ont pas lieu d’être. - Mettez en place une politique DOCKER-USER de base et intégrez‑la dans le provisionnement des hôtes, pas dans un rituel tribal.
- Réduisez le multi‑homing par conception : un réseau par service sauf si vous pouvez justifier le rayon d’impact.
- Faites des attachements réseau et des publications de ports des changements soumis à revue, pas des improvisations du vendredi soir.
Vous n’avez pas besoin d’une sécurité parfaite pour arrêter les expositions accidentelles. Vous avez besoin de liaisons explicites, d’une politique explicite et de moins de surprises. C’est simplement de bonnes pratiques d’exploitation.