Skip to main content

Gestion du réseau et des ports Docker

Cette page est dédier à la question suivante :

Comment bien configuré ses conteneurs sur le réseau ?

Portée : Docker Compose, hosts Linux avec UFW comme pare-feu, reverse proxy type Traefik/Caddy/nginx.
Objectif : standardiser une architecture réseau 100 % maîtrisée sur toutes les stacks compose.

1. Comportement par défaut de Docker

1.1 Réseaux créés automatiquement

Par défaut, docker compose up crée un réseau bridge dédié par projet (nommé <nom_projet>_default), distinct du bridge docker0 historique. 
Tous les services d'un même fichier compose sont attachés à ce réseau et se résolvent entre eux par nom de service (DNS interne Docker), sans avoir besoin de publier le moindre port.

➡️ Conséquence clé : la communication inter-conteneurs ne nécessite jamais ports:. 
ports: ne sert qu'à exposer un service en dehors du host Docker (vers le LAN/WAN).

1.2 EXPOSE vs ports:
Directive Effet réel Portée
EXPOSE (Dockerfile) Documentation uniquement, aucun binding réseau réel N/A
expose: (compose) Idem, informatif N/A
ports: (compose) Publie réellement le port sur une interface du host via NAT Host + extérieur
1.3 Docker et iptables : le piège UFW

C'est le point le plus critique en environnement Ops/DevSec.

Quand un conteneur publie un port avec ports:, Docker insère automatiquement des règles dans les chaînes DOCKER et DOCKER-USER de la table nat/filter d'iptables — en amont de la chaîne INPUT gérée par UFW.

Conséquence : un port publié via ports: est accessible depuis l'extérieur même si UFW le bloque explicitement. 
UFW ne voit jamais passer ce trafic car Docker route directement via NAT/FORWARD.

Trafic entrant → PREROUTING (Docker DNAT) → FORWARD → DOCKER-USER → DOCKER → conteneur
                                                ↑
                                    UFW (INPUT) n'intercepte PAS ce chemin


2. Le comportement de ports: en détail

2.1 Syntaxes
ports:
  - "8080:80"            # host:container — bind 0.0.0.0 (TOUTES interfaces) par défaut, port ouvert sur iptables
  - "127.0.0.1:8080:80"  # bind explicite localhost uniquement
  - "10.10.10.1:8080:80" # bind sur une IP précise (ex: interface WireGuard)
  - "8080:80/udp"        # protocole explicite, port ouvert sur iptables
2.2 Le piège du binding implicite

"8080:80" publie sur 0.0.0.0:8080, donc sur toutes les interfaces réseau du host (LAN, WAN, VPN...), pas seulement en local.

C'est la cause n°1 des services/ports exposées par erreur sur Internet.


3. Bonnes pratiques Ops / DevSec

  • Principe de moindre exposition : ne publier (ports:) que ce qui doit être joignable depuis l'extérieur du host. Tout le reste communique via le réseau interne Docker (DNS de service).
  • Un réseau dédié par stack, jamais le réseau default partagé pour plusieurs projets sans raison. Nommer explicitement :
networks:
    proxy-web:
      name: proxy-public # Destiné à exposé les services sur les ports 80/443
    backend:
      name: <stack>-internal # Destiné a rester en local, jamais exposé publiquement mais reste accessbles pour les autres services internes
      internal: true
Type d’exposition  Type de service
internal: true DB / cache /  workers / Clamav
proxy-public Traefik/ Caddy/ nginx
network_mode: host VPN

 

Éviter network_mode: host sauf nécessité technique réelle (ex: WireGuard qui a besoin d'accéder aux interfaces réseau du host). Cas contraire, host désactive toute l'isolation réseau Docker.

Convention de nommage des réseaux cohérente sur toutes les stacks (<stack>-internal, proxy-public, vpn-admin...) pour faciliter l'audit avec docker network ls.


4. Commandes utiles pour l'audit

4.1 Depuis Docker

# Ports publiés par tous les conteneurs actifs
docker ps --format "table {{.Names}}\t{{.Ports}}"

# Ports publiés par un conteneur précis
docker port <container>

# Lister les réseaux et repérer les réseaux "trop partagés"
docker network ls

# Détail d'un réseau : conteneurs attachés, subnet, internal ou non
docker network inspect <network>

# Vue globale via l'API compose
docker compose ps
4.2 Depuis le host (vérité terrain, indépendante de Docker)
# Tous les sockets en écoute, avec le PID/process
ss -tulpn

# Équivalent netstat
sudo netstat -tulpn

# Règles NAT injectées par Docker (bindings réels)
sudo iptables -t nat -L DOCKER -n --line-numbers

# Règles de filtrage custom (celles qui priment sur UFW)
sudo iptables -L DOCKER-USER -n --line-numbers

# Vérification externe depuis une autre machine (la vraie preuve)
nmap -Pn -p- <ip_du_host>

ss/iptables sur le host sont la seule vérité fiable : la lecture des docker-compose.yml seule ne garantit pas ce qui est réellement exposé (héritage de configs, overrides, etc.).