Problema
En entornos de homelab o pequeñas infraestructuras es frecuente lanzar varios servicios en contenedores Docker y conectar todos ellos a la red predeterminada bridge. Esa configuración permite que cualquier contenedor alcance a otro usando su dirección IP y puerto expuesto, lo que rompe la premisa de “aislamiento por diseño”. Cuando varios servicios comparten la misma subred, un atacante que comprometa un contenedor puede explorar libremente los demás, y usuarios de la LAN pueden saltarse reglas de VLAN porque el tráfico nunca sale del host. El objetivo típico es que solo el reverse proxy sea la puerta de entrada y que el resto de contenedores queden invisibles para la red local y entre sí.
Causa
El comportamiento descrito proviene de tres factores habituales:
- Red predeterminada
bridgesin segmentación – Docker asigna automáticamente una subred privada y conecta todos los contenedores a ella. No hay separación lógica entre aplicaciones distintas. - Puertos publicados en el host – Cada vez que se usa
ports: "HOST:CONTAINER"Docker abre una regla de NAT eniptablesque permite el acceso directo desde cualquier host en la LAN. - Falta de políticas de firewall a nivel de contenedor – UFW o firewalld operan sobre la interfaz del host (
docker0), pero la dirección IP del contenedor puede cambiar, lo que invalida reglas estáticas.
Estos puntos hacen que la seguridad dependa de la configuración manual de puertos y de la constancia de las IPs, algo frágil en entornos donde los contenedores se recrean con frecuencia.
Solución
La estrategia más robusta combina redes Docker aisladas, exposición únicamente a través del reverse proxy y políticas de firewall basadas en etiquetas de red. El flujo típico es:
- Crear una red interna para cada grupo de servicios (por ejemplo
frontend,backend,db). Cada red es privada; los contenedores solo pueden comunicarse con los que comparten la misma red. - Colocar el reverse proxy (NGINX, Traefik, Caddy…) en una red “exposed” que sí tenga puertos publicados al host. El proxy también se conecta a las redes internas de los servicios que debe enrutar.
- Eliminar la directiva
portsde los servicios que no deben ser accesibles directamente. En su lugar, usandepends_ony la referencia de nombre de contenedor (service_name:port) dentro de la red interna. - Aplicar reglas de firewall que bloqueen todo el tráfico entrante a
docker0y permitan solo la interfaz del proxy. Con UFW se puede usar la etiqueta de red (docker0) o, de forma más precisa, reglas deiptablesque acepten tráfico desde la IP del contenedor proxy y lo descarten para el resto. - Opcional: usar
docker network inspecty etiquetas para generar reglas dinámicas con scripts que actualiceniptablescada vez que Docker crea o elimina una red.
Paso a paso
1. Definir redes en docker‑compose.yml
networks:
frontend:
backend:
db:
exposed:
driver: bridge
ipam:
config:
- subnet: 172.25.0.0/24 # red que será publicada al host
2. Configurar el reverse proxy
services:
reverse-proxy:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
networks:
- exposed
- frontend
- backend
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
3. Conectar cada aplicación a su red interna
app-web:
image: my/webapp
networks:
- frontend
# sin `ports`, solo accesible vía proxy
app-api:
image: my/api
networks:
- backend
# sin `ports`
db:
image: postgres:15
networks:
- db
environment:
POSTGRES_PASSWORD: secret
4. Bloquear acceso directo con UFW (o iptables)
# Permitir solo al proxy (IP estática dentro de la red `exposed`)
PROXY_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -qf "name=reverse-proxy"))
sudo ufw deny in on docker0 to any
sudo ufw allow in on docker0 from $PROXY_IP to any
Si la IP del proxy cambia, un pequeño cron o systemd‑timer que vuelva a ejecutar el fragmento mantiene la regla actualizada.
5. Validar que los puertos no estén publicados
docker compose ps | grep -i "0.0.0.0"
No debería aparecer ninguna entrada para los servicios internos.
Cuándo aplicar esta solución
- Entornos multi‑servicio donde varios contenedores deben comunicarse entre sí pero no con la LAN.
- Homelabs o servidores de pruebas que usan una única máquina física y desean evitar “cross‑talk” accidental.
- Escenarios de LAN party, eventos o workshops donde varios usuarios comparten la misma red y no se confía en la segmentación de VLAN del router.
- No es necesario cuando cada contenedor ya está aislado por máquinas virtuales o cuando se usa un orquestador con políticas de red avanzadas (Kubernetes NetworkPolicies).
No se recomienda en entornos donde los contenedores deben ser accesibles directamente desde la red externa (por ejemplo, bases de datos expuestas a clientes externos) sin un proxy dedicado.
Código
# 1. Crear redes aisladas
docker network create frontend
docker network create backend
docker network create db
docker network create --subnet 172.25.0.0/24 exposed
# 2. Lanzar compose (asume archivo docker-compose.yml preparado)
docker compose up -d
# 3. Configurar UFW dinámico
PROXY_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -qf "name=reverse-proxy"))
sudo ufw deny in on docker0 to any
sudo ufw allow in on docker0 from $PROXY_IP to any
Verificación
-
Comprobar que solo el proxy escucha en el host
sudo ss -tlnp | grep -E '80|443'La salida debe mostrar
nginx(o el contenedor del proxy) y nada más. -
Intentar acceso directo a un contenedor interno
Desde otro host de la LAN, ejecuta:curl http://<IP_DEL_HOST>:8080 # puerto que habría usado app-webLa petición debe fallar con “Connection refused”.
-
Acceder vía proxy
curl http://<IP_DEL_HOST>/web # ruta configurada en NGINXDebería devolver la respuesta del contenedor
app-web. -
Revisar reglas de firewall
sudo ufw status numbered | grep docker0Debe aparecer una regla “ALLOW IN” solo desde la IP del proxy y una regla “DENY IN” para el resto.
Notas adicionales
- IP estática para el proxy: asignar una IP fija dentro de la red
exposedevita que el script de UFW tenga que buscarla cada vez. Se hace con la opciónipamen la definición de la red. - Traefik como alternativa: Traefik detecta automáticamente contenedores en redes Docker y configura rutas sin necesidad de archivos de configuración estáticos.
- Persistencia de reglas: UFW guarda las reglas en
/etc/ufw/user.rules. Si prefieresiptablespuro, guarda el script en/etc/rc.localo usaiptables‑persistent. - Monitorización:
docker network lsydocker network inspectson útiles para auditar que no haya contenedores conectados a la redexposedsin pasar por el proxy. - Escalabilidad: para cientos de servicios, agrupa redes por dominio de negocio (frontend, api, jobs) y conecta el proxy a todas ellas. El número de reglas de firewall sigue siendo constante porque solo se controla la interfaz del proxy.