En el capítulo anterior montaste WireGuard. Ahora accedes a tu red local desde cualquier lugar sin exponer servicios de administración a internet. Buen avance. Pero hay un problema. Tus contenedores Docker siguen hablando entre sí como les da la gana. Todos en la misma red. Todos pueden verse. Todos pueden conectarse a todos. ¿Te parece seguro que un contenedor de WordPress pueda hablar directamente con Redis? ¿O que un contenedor de Nextcloud pueda hacer peticiones a la base de datos de otro servicio completamente distinto? La respuesta es no. Y si tu intuición te dice que no, tienes razón.
Docker por defecto pone todos tus contenedores en una misma red bridge. Eso significa que cualquier contenedor puede alcanzar a cualquier otro por su IP interna. No hay aislamiento. No hay segmentación. Es como poner a todos tus vecinos en el mismo cuarto sin puertas.
En este capítulo vas a aprender a diseñar redes Docker que aíslen tus servicios por función. Redes públicas para lo que necesita estar expuesto. Redes privadas para lo que no debe salir al exterior. Y redes de datos para lo que solo debe ser accesible por unos pocos.
El problema: todos los contenedores en la misma red
Cuando ejecutas docker-compose up sin definir redes, Docker crea una red por defecto para ese archivo. Todos los servicios del mismo docker-compose.yml se conectan a esa red y pueden comunicarse entre sí por el nombre del servicio.
Esto es cómodo. Muy cómodo. No tienes que pensar en redes. Cada contenedor encuentra a los demás por su nombre. Pero es un desastre de seguridad.
Imagina que tienes esta configuración:
services:
web:
image: nginx
ports:
- "80:80"
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secreta
redis:
image: redis:7
Los tres contenedores están en la misma red. WordPress puede hablar con PostgreSQL directamente. También puede hablar con Redis. ¿Para qué necesita WordPress acceso a Redis? Para nada. Pero lo tiene.
Ahora imagina que WordPress tiene una vulnerabilidad. Un atacante la explota y obtiene una shell dentro del contenedor. Desde ahí, hace un escaneo de la red interna:
nmap -sn 172.17.0.0/16
Descubre PostgreSQL en 172.17.0.3 y Redis en 172.17.0.4. Intenta conectarse a PostgreSQL con contraseñas por defecto. Si no cambiaste la contraseña, o si usaste una débil, el atacante tiene tu base de datos.
Todo porque todos los contenedores estaban en la misma red.
Este es el problema fundamental. Docker te da comodidad a costa de seguridad. Y en un entorno self-hosted, la comodidad no es excusa.
Redes Docker: tipos y usos
Docker tiene varios tipos de redes. Cada una sirve para un propósito distinto. Vamos a ver las que te interesan para segmentar tu servidor.
Bridge
La red bridge es la red por defecto de Docker. Crea un puente virtual en el host. Los contenedores conectados a un bridge pueden comunicarse entre sí y con el host. No pueden comunicarse con contenedores de otros bridges a menos que los conectes explícitamente.
Cuando ejecutas docker-compose up sin definir redes, Docker crea un bridge automático para ese proyecto. Cada proyecto tiene su propio bridge. Los contenedores del proyecto A no pueden hablar con los del proyecto B.
Esto ya es un avance. Pero dentro del mismo proyecto, todos los contenedores siguen viéndose.
Para segmentar bien, necesitas crear múltiples bridges en el mismo proyecto y asignar cada servicio a la red que le corresponda.
Overlay
La red overlay conecta contenedores en múltiples hosts Docker. Es la red que usas cuando tienes un cluster Docker Swarm. Los contenedores en diferentes máquinas se comunican como si estuvieran en la misma red local.
Para un servidor self-hosted típico con un solo nodo, no necesitas overlay. Bridge es suficiente.
Pero si tienes varios servidores y usas Docker Swarm, overlay te permite segmentar a través de múltiples máquinas. Es el mismo concepto, pero distribuido.
Macvlan
La red macvlan asigna una dirección MAC real a cada contenedor. El contenedor aparece en tu red local como un dispositivo físico más, con su propia IP.
Esto es útil cuando quieres que un contenedor sea directamente accesible en tu red local sin pasar por NAT. Por ejemplo, un servidor DHCP o un DNS.
Para la segmentación de servicios web, macvlan no es necesaria. Te conviene mantener los contenedores en redes bridge aisladas y solo exponer lo necesario a través de Traefik.
¿Cuál usas?
Para el 99% de los casos en un servidor self-hosted, usas redes bridge. Creas varias redes bridge personalizadas y conectas cada contenedor solo a las que necesita.
Overlay la usas si tienes Docker Swarm. Macvlan la usas en casos muy concretos donde el contenedor necesita ser un ciudadano de pleno derecho en tu red local.
El resto del capítulo se centra en redes bridge. Son las que realmente necesitas.
Diseño de redes: pública, privada y de datos
Para segmentar bien, necesitas tres tipos de redes.
Red pública (traefik_public)
Esta red conecta Traefik con los servicios que deben ser accesibles desde internet. Traefik está en esta red. Cada servicio que expones con Traefik también está en esta red.
Nada más debería estar en esta red. Ni bases de datos, ni Redis, ni servicios internos.
networks:
traefik_public:
external: true
La declaras como externa porque Traefik ya la creó cuando lo configuraste en el capítulo 5. Si no la creaste explícitamente, la creas ahora:
docker network create traefik_public
Red privada (internal_backend)
Esta red conecta los servicios que se comunican entre sí pero no deben ser accesibles desde fuera. Tu aplicación web y tu API están en esta red. Se hablan entre ellas aquí.
Traefik no está en esta red. Los contenedores de esta red solo son accesibles desde otros contenedores de la misma red, o desde contenedores que conectes explícitamente a ella.
networks:
internal_backend:
driver: bridge
internal: true
El parámetro internal: true es clave. Hace que la red no tenga acceso al exterior. Los contenedores en esta red solo pueden hablar entre sí. No tienen salida a internet ni al host.
¿Por qué querrías esto? Porque si un contenedor en esta red se ve comprometido, no puede hacer peticiones a internet. No puede contactar con un servidor de comando y control. No puede descargar malware. Está aislado.
Usa internal: true con cuidado. Si tu base de datos necesita hacer peticiones externas (por ejemplo, para réplicas o backups externos), no puedes ponerla en una red interna. Pero para la mayoría de los casos, es una capa de seguridad enorme.
Red de base de datos (db_network)
Esta red es la más restrictiva. Solo contiene las bases de datos y los servicios que necesitan acceder a ellas.
Ni Traefik, ni la aplicación web, ni nada más debería estar en esta red. Solo el servicio que necesita la base de datos y la base de datos misma.
networks:
db_network:
driver: bridge
internal: true
¿Por qué separar la base de datos de la red privada del backend? Porque quieres minimizar el alcance de un posible ataque.
Si un atacante compromete tu aplicación web, tiene acceso a la red privada del backend. Desde ahí puede escanear, moverse lateralmente, intentar explotar otros servicios. Si la base de datos está en su propia red, el atacante necesita un salto más para alcanzarla.
Es el principio de defensa en profundidad. Varias capas. Cada capa añade fricción al atacante.
El diseño completo
Visualízalo así:
internet
|
|
Traefik (red pública)
|
|
Aplicación web (red pública + red privada)
|
|
API / Backend (red privada + red de datos)
|
|
Base de datos (red de datos, aislada)
Cada servicio tiene acceso solo a lo que necesita. Nada más.
Traefik ve la aplicación web porque están en la red pública. La aplicación web ve la API porque están en la red privada. La API ve la base de datos porque están en la red de datos.
Pero Traefik no ve la API ni la base de datos. La aplicación web no ve la base de datos. La API no ve internet.
Cada uno en su capa. Cada capa con su red.
Conectar contenedores a múltiples redes
Un contenedor puede estar en varias redes a la vez. De hecho, ese es el truco para segmentar bien.
Tu aplicación web necesita estar en la red pública (para que Traefik le pase tráfico) y en la red privada (para hablar con la API). Pero no necesita estar en la red de datos.
Tu API necesita estar en la red privada (para recibir peticiones de la web) y en la red de datos (para consultar la base de datos). Pero no necesita estar en la red pública.
Tu base de datos solo necesita estar en la red de datos. Punto.
En Docker Compose, conectas un servicio a múltiples redes así:
services:
web:
image: nginx
networks:
- traefik_public
- internal_backend
api:
image: mi-api
networks:
- internal_backend
- db_network
db:
image: postgres:15
networks:
- db_network
networks:
traefik_public:
external: true
internal_backend:
driver: bridge
internal: true
db_network:
driver: bridge
internal: true
Fíjate en el patrón. Cada servicio solo declara las redes que necesita. No pongas db_network en la web. No pongas traefik_public en la API. Sé preciso.
Cada red adicional que conectas a un contenedor es una superficie de ataque potencial. Si el contenedor se ve comprometido, el atacante tiene acceso a todas las redes a las que ese contenedor está conectado. Menos redes, menos riesgo.
Exponer servicios solo en redes internas
Hay servicios que nunca, bajo ninguna circunstancia, deberían tener una interfaz accesible desde fuera.
Lista negra de servicios que no deben salir a internet:
- PostgreSQL, MySQL, MariaDB.
- Redis, Memcached.
- RabbitMQ, Kafka.
- Cualquier base de datos o cola de mensajes.
- APIs internas que solo consumen otros servicios.
- Paneles de administración de bases de datos (phpMyAdmin, Adminer).
- Servicios de monitorización internos.
Todos estos servicios deberían estar en redes internas, sin puertos expuestos en el host.
Mira este ejemplo de lo que NO debes hacer:
services:
db:
image: postgres:15
ports:
- "5432:5432" # ERROR: esto expone PostgreSQL a la red del host
Esa línea ports expone PostgreSQL en 0.0.0.0:5432. Cualquier dispositivo en tu red local puede conectarse. Si tu firewall tiene abierto ese puerto, cualquier persona en internet puede intentarlo.
Nunca expongas puertos de bases de datos. Jamás.
Si necesitas acceder a la base de datos desde tu máquina para hacer consultas o migraciones, hazlo a través de la VPN de WireGuard que configuraste en el capítulo anterior. O ejecuta un cliente dentro de la red Docker.
La forma correcta:
services:
db:
image: postgres:15
expose:
- "5432"
networks:
- db_network
expose documenta el puerto pero no lo abre al exterior. Solo los contenedores en la misma red Docker pueden alcanzarlo.
Traefik en la red pública con acceso a servicios internos
Traefik es el caso especial. Necesita estar en la red pública para recibir tráfico de internet. Pero también necesita poder reenviar ese tráfico a los servicios que gestiona.
La cuestión es cómo conectar Traefik a los servicios sin exponerlos.
La respuesta: Traefik solo necesita estar en la red pública. Los servicios que Traefik gestiona también deben estar en la red pública (o al menos en una red donde Traefik pueda alcanzarlos).
Pero espera. Dijiste que la API no debe estar en la red pública. ¿Cómo hace Traefik para reenviar tráfico a la API si no están en la misma red?
Aquí tienes dos enfoques.
Enfoque 1: proxy inverso en la red pública, servicios en su red
Traefik está en la red pública. Tu aplicación web está en la red pública y en la red privada. Traefik habla con la aplicación web a través de la red pública. La aplicación web habla con la API a través de la red privada.
Traefik (pública) --> Web (pública + privada) --> API (privada + datos) --> DB (datos)
Este es el diseño más limpio. Traefik solo ve la web. La web ve la API pero no ve la DB. La API ve la DB. Cada uno ve solo lo que necesita.
Traefik no necesita acceso directo a la API ni a la base de datos. Traefik solo necesita reenviar tráfico HTTP a la web. La web se encarga del resto.
Enfoque 2: Traefik como proxy con múltiples redes
A veces necesitas que Traefik enrute directamente a servicios que no están diseñados para ser expuestos por separado. Por ejemplo, si tienes un servicio que no tiene su propio frontend y necesitas que Traefik le pase tráfico directamente.
En ese caso, Traefik necesita estar en la red de ese servicio. Pero sigues sin querer exponer ese servicio a internet directamente.
La solución: Traefik está en la red pública y también en las redes de los servicios a los que necesita enrutar. Pero los servicios no están en la red pública.
services:
traefik:
image: traefik:v3
networks:
- traefik_public
api:
image: mi-api
networks:
- internal_backend
admin-panel:
image: admin-panel
networks:
- internal_backend
networks:
traefik_public:
external: true
internal_backend:
driver: bridge
internal: true
En este diseño, Traefik no puede alcanzar la API ni el panel de administración porque están en otra red. Necesitas conectar Traefik también a internal_backend.
services:
traefik:
image: traefik:v3
networks:
- traefik_public
- internal_backend
Ahora Traefik puede enrutar tráfico a la API y al panel de administración. Pero la API y el panel siguen sin estar en la red pública. No son accesibles desde internet directamente. Solo a través de Traefik.
¿Y la base de datos? Sigue en su propia red, inalcanzable para Traefik.
Este enfoque es menos restrictivo que el primero porque Traefik tiene acceso a la red privada. Pero sigue siendo seguro porque Traefik es el único punto de entrada y está protegido con middlewares de seguridad.
Mi recomendación: usa el enfoque 1 siempre que puedas. El enfoque 2 solo cuando necesites enrutar directamente a servicios internos sin frontend propio.
Aislar bases de datos: sin interfaz externa
Las bases de datos son el tesoro de tu servidor. Ahí están los datos de tus usuarios, tus contraseñas (hasheadas, espero), tu contenido, tu configuración.
Una base de datos expuesta es una catástrofe esperando ocurrir.
Reglas de oro para bases de datos en Docker
Primera regla: nunca expongas puertos de base de datos con ports. Usa siempre expose para documentar el puerto interno.
Segunda regla: cada base de datos va en su propia red aislada. No compartas la red de base de datos entre servicios que no necesiten esa base de datos en concreto.
Tercera regla: no pongas la base de datos en la misma red que Traefik ni que servicios públicos.
Cuarta regla: usa contraseñas fuertes incluso en redes internas. Una red aislada no es excusa para usar password: password.
Quinta regla: si puedes, usa internal: true en la red de la base de datos. Así los contenedores de esa red no tienen acceso a internet.
Ejemplo de base de datos aislada
services:
app:
image: mi-app
networks:
- traefik_public
- internal_backend
api:
image: mi-api
networks:
- internal_backend
- db_network
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_DB: miapp
POSTGRES_USER: miapp
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
expose:
- "5432"
networks:
- db_network
restart: unless-stopped
networks:
traefik_public:
external: true
internal_backend:
driver: bridge
internal: true
db_network:
driver: bridge
internal: true
volumes:
pgdata:
Fíjate en los detalles:
dbsolo tiene la reddb_network. No tiene ninguna otra.dbusaexpose: 5432, noports: 5432:5432.db_networktieneinternal: true. La base de datos no tiene salida a internet.- La contraseña viene de una variable de entorno
${DB_PASSWORD}, no está hardcodeada. - Solo
apiestá endb_network. Laappno puede hablar directamente con la base de datos.
Si un atacante compromete la app, puede hablar con la api a través de internal_backend. Pero no puede alcanzar la base de datos porque no está en db_network. Necesita comprometer también la api para llegar a la base de datos.
Eso es defensa en profundidad.
Seguridad por defecto: no exponer puertos innecesarios
Hay una tendencia a añadir ports a todos los servicios en Docker Compose. «Por si acaso». «Para hacer pruebas». «Por si necesito acceder directamente».
Cada puerto expuesto es una puerta abierta. Cada puerto que no necesitas, ciérralo.
¿Puerto expuesto o no?
Pregúntate: ¿necesito acceder a este servicio desde fuera de Docker?
Si la respuesta es no, no expongas el puerto.
¿Quién necesita acceder a este servicio?
- Si solo lo necesita otro contenedor, no expongas el puerto. La red Docker es suficiente.
- Si lo necesitas tú desde tu máquina, usa VPN o un túnel SSH. No expongas el puerto.
- Si lo necesita toda la humanidad, usa Traefik con autenticación. Y aún así, valora si realmente debe ser público.
Alternativas a exponer puertos
Necesitas hacer una consulta a PostgreSQL desde tu máquina. No expongas el puerto 5432. En su lugar:
Opción A: conéctate a la VPN (WireGuard) y ejecuta psql desde un contenedor temporal en la misma red:
docker run -it --rm --network db_network postgres:15 psql -h db -U miapp
Opción B: usa un túnel SSH si no tienes VPN:
ssh -L 5432:db:5432 usuario@servidor
Opción C: instala un cliente de base de datos en el propio servidor y conéctate a través del socket Docker.
Ninguna de estas opciones expone la base de datos a internet. Todas son seguras si las usas correctamente.
La regla de oro
No pongas ports en un servicio a menos que sepas exactamente por qué lo necesitas y hayas evaluado el riesgo.
Para servicios de base de datos, la regla es absoluta: nunca uses ports.
Para servicios de aplicación, usa ports solo si el servicio no va a través de Traefik. Y si va a través de Traefik, no necesitas ports. Traefik se conecta a través de la red Docker interna.
# MAL: expones el servicio directamente y además usas Traefik
services:
web:
image: nginx
ports:
- "8080:80" # No necesitas esto si usas Traefik
networks:
- traefik_public
# BIEN: solo red Docker, Traefik hace de puerta de entrada
services:
web:
image: nginx
expose:
- "80"
networks:
- traefik_public
La diferencia es sutil pero importante. Con ports, el servicio es accesible en localhost:8080 y en la IP del servidor. Con expose, solo es accesible a través de la red Docker. Traefik lo alcanza por la red Docker, pero nadie más.
Ejemplo práctico: stack de aplicación web con base de datos aislada
Vamos a montar un ejemplo completo. Una aplicación web típica con frontend, API, Redis para caché y PostgreSQL como base de datos.
Cada servicio en su red. Cada red con su propósito.
# docker-compose.yml
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik_public
ports:
- "80:80"
- "443:443"
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/traefik.yml:ro
- ./traefik/config:/config:ro
- ./traefik/certs:/certs
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.tudominio.com`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=auth@file"
frontend:
image: nginx:alpine
container_name: frontend
restart: unless-stopped
expose:
- "80"
networks:
- traefik_public
- frontend_backend
volumes:
- ./frontend/build:/usr/share/nginx/html:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.frontend.rule=Host(`app.tudominio.com`)"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.routers.frontend.tls=true"
- "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
- "traefik.http.services.frontend.loadbalancer.server.port=80"
depends_on:
- api
api:
build: ./api
container_name: api
restart: unless-stopped
expose:
- "3000"
networks:
- frontend_backend
- api_cache
- api_db
environment:
DATABASE_URL: postgres://miapp:${DB_PASSWORD}@db:5432/miapp
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
expose:
- "6379"
networks:
- api_cache
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
db:
image: postgres:15-alpine
container_name: db
restart: unless-stopped
expose:
- "5432"
networks:
- api_db
environment:
POSTGRES_DB: miapp
POSTGRES_USER: miapp
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U miapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
traefik_public:
external: true
frontend_backend:
driver: bridge
internal: true
api_cache:
driver: bridge
internal: true
api_db:
driver: bridge
internal: true
volumes:
redis_data:
pgdata:
Analicemos cada parte.
Redes en este stack
Tienes cuatro redes.
traefik_public: conecta Traefik con el frontend. Traefik recibe peticiones de internet y las reenvía al frontend. Nadie más está en esta red salvo Traefik y frontend.
frontend_backend: conecta el frontend con la API. El frontend hace peticiones a la API a través de esta red. Es interna, no tiene acceso a internet.
api_cache: conecta la API con Redis. Solo ellos dos están en esta red. Redis no tiene otra red.
api_db: conecta la API con PostgreSQL. Solo ellos dos. PostgreSQL no tiene otra red.
Flujo de tráfico
Cuando alguien visita app.tudominio.com:
- El tráfico llega a Traefik por el puerto 443.
- Traefik reenvía la petición al frontend a través de
traefik_public. - El frontend sirve el HTML estático. El JavaScript en el navegador hace llamadas a la API.
- El frontend reenvía las peticiones de la API a través de
frontend_backend. - La API procesa la petición. Si necesita datos, consulta PostgreSQL a través de
api_db. Si necesita caché, consulta Redis a través deapi_cache. - La respuesta vuelve por el mismo camino.
Aislamiento aplicado
- Traefik solo ve el frontend. No ve la API, ni Redis, ni PostgreSQL.
- El frontend solo ve Traefik y la API. No ve Redis ni PostgreSQL.
- La API ve el frontend, Redis y PostgreSQL. Pero no ve Traefik ni internet.
- Redis solo ve la API. No ve nada más.
- PostgreSQL solo ve la API. No ve nada más.
Si alguien compromete el frontend, tiene acceso a la API. Pero no tiene acceso directo a Redis ni a PostgreSQL. Para llegar a los datos, necesita comprometer también la API.
Si alguien compromete Traefik, solo ve el frontend. No puede escalar a la API ni a los datos.
Si alguien compromete Redis, solo ve la API. No puede acceder a PostgreSQL ni al frontend.
Cada contenedor tiene el mínimo acceso necesario para funcionar. Nada más.
Verificación: comprobar conectividad
Has montado el stack. Ahora toca verificar que el aislamiento funciona.
Verificar redes creadas
docker network ls
Deberías ver tus redes:
NETWORK ID NAME DRIVER SCOPE
abc123... traefik_public bridge local
def456... frontend_backend bridge local
ghi789... api_cache bridge local
jkl012... api_db bridge local
Verificar conectividad correcta
Entra en el contenedor de la API y comprueba que puede alcanzar Redis y PostgreSQL:
docker exec -it api sh
# Probar conexión a Redis
nc -zv redis 6379
# Respuesta esperada: Connected to redis 6379
# Probar conexión a PostgreSQL
nc -zv db 5432
# Respuesta esperada: Connected to db 5432
# Probar conexión al frontend
nc -zv frontend 80
# Respuesta esperada: Connected to frontend 80
Todo esto debería funcionar porque la API está en las redes necesarias.
Verificar aislamiento
Ahora comprueba que lo que NO debe funcionar, realmente no funciona.
Entra en el frontend:
docker exec -it frontend sh
# La API debería ser accesible (misma red frontend_backend)
nc -zv api 3000
# Respuesta esperada: Connected to api 3000
# Redis NO debería ser accesible
nc -zv redis 6379
# Respuesta esperada: connection refused o timeout
# PostgreSQL NO debería ser accesible
nc -zv db 5432
# Respuesta esperada: connection refused o timeout
Entra en Redis:
docker exec -it redis sh
# PostgreSQL NO debería ser accesible
nc -zv db 5432
# Respuesta esperada: connection refused o timeout
# La API tampoco debería ser accesible... espera
nc -zv api 3000
# Respuesta esperada: connection refused o timeout
Redis solo está en api_cache. La API también está en api_cache. ¿Por qué Redis no puede alcanzar la API? Porque Redis no tiene la API en su red… No, espera. Sí debería poder. Vamos a aclarar.
Si dos contenedores están en la misma red, pueden comunicarse. Redis y la API están en api_cache. La API puede alcanzar Redis y Redis puede alcanzar la API. Pero Redis no puede alcanzar PostgreSQL porque están en redes diferentes.
Comprueba que Redis NO puede alcanzar PostgreSQL. Eso es lo importante.
docker exec -it redis sh
nc -zv db 5432
# Debería fallar
Verificar desde fuera de Docker
Desde el host (tu servidor), intenta conectar a los puertos de los servicios internos:
# Intentar conectar a PostgreSQL
nc -zv localhost 5432
# Debería fallar porque PostgreSQL no tiene puerto expuesto
# Intentar conectar a Redis
nc -zv localhost 6379
# Debería fallar por la misma razón
Si tienes éxito en alguna de estas conexiones, has expuesto un puerto que no deberías. Revisa tu docker-compose.yml.
Verificar el aislamiento de red interna
Las redes con internal: true no tienen acceso a internet. Compruébalo:
docker exec -it api sh
ping -c 1 8.8.8.8
# Debería fallar (no hay salida a internet)
docker exec -it db sh
ping -c 1 8.8.8.8
# También debería fallar
Si tienes conectividad a internet desde estos contenedores, la red no está correctamente aislada. Revisa que has puesto internal: true en las redes.
Verificar conectividad de Traefik
Desde Traefik, comprueba que puede alcanzar el frontend:
docker exec -it traefik sh
wget -qO- http://frontend
# Debería recibir el HTML del frontend
Y que NO puede alcanzar la API, Redis ni PostgreSQL:
docker exec -it traefik sh
nc -zv api 3000
# Debería fallar
Si Traefik puede alcanzar la API directamente, tienes un problema de segmentación. Traefik solo debería ver el frontend.
Automatizar las pruebas
Puedes crear un script para verificar el aislamiento después de cada despliegue:
#!/bin/bash
# test-isolation.sh
echo "Comprobando aislamiento de red..."
# La API debe poder alcanzar Redis y DB
docker exec api nc -zv redis 6379 && echo "OK: API -> Redis" || echo "FAIL: API -> Redis"
docker exec api nc -zv db 5432 && echo "OK: API -> DB" || echo "FAIL: API -> DB"
# La API no debe tener salida a internet
docker exec api ping -c 1 -W 2 8.8.8.8 && echo "FAIL: API -> Internet" || echo "OK: API no tiene internet"
# Redis no debe alcanzar DB
docker exec redis nc -zv db 5432 && echo "FAIL: Redis -> DB" || echo "OK: Redis no ve DB"
# Frontend no debe alcanzar DB ni Redis
docker exec frontend nc -zv db 5432 && echo "FAIL: Frontend -> DB" || echo "OK: Frontend no ve DB"
docker exec frontend nc -zv redis 6379 && echo "FAIL: Frontend -> Redis" || echo "OK: Frontend no ve Redis"
# DB no debe tener salida a internet
docker exec db ping -c 1 -W 2 8.8.8.8 && echo "FAIL: DB -> Internet" || echo "OK: DB no tiene internet"
echo "Verificación completada."
Este script lo ejecutas después de cada docker-compose up -d. Si algo falla, sabes que la segmentación no es correcta.
Buenas prácticas adicionales
Nombra las redes de forma descriptiva
No llames a tus redes red1, red2 o default. Usa nombres que indiquen su propósito: traefik_public, internal_backend, db_network, cache_network.
Dentro de unos meses, cuando vuelvas a mirar la configuración, agradecerás haber puesto nombres claros.
Usa redes externas para Traefik
Traefik suele estar en un proyecto Docker Compose separado del resto de servicios. Declara traefik_public como red externa en todos los proyectos que la necesiten.
networks:
traefik_public:
external: true
Y crea la red antes de desplegar nada:
docker network create traefik_public
No compartas redes de bases de datos
Cada base de datos debería tener su propia red. No pongas PostgreSQL de una aplicación y MySQL de otra en la misma red. Si un atacante compromete una aplicación, no debería poder alcanzar la base de datos de otra.
Usa internal: true cuando puedas
Las redes internas (internal: true) no tienen acceso a internet. Esto limita lo que un atacante puede hacer si compromete un contenedor.
Pero ten cuidado. Algunos contenedores necesitan acceso a internet para funcionar. Por ejemplo, un contenedor que necesita descargar actualizaciones o consultar una API externa. En ese caso, no uses internal: true.
Evalúa cada servicio. Si no necesita internet, pon internal: true. Si lo necesita, usa una red bridge normal pero limita el acceso con firewall.
Documenta el diseño de red
Dibuja un diagrama de tu segmentación. Puede ser un texto simple como el que viste antes, o un diagrama en draw.io. Lo importante es que cualquier persona (o tú mismo dentro de seis meses) entienda qué redes existen y qué contenedores están en cada una.
Mantén la documentación en el mismo repositorio que los archivos Docker Compose. Un simple README.md con el diagrama y la explicación es suficiente.
Revisa periódicamente
La segmentación no es algo que configuras una vez y olvidas. Cuando añades un nuevo servicio, revisa:
- ¿Qué redes necesita?
- ¿Qué otros servicios están en esas redes?
- ¿Debería crear una nueva red para aislar este servicio?
Cada nuevo servicio es una oportunidad para mejorar la segmentación o para romperla.
Resumen
La segmentación de red en Docker es una de las medidas de seguridad más efectivas que puedes implementar. No requiere hardware adicional. No requiere software externo. Solo requiere pensar en cómo se comunican tus servicios.
En este capítulo has visto:
- El problema de tener todos los contenedores en la misma red Docker.
- Los tipos de redes Docker: bridge, overlay y macvlan, y cuándo usar cada una.
- El diseño de tres capas: red pública (traefik_public), red privada (internal_backend) y red de base de datos (db_network).
- Cómo conectar contenedores a múltiples redes con el mínimo necesario.
- Por qué las bases de datos y Redis nunca deben exponer puertos al exterior.
- Cómo integrar Traefik respetando la segmentación: solo ve lo que necesita enrutar.
- El aislamiento total de bases de datos sin interfaz externa.
- La regla de no exponer puertos innecesarios: usa
exposeen lugar deports. - Un ejemplo práctico completo con frontend, API, Redis y PostgreSQL.
- Pruebas de verificación para confirmar que el aislamiento funciona.
Cada red que creas es una barrera. Cada barrera que pones entre un atacante y tus datos es tiempo que ganas. Y en seguridad, el tiempo es lo único que no puedes recuperar.
Ahora tus servicios Docker no viven en el mismo pasillo abierto. Tienen sus propias habitaciones, con sus propias puertas, y solo tú tienes las llaves para abrir las que realmente necesitas.
En el próximo capítulo vas a llevar la segmentación un paso más allá. Redes Docker está bien para un solo servidor, pero ¿y si tienes varios servidores y quieres que servicios específicos se comuniquen entre máquinas de forma segura? Ahí entran las mallas VPN con Tailscale, Netbird o Headscale. Pero eso es otra historia.
Más información,
- Documentación oficial de redes Docker: https://docs.docker.com/network/
- Documentación de Docker Compose networks: https://docs.docker.com/compose/networking/
- Documentación de redes bridge en Docker: https://docs.docker.com/network/bridge/
- Documentación de internal networks en Docker: https://docs.docker.com/compose/networking/#configure-the-default-network
- Documentación de Traefik v3 con Docker: https://doc.traefik.io/traefik/providers/docker/
- Tutorial de Docker en atareao.es