1Por qué importa
Hay dos formas de aprender seguridad ofensiva y solo una termina bien. La primera es leer una técnica y probarla contra lo primero que responda; la segunda es montar un objetivo propio, romperlo, mirar los logs y reconstruirlo. La diferencia no es solo legal: la primera no enseña casi nada, porque cuando algo funciona no puedes ver por qué funcionó, y cuando falla no puedes distinguir si falló tu técnica o si el objetivo ya estaba parcheado.
Un laboratorio bien montado te da tres cosas que ningún objetivo ajeno te da nunca. Observabilidad: puedes leer los logs de las dos puntas del ataque a la vez. Reproducibilidad: si algo funcionó, lo puedes repetir mañana en el mismo estado exacto. Y reversibilidad: destruyes y reconstruyes en segundos, así que puedes permitirte romper cosas de verdad, que es la única manera de entenderlas.
Hay una cuarta razón, menos evidente y más importante: un laboratorio de seguridad es una máquina llena de software deliberadamente vulnerable. Si no está aislado, no es un laboratorio, es una brecha esperando su turno dentro de tu propia casa. La disciplina de aislamiento que vas a aprender aquí no es burocracia del curso; es lo único que separa “estoy practicando” de “he montado un punto de entrada en mi red doméstica”.
Al final de esta lección tienes que ser capaz de mirar cualquier
docker-compose.yml —el del curso o el de tu trabajo— y responder en
treinta segundos: qué se publica al host, qué puede salir a internet, y qué pieza
es la única que cruza la frontera. Esas tres respuestas son un modelo de amenazas
en miniatura.
2Conceptos
- Laboratorio: conjunto de piezas desechables, reproducibles y aisladas donde se practica de verdad.
- Topología: quién puede hablar con quién. Es la parte del laboratorio que de verdad importa.
- Frontera de confianza: el punto donde termina lo que controlas y empieza lo que solo esperas que se porte bien.
- Superficie de ataque: todo lo que un atacante puede tocar. En un laboratorio hay dos: la interna (a propósito) y la que apunta a tu máquina (que debe ser cero).
- Red interna: red de Docker sin ruta de salida. Los contenedores se ven entre ellos y no llegan a internet.
- Publicar un puerto: pedirle al host que reenvíe un puerto suyo a un contenedor. Es la única forma en que el laboratorio toca tu máquina.
- Pieza de borde: el único servicio con un pie en cada red. En el laboratorio de referencia, el proxy inverso.
3Explicación profunda
El laboratorio de referencia del curso tiene cinco servicios. No son cinco por gusto: cada uno representa una categoría de pieza que vas a encontrar en cualquier sistema real, y quitar cualquiera de ellos deja un hueco pedagógico concreto.
| Pieza | Servicio | Para qué está |
|---|---|---|
| Proxy inverso | proxy | Único punto de entrada. Es la frontera de confianza dibujada en YAML, y la única pieza que ve las dos redes. |
| Aplicación vulnerable | app | El objetivo. Sirve contenido y, según la fase, tendrá fallos plantados a propósito. |
| Base de datos | db | El activo. Sin un sitio donde vivan los datos, media docena de ataques no tienen sentido. |
| Observabilidad | monitor | El lado azul. Sin logs no hay defensa, y la mitad del curso es mirar aquí después de atacar. |
| Puesto de atacante | atacante | Desde dónde se lanzan las pruebas. Vive dentro, no en tu máquina, y eso es deliberado. |
El detalle que más gente pasa por alto es el último. El atacante vive dentro del
laboratorio, no en tu terminal. Cuesta un poco más al principio —hay que entrar con
docker compose exec— y a cambio compra dos cosas. La primera es que tus
herramientas ofensivas no se instalan en tu máquina de trabajo. La segunda es que,
cuando pruebas si algo tiene salida a internet, la respuesta que obtienes es la del
laboratorio y no la de tu portátil, que sí la tiene.
Dos redes, y por qué no una
Docker crea por defecto redes de tipo bridge: un puente virtual en tu
máquina, con un rango propio, y reglas de NAT que le dan salida a internet a todo lo
que se conecte. Ese comportamiento por defecto es exactamente lo que no queremos para
software deliberadamente roto.
Añadir internal: true a una red cambia una sola cosa, y es la decisiva:
Docker no programa la regla de enmascaramiento que da salida a esa red. Los
contenedores siguen teniendo dirección, siguen viéndose entre ellos, siguen resolviendo
nombres… y no tienen por dónde salir. No es un cortafuegos con reglas que se puedan
equivocar: es la ausencia de una ruta.
bridge normal | bridge con internal: true | |
|---|---|---|
| Contenedor ↔ contenedor | sí | sí |
| DNS por nombre de servicio | sí | sí |
| Contenedor → internet | sí | no |
| Host → contenedor (puerto publicado) | sí | no, en la práctica |
De ahí sale la topología: lab_borde es una red normal, con salida, y solo
la toca el proxy. lab_interna es interna, y ahí viven las
otras cuatro piezas. El proxy tiene un pie en cada una. Esa asimetría es todo el
diseño: hay exactamente un camino entre el laboratorio y el mundo, y está
escrito en cuatro líneas que puedes auditar de un vistazo.
Resolución de nombres: por qué el laboratorio es reproducible
Cada red de Docker trae un resolutor propio. Dentro de una red, el nombre del
servicio del compose resuelve a la dirección del contenedor, y se actualiza solo
si el contenedor se recrea. Por eso el proxy puede escribir http://app en
su configuración y funcionar en tu máquina, en la mía y dentro de dos años.
Es también la razón de que en este curso no aparezcan direcciones escritas a mano. Fijar una dirección literal en un laboratorio es la forma más rápida de que deje de levantar en otra máquina, y además rompe el hábito que quieres construir: en infraestructura moderna, las direcciones son efímeras y los nombres son el contrato.
Publicar un puerto no es “abrirlo un poco”
Un contenedor que no publica puertos no es alcanzable desde tu máquina, aunque
escuche en el 80 dentro de sí mismo. Publicar es pedirle al host que reenvíe. Y la
sintaxis tiene una trampa de diseño heredada: si escribes 8080:80, la
parte de la dirección se omite y Docker asume 0.0.0.0, es decir,
todas las interfaces de tu máquina.
Traducido: tu aplicación deliberadamente vulnerable queda accesible para cualquiera
que comparta red contigo. La red del trabajo, la de la cafetería, la wifi de invitados
del hotel. Escribir 127.0.0.1:8080:80 es una sola diferencia de nueve
caracteres y significa “solo procesos de esta máquina”. En este curso esa forma no es
una recomendación de estilo; es obligatoria en todos los laboratorios.
Y hay un agravante que casi nadie espera la primera vez: en Linux, Docker inserta sus reglas de reenvío en una cadena que se evalúa antes que las reglas del firewall del sistema. Un puerto publicado en todas las interfaces puede seguir siendo alcanzable aunque tu firewall diga que ese puerto está cerrado. Volveremos a esto en la lección de reset y firewall; por ahora quédate con que la seguridad del puerto la decide el compose, no el firewall.
4Ejemplo sencillo
# Un laboratorio es un edificio con una sola puerta proxy la porteria: unica puerta a la calle, y da a un portal cerrado app el piso que vas a forzar db la caja fuerte del piso monitor las camaras: existen para que puedas ver que paso atacante tu, dentro del edificio, sin llaves de la calle # Lo que define el edificio no son las habitaciones, son las puertas lab_borde la calle solo la porteria da a ella lab_interna el portal todos se ven, nadie sale 127.0.0.1 tu propia acera la porteria solo abre hacia ahi
5Ejemplo técnico
Levanta el laboratorio base y pregúntale a Docker —no al compose— cómo quedó de verdad. Estas tres respuestas son las que auditas cada vez que montas un entorno:
La primera comprobación es la que de verdad importa y casi nadie hace: contar las líneas con puertos. En este laboratorio debe haber exactamente una. Si aparecen dos, alguien publicó algo sin querer; si la que hay no lleva dirección delante, el laboratorio está expuesto a la red local aunque “funcione igual”.
La segunda muestra el resolutor interno funcionando. Ese es el mecanismo que permite
que la configuración del proxy diga proxy_pass http://app; sin conocer
ninguna dirección, y el que hace que el laboratorio se comporte igual en cualquier
máquina.
6Diagrama
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
H[host 127.0.0.1:8080] --> P[proxy]
P --> A[app]
P --> M[monitor]
subgraph interna[lab_interna internal:true]
A
M
D[db]
T[atacante]
end
A --> D
T --> A
T --> D
7Código
Todo lo anterior cabe en el esqueleto del compose. Estas son las líneas que definen la seguridad del laboratorio; el resto son detalles de las imágenes:
# las dos redes: aqui esta el 90% del diseno networks: lab_borde: driver: bridge # tiene salida; solo el proxy la toca lab_interna: driver: bridge internal: true # sin ruta de salida: no es un filtro, es una ausencia services: proxy: ports: - "127.0.0.1:8080:80" # con direccion: solo esta maquina networks: - lab_borde # un pie fuera... - lab_interna # ...y otro dentro. La unica pieza asi. app: networks: - lab_interna # sin "ports": inalcanzable desde el host atacante: networks: - lab_interna # el atacante vive dentro, no en tu terminal
Léelo como se lee un plano y no como configuración. Las dos preguntas que responde son
“¿cuántas piezas aparecen bajo ports?” y “¿cuántas piezas listan más de
una red?”. Las respuestas correctas en un laboratorio son una y una, y
tiene que ser la misma pieza. Cualquier otra combinación necesita una justificación
escrita.
8Qué puede salir mal
Publicar sin dirección. 8080:80 en lugar de
127.0.0.1:8080:80. Es la forma que aparece en el 90% de los tutoriales
de internet, porque en un tutorial no hay software vulnerable dentro. En un
laboratorio, es una invitación abierta a quien comparta wifi contigo.
Dar salida al contenedor equivocado. Añadir lab_borde a la lista
de redes del atacante o de la aplicación vulnerable. Es una línea, no rompe nada
visiblemente, y convierte tu laboratorio en algo que puede alcanzar máquinas de
terceros.
Montar tu directorio personal. Un volumes: - ~/:/host “para
pasar un archivo” le da a la aplicación vulnerable acceso de lectura y escritura a
tus claves, tus tokens y tu historial. Los montajes de un laboratorio deben ser
rutas del propio laboratorio, y de solo lectura siempre que se pueda.
Etiquetas móviles. Usar latest hace que el laboratorio de hoy no
sea el de mañana. Cuando un ejercicio deje de reproducirse no vas a saber si fue
culpa tuya o de una imagen que cambió por debajo.
Dejarlo levantado. Un laboratorio que arranca solo con
restart: unless-stopped y sobrevive al reinicio es un servicio
vulnerable corriendo permanentemente en tu equipo. Se levanta para trabajar y se
destruye al terminar.
9Cómo lo abusaría un atacante
Un laboratorio de seguridad mal aislado es uno de los objetivos más golosos que
existen en una red compartida, y por una razón elegante: alguien ya se molestó
en instalar software vulnerable, en configurarlo para que el fallo funcione y en
dejarlo corriendo. No hay que buscar el defecto; viene documentado en el
LEEME.
La forma de encontrarlo es aburrida: barrer los puertos altos habituales —el 8080, el 3000, el 8000— en el rango de la red local. Un servicio de práctica se identifica solo, porque suele responder con una portada que dice exactamente qué es. A partir de ahí el trabajo ya está hecho por la víctima.
Y lo interesante no es el laboratorio, es lo que hay detrás. Un contenedor con ejecución de comandos y salida a internet sirve para tres cosas: mirar qué más hay en la red interna del anfitrión, montar un punto de entrada persistente, y usar tu conexión para atacar a terceros. Ese tercer punto es el que convierte un descuido de aprendizaje en un problema legal con tu nombre en los registros.
La segunda vía es más silenciosa: los montajes. Si la aplicación vulnerable tiene montado un directorio de tu máquina, la lectura de archivos que practicas como ejercicio deja de ser un ejercicio en cuanto la ruta apunta fuera del contenedor. No hace falta escapar de nada; se lo diste tú.
10Cómo defenderlo
up -d
- Un solo puerto publicado, siempre con
127.0.0.1:delante. Si necesitas dos, escribe por qué en un comentario del compose. - Una sola pieza con dos redes. El resto vive únicamente en la red con
internal: true. - Verifica en vez de suponer:
docker compose pspara los puertos y uncurlhacia fuera desde el atacante para la salida. - Montajes solo del laboratorio, con
:rocuando el contenedor no necesite escribir. Nunca tu directorio personal, nunca la raíz del repositorio. - Etiquetas fijas de imagen. Un laboratorio que no se reproduce no sirve para aprender, porque no puedes comparar dos ejecuciones.
- Destruye al terminar con
docker compose down -v. La-vse lleva los volúmenes, y eso también es defensa: los datos de práctica no deben acumularse. - Trata el compose como código revisable. Un cambio en
portso ennetworksmerece la misma atención que un cambio en el control de acceso de una aplicación, porque es lo mismo.
11Laboratorio
Levanta el laboratorio y audita la topología antes de tocar nada:
cd cursos/seguridad/labs/lab-00-base
docker compose up -d
docker compose ps
Ahora las tres preguntas de la lección, una por comando. Primero, qué toca tu máquina:
docker compose ps --format "{{.Service}} {{.Ports}}"
Segundo, quién puede salir. Y tercero, quién ve a quién:
Dibuja en papel lo que acabas de comprobar y compáralo con el diagrama de la sección 6. Si tu dibujo tiene una flecha que el diagrama no tiene, o al revés, una de las dos cosas está mal y merece la pena averiguar cuál. Cuando termines:
docker compose down -v
12Ejercicios
Clasifica cinco piezas
Para cada servicio del lab-00-base responde tres cosas en una línea:
(1) ¿publica algún puerto al host?, (2) ¿puede salir a internet?, (3) ¿qué se
pierde del curso si lo borras del compose?
ports y networks de cada servicio.internal: true.
proxy: publica el 8080 en localhost, sí sale a internet, y sin él
desaparece la frontera de confianza (y el único acceso desde el host).
app: no publica, no sale, y sin ella no hay objetivo.
db: no publica, no sale, y sin ella no hay activo que proteger ni
ataques con sentido.
monitor: no publica, no sale, y sin él se pierde toda la mitad
defensiva del curso, que consiste en mirar logs después de atacar.
atacante: no publica, no sale, y sin él tendrías que atacar desde
tu propia máquina, que sí tiene internet y sí tiene tus datos.
Clave: las columnas 1 y 2 tienen que salirte leyendo, sin ejecutar nada. Si necesitaste levantar el laboratorio para responder, vuelve a leer el compose hasta que no haga falta.
Añade una sexta pieza sin romper el aislamiento
Copia el lab-00-base a un directorio propio y añade un servicio
cache con Redis que solo puedan usar app y
atacante. Requisitos: no publica puertos, no tiene salida a internet, y
monitor no debe poder alcanzarlo. Demuestra las tres condiciones con
comandos.
monitor no llegue, la separación tiene que estar en las redes, no en la configuración de Redis.internal: true y ponle solo los tres servicios que deben verse.
Se añade lab_cache con internal: true. El servicio
cache vive solo en ella; app y atacante
se añaden a su lista de redes sin quitarles lab_interna.
monitor no se toca, así que no tiene forma de resolver ni de
alcanzar el nuevo servicio.
networks: lab_cache: driver: bridge internal: true services: cache: image: redis:7-alpine networks: [lab_cache] app: networks: [lab_interna, lab_cache] atacante: networks: [lab_interna, lab_cache]
Comprobaciones: docker compose ps sigue mostrando una única línea
con puertos; desde cache un curl hacia fuera falla; y
docker compose exec monitor sh -c "getent hosts cache" no resuelve.
Clave: lo que se evalúa es que la separación esté en la topología. Una contraseña en Redis no es aislamiento: es una puerta con cerradura en una habitación a la que no debería llegarse.
Demuestra qué hace internal: true por debajo
internal: true no es magia: se traduce en reglas concretas del
anfitrión. Investiga qué crea Docker en el sistema para una red normal que no crea
para una interna, y escribe un párrafo explicando por qué el resultado es “no hay
ruta” y no “hay una ruta bloqueada”. Explica también qué implicación tiene esa
diferencia para alguien que ya tiene ejecución dentro del contenedor.
docker network inspect muestra la diferencia declarada.
Para una red normal, Docker crea el puente, le asigna un rango y programa una
regla de enmascaramiento para el tráfico que sale de ese rango hacia el
exterior; además permite el reenvío entre el puente y la interfaz de salida.
Con internal: true el puente y el rango existen igual, pero no se
programa el enmascaramiento y se bloquea el reenvío hacia fuera del puente.
La implicación práctica es la que importa: alguien con ejecución dentro del contenedor no puede “desbloquear” nada desde dentro, porque el contenedor no tiene privilegios sobre la red del anfitrión. La única forma de dar salida es modificar el compose y recrear la red, es decir, desde fuera y a propósito. Un control que solo se puede desactivar desde fuera del sistema controlado es mucho más fuerte que uno que se configura dentro.
Clave: tiene que aparecer la distinción entre “ausencia de ruta” y “regla que deniega”, y la consecuencia sobre quién puede revertirla. Si tu párrafo no dice quién puede desactivar el control, está incompleto.
Red Team · mapea el laboratorio desde dentro
Desde el contenedor atacante, y sin leer el docker-compose.yml,
reconstruye la topología: qué máquinas hay en tu red, qué puertos tienen abiertos y
cuál de ellas es la frontera hacia el exterior. Entrega un dibujo. Después compáralo
con el compose y anota qué no pudiste ver desde dentro.
atacante ya trae nmap y utilidades de DNS instaladas.
El procedimiento: ip -4 addr para saber tu dirección y tu máscara,
un barrido del rango para encontrar vecinos vivos, y un escaneo de puertos sobre
cada uno. El 5432 identifica la base de datos, el 80 aparece dos veces (la
aplicación y el lado interno del proxy), y los contenedores de utilidades no
escuchan nada.
La pieza de borde se identifica por descarte y por comportamiento: es la única
que responde en dos redes distintas. Lo que no puedes ver desde dentro es
igual de instructivo: no ves el mapeo de puertos hacia el host, no ves que
existe lab_borde ni quién está en ella, y no ves los volúmenes.
Esa es exactamente la asimetría de información que tiene un atacante real
cuando cae dentro de una red.
Clave: el entregable no es la lista de puertos, es la lista de lo que no pudiste deducir desde dentro. Guárdala: describe la ventaja que tiene el defensor.
Blue Team · ¿deja rastro un escaneo interno?
Repite el escaneo del ejercicio anterior mientras sigues los logs de
proxy y de app. Responde: ¿qué parte del escaneo aparece
en los logs y qué parte es invisible? Después propón la mínima instrumentación que
haría visible lo invisible, y di qué coste tiene.
docker compose logs -f en otra terminal antes de lanzar el escaneo, no después.Lo que aparece: las peticiones HTTP completas, con ruta y agente. Lo que no aparece: el descubrimiento de máquinas, las conexiones que se abren y se cierran sin enviar datos, y todo el tráfico hacia servicios que no registran conexiones rechazadas. Un escaneo cuidadoso puede recorrer entera la red interna sin dejar una sola línea en los logs de aplicación.
La instrumentación mínima es registro a nivel de red: contar conexiones por origen y por puerto de destino, y alertar cuando un origen toca muchos destinos distintos en poco tiempo. El coste es volumen —mucho más que los logs de aplicación— y una tasa de falsos positivos que hay que domar con contexto: los comprobadores de salud y los orquestadores hacen exactamente ese patrón todo el día.
Clave: la conclusión que se evalúa es que los logs de aplicación no cubren la capa de red. Si tu respuesta no nombra ese hueco, el escaneo te pasó por delante sin que lo vieras.
Architecture Challenge · un laboratorio para un equipo de veinte
Diseña cómo distribuirías este laboratorio a veinte personas de una empresa para un taller de un día. Decide: dónde corre (portátil de cada uno o infraestructura compartida), cómo garantizas que nadie exponga su instancia a la red corporativa, qué haces con las credenciales de ejemplo, cómo lo destruyes al terminar y cómo demuestras que se destruyó.
Una propuesta razonable: local en cada portátil, porque evita crear un objetivo
común y elimina la tentación de dejarlo corriendo entre sesiones. El control no
es documental sino ejecutable: un script previo al arranque que revisa el
compose, falla si algún ports no empieza por 127.0.0.1:
y falla si algún servicio distinto del proxy lista una red sin
internal: true. Ese script se ejecuta también al final, con
docker compose ls, para demostrar que no queda nada arriba.
Credenciales: se generan por participante al levantar, nunca se comparten, y se destruyen con los volúmenes. El punto no es que sean secretas —son de laboratorio— sino no enseñar el hábito de reutilizar una contraseña conocida por veinte personas.
Clave: no hay una única respuesta. Se evalúa que el control sea verificable por una máquina y que exista una prueba de destrucción. “Se pidió a los asistentes que apagaran su laboratorio” no es una prueba.
13Preguntas de comprensión
¿Por qué el puesto de atacante es un contenedor y no tu propia terminal?
Tu máquina tiene internet, tus claves y tu historial. Un atacante que vive dentro solo puede alcanzar lo que el laboratorio le deja alcanzar.
Un compañero te pasa un laboratorio donde la aplicación vulnerable publica 3000:3000. ¿Cuál es el problema?
Sin dirección, Docker escucha en todas las interfaces. Cualquiera que comparta red contigo alcanza la aplicación vulnerable.
Dentro de lab_interna, ¿puede atacante conectarse al puerto de la base de datos?
La red interna es un muro hacia fuera, no entre las piezas. Dentro se ve todo con todo, y eso es lo que permite practicar movimiento lateral.
¿Cuál de estas comprobaciones detecta el error más grave en menos tiempo?
Un laboratorio expuesto a la red local carga perfectamente en el navegador. La única comprobación que lo detecta es mirar la dirección de los puertos publicados.
14Reto adicional
Escribe el verificador de topología
Escribe un script que reciba un docker-compose.yml y falle con código
distinto de cero si: algún puerto publicado no empieza por 127.0.0.1:,
más de un servicio pertenece a una red sin internal: true, o algún
montaje apunta fuera del directorio del laboratorio. Pruébalo contra el
lab-00-base —debe pasar— y contra una copia rota a propósito.
docker compose config --format json y recorre esa estructura, no el archivo escrito a mano.
La clave del ejercicio es no leer el YAML original. docker compose
config expande las formas cortas, resuelve variables y aplica los valores
por defecto; sobre esa salida las tres reglas son comprobaciones directas: por
cada servicio, recorrer ports y exigir host_ip; cruzar
networks con la definición de redes y contar cuántos servicios
tocan alguna que no sea interna; y comprobar que cada montaje de tipo enlace
resuelve a una ruta dentro del directorio del laboratorio.
El detalle que separa un verificador útil de uno decorativo es el mensaje de error: tiene que decir qué servicio, qué línea conceptual y cuál es la forma correcta. Un script que solo dice “topología inválida” se desactiva la tercera vez que alguien lo ve.
Clave: guárdalo. Es el mismo patrón que vas a aplicar en la fase de CI/CD sobre manifiestos reales, y la diferencia entre este script y una política de plataforma es solo dónde se ejecuta.
15Resumen
- Un laboratorio son cinco papeles: borde, objetivo, activo, observación y atacante. Quitar uno deja un hueco concreto en lo que puedes aprender.
- Lo que define el laboratorio no son las piezas sino la topología: quién habla con quién.
internal: truequita la ruta de salida sin tocar la comunicación interna. No es un filtro que se pueda equivocar: es una ausencia.- Solo una pieza cruza la frontera, y solo un puerto se publica, siempre con
127.0.0.1:delante. - Publicar un puerto sin dirección expone tu laboratorio vulnerable a toda la red donde estés conectado, y el firewall del sistema puede no salvarte.
- El atacante vive dentro. Si atacas desde tu terminal, estás midiendo la conectividad de tu portátil, no la del laboratorio.
- Verifica en vez de suponer: los puertos con
ps, la salida con uncurldesde dentro, la visibilidad con el DNS interno.
internal: true en una red de Docker?8080:80 y 127.0.0.1:8080:80?16Mis notas
Estas notas se guardan solo en este navegador. No se envían a ningún servidor. Expórtalas desde el panel de progreso si cambias de máquina.
17Recursos
Los comentarios se cargan solo si los pides, desde GitHub Discussions. No se carga nada de terceros mientras no pulses.