Saltar al contenido
Nuevo

DeepSeek V4.1 Flash gratis por 7 días: 10M tokens al día tras cualquier recarga. Hasta el 30/11.

Cómo funciona

Cómo crear una red Docker aislada para tu laboratorio

Crea una red Docker sin acceso a internet, ejecuta OWASP Juice Shop en ella y publica una copia para el navegador solo en 127.0.0.1.

Principiante12 min de lecturaPor NotHoly

Un laboratorio es tan seguro como los caminos que entran y salen de él. Si tu aplicación vulnerable puede llegar a internet, puede hacer llamadas a lugares que nunca quisiste tocar. Si escucha en todas las interfaces de tu máquina, cualquiera conectado a la misma red Wi-Fi puede alcanzarla. Docker te da control sobre los dos caminos con unos pocos comandos. Esta guía usa OWASP Juice Shop como objetivo y recorre cada uno.

Vas a crear una red sin salida, levantar Juice Shop en ella, llamar a la aplicación desde un segundo contenedor y comprobar que esa red no llega a internet. Después ejecutarás una copia aparte para tu navegador, publicada solo en la interfaz de loopback, para que nada de tu red local pueda verla. El aislamiento es el primer control de seguridad de cualquier laboratorio, y todos los ejercicios posteriores, del escaneo a la práctica de explotación, dependen de él.

Si todavía no has ejecutado Juice Shop, empieza por OWASP Juice Shop con Docker. Este artículo da por sentados los conceptos básicos de contenedores de esa guía.

Por qué el aislamiento va primero

Un laboratorio de seguridad ejecuta software hecho para romperse. Juice Shop tiene fallas intencionales, y de eso se trata. El riesgo no es la aplicación en sí. El riesgo es lo que pasa cuando una falla, una mala configuración o un error de tipeo le da a la aplicación, o a una herramienta que ejecutas contra ella, un camino hacia otro lugar.

Tres casos muestran cómo sale mal:

  • La aplicación tiene una función que descarga una URL, y un payload que estás probando hace que consulte un host que no pretendías. Sin control de salida, la petición sale de tu máquina.
  • Un contenedor de herramienta ejecuta un comando contra una dirección mal escrita. Sin una frontera de red, esa dirección podría pertenecer a tu router, a tu empresa o al servidor de un desconocido.
  • Publicas la aplicación con un simple -p 3000:3000. Docker pasa a aceptar conexiones en todas las interfaces del host, así que otros dispositivos de tu red pueden alcanzar la aplicación vulnerable.

El aislamiento cubre cada caso. Una red interna no tiene ruta hacia afuera. Un puerto atado al loopback no tiene camino por la LAN. Una red dedicada separa los contenedores del laboratorio de tus otros proyectos. Ninguno de estos pasos es difícil. Solo es fácil olvidarlos cuando tienes prisa, y por eso esta guía te pide verificar cada uno.

Redes de Docker en cinco minutos

Necesitas tres ideas antes de que los comandos tengan sentido.

Redes bridge

Docker conecta los contenedores a redes virtuales. El driver por defecto es bridge. Cada contenedor en una red bridge recibe su propia dirección IP en una subred privada. En Linux, el bridge vive en el propio host. En Windows y macOS, Docker Desktop ejecuta el motor dentro de una máquina virtual Linux, así que el bridge vive dentro de esa VM.

Los contenedores en el bridge por defecto se alcanzan por dirección IP, pero no por nombre. Una red bridge que creas tú con docker network create resuelve los nombres de los contenedores por DNS, con un resolvedor integrado en Docker. Por eso el contenedor de herramienta de esta guía puede llamar a http://juice:3000 sin depender de una IP que cambia después de un reinicio.

Redes internas

Cuando creas una red con --internal, Docker restringe el acceso externo a ella. La documentación de Docker lo describe así: los contenedores de una red interna pueden comunicarse entre sí, pero no con nada de afuera. Eso incluye internet. Lo verás en el paso 4, cuando la consulta de un host externo falle.

Puertos publicados

Por defecto, el puerto de un contenedor no se publica hacia otras máquinas. La opción -p o --publish cambia eso. Mapea una dirección y un puerto del host a un puerto del contenedor, y la dirección que elijas define qué interfaces aceptan conexiones.

-p 3000:3000 escucha en todas las interfaces del host, incluida la dirección de tu LAN. -p 127.0.0.1:3000:3000 escucha solo en loopback, así que solo los programas de la misma máquina pueden conectarse. La documentación de Docker sobre publicación de puertos muestra esta misma forma. La diferencia es un prefijo, y importa.

Qué vas a montar

El laboratorio tiene cinco partes:

  • juicenet, una red interna sin ruta de salida.
  • juice, Juice Shop conectado solo a juicenet, sin puerto publicado.
  • Un contenedor desechable curlimages/curl en juicenet que llama a juice por nombre.
  • El mismo contenedor desechable intentando llegar a example.com, lo que debe fallar.
  • juice-local, una segunda instancia de Juice Shop publicada en 127.0.0.1:3000 para tu navegador.

El contenedor juice es el que aíslas. juice-local es una copia de comodidad para navegar. Corre en la red por defecto, así que sí llega a internet. Ten eso presente y no pongas credenciales reales ni datos personales en la aplicación.

Antes de empezar

Necesitas:

  • Docker instalado y el motor en ejecución. docker info debe mostrar los detalles del servidor sin errores. En la máquina que usé para esta guía, Docker Engine 29.8.2 corría en Windows 11 a través de Docker Desktop.
  • El puerto 3000 libre en el host para la copia del navegador.
  • Un shell. Los ejemplos usan Git Bash en Windows 11. Los comandos funcionan igual en shells de macOS y Linux. En PowerShell, las cadenas de formato de -w entre comillas simples necesitan otra forma de entrecomillar.

Revisa los nombres antes de elegirlos. Otros proyectos de tu máquina pueden usar ya nombres como juice o juicenet. Ejecuta esto primero:

bash
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'

Si alguna de las listas muestra juice, juice-local o juicenet, elige otros nombres o elimina antes los viejos. No borres nada que no hayas creado tú.

Revisa también la imagen que vas a ejecutar. Juice Shop publica etiquetas de versión en su imagen:

bash
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'

Este comando falla hasta que la imagen esté local, porque el primer docker run es el que la descarga. En la máquina que usé para esta guía, la imagen informó la versión 20.3.0. La tuya puede informar otra, así que anótala.

Paso 1: crear la red interna

bash
docker network create --internal juicenet

Docker imprime el ID de la nueva red. El tuyo será distinto. Esto fue lo que imprimió en la máquina de prueba:

text
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9

Confirma que la opción se aplicó:

bash
docker network inspect juicenet --format '{{.Internal}}'
text
true

Si la salida es false, creaste la red sin --internal. Elimínala con docker network rm juicenet y créala de nuevo. No sigas hasta ver true, porque todas las pruebas siguientes suponen que la opción está activa.

Paso 2: levantar Juice Shop en la red interna

bash
docker run -d --name juice --network juicenet bkimminich/juice-shop

Cada opción tiene su función. -d ejecuta el contenedor en segundo plano. --name juice le da un nombre DNS que los demás contenedores de juicenet pueden resolver. --network juicenet lo conecta solo a esa red interna. No hay -p, así que el contenedor no publica ningún puerto hacia el host.

Docker imprime el ID del contenedor:

text
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8

La aplicación escucha en el puerto 3000. Juice Shop tarda en arrancar después de que el contenedor sube, así que no pruebes de inmediato. Usa un bucle que verifique si un contenedor de herramienta recibe HTTP 200:

bash
for i in $(seq 1 30); do
  s=$(docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000)
  echo "try $i: $s"
  [ "$s" = "200" ] && break
  sleep 3
done

Mientras la aplicación arranca, el bucle imprime 000, que significa que curl no pudo conectarse. En la máquina de prueba imprimió 000 cuatro veces y luego 200 en el quinto intento. A ti puede tomarte más o menos intentos. El bucle se rinde tras 30 intentos. Si nunca ves un 200, revisa docker logs juice para ver los errores.

Paso 3: llegar a la aplicación desde un contenedor de herramienta

bash
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000
text
200

Lee cada parte. --rm elimina el contenedor de herramienta cuando termina. --network juicenet pone la herramienta en la misma red interna que la aplicación. curlimages/curl es una imagen pequeña que contiene curl. http://juice:3000 usa el nombre del contenedor, que Docker resuelve dentro de juicenet. La opción -s oculta la barra de progreso, -o /dev/null descarta el cuerpo de la respuesta y -w imprime solo el código de estado.

Este es el patrón que vas a reutilizar. Un escáner, un proxy o un script entra en la red del laboratorio y apunta al contenedor por nombre. Nada en tu host necesita enrutar hacia la aplicación.

La etiqueta de la imagen aquí no está fijada, así que Docker descarga lo que sea latest hoy. Para un laboratorio que piensas repetir, fija la etiqueta una vez que la hayas probado.

Paso 4: comprobar que la red no tiene internet

Una prueba que pasa solo significa algo cuando a su lado hay una prueba que falla. El paso 3 mostró que el contenedor de herramienta llega a la aplicación, así que la red funciona. Ahora verifica el otro sentido:

bash
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"
text
curl: (28) Resolving timed out after 5000 milliseconds
exit=28

La opción -m 5 limita la petición a cinco segundos. -sS oculta la barra de progreso pero deja visibles los errores. El código de salida 28 es el código de timeout de curl. El fallo ocurre en la resolución de nombres: la consulta de example.com nunca recibe respuesta, así que curl ni siquiera abre una conexión. Es lo esperable en una red sin ruta hacia afuera.

Este es el resultado que quieres. El laboratorio llega a sus propios contenedores y no llega al exterior. Guarda la salida de los pasos 3 y 4 en tus notas de laboratorio con la fecha y la versión de Docker. Ese registro ayuda cuando vuelvas y algo haya cambiado.

Paso 5: ejecutar una copia solo en loopback para el navegador

Tu navegador no resuelve el nombre juice, y el contenedor juice no publica ningún puerto, así que no puedes llegar a él desde el host. Para navegar, levanta una segunda copia y publícala en loopback:

bash
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shop

La opción -p 127.0.0.1:3000:3000 mapea el puerto 3000 de la interfaz de loopback al puerto 3000 dentro del contenedor. Esta copia usa el bridge por defecto, no juicenet, así que no está aislada de internet. Es una concesión deliberada para una copia de navegación. Si necesitas algo más estricto, tendrás que diseñar y probar un montaje aparte.

La copia tarda un poco en arrancar. Usa el mismo tipo de bucle de antes, apuntando al host:

bash
for i in $(seq 1 30); do
  s=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
  echo "try $i: $s"
  [ "$s" = "200" ] && break
  sleep 3
done

En la máquina de prueba la copia respondió 200 en el décimo intento. Ahora confirma dónde está atado el puerto:

bash
docker port juice-local
text
3000/tcp -> 127.0.0.1:3000

Después, revisa el lado de la LAN. Busca la dirección IPv4 de tu máquina en la red local con ipconfig en Windows o ip addr en Linux. Luego pide la aplicación en esa dirección:

bash
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://TU-IP-DE-LAN:3000/

Reemplaza TU-IP-DE-LAN por tu dirección. En la máquina de prueba esto imprimió 000 y curl salió con código 7, lo que significa que no pudo conectarse. La misma petición a 127.0.0.1 devolvió 200. Esa diferencia es todo el sentido de atar al loopback.

Ahora abre http://127.0.0.1:3000 en el navegador. Debes ver la página de inicio de Juice Shop. Usa la aplicación solo desde esa misma máquina.

Ejecuta docker ps para ver los dos contenedores lado a lado:

bash
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'
text
NAMES         PORTS
juice-local   127.0.0.1:3000->3000/tcp
juice         3000/tcp

La fila de juice muestra 3000/tcp sin dirección del host. El puerto existe dentro de la red, pero no está mapeado al host. La fila de juice-local muestra el enlace con el host.

Paso 6: limpiar

Detén y elimina el laboratorio. Los contenedores tienen que salir antes que la red, porque Docker se niega a eliminar una red que aún tiene contenedores conectados:

bash
docker rm -f juice juice-local && docker network rm juicenet
text
juice
juice-local
juicenet

Docker imprime los nombres que eliminó. Luego confirma que no quedó nada:

bash
docker ps -a --filter name=juice --format '{{.Names}}'
docker network ls --filter name=juicenet --format '{{.Name}}'

Ambos comandos deben imprimir vacío. Los filtros coinciden con parte del nombre, así que lee la salida antes de dar por hecho que está completa, y elimina solo lo que iniciaste tú. Además, --rm solo limpia los contenedores desechables de herramienta. Los contenedores iniciados con -d se quedan hasta que los elimines, y por eso existe este paso.

Errores comunes

Publicar sin dirección. -p 3000:3000 escucha en todas las interfaces del host. Usa el prefijo 127.0.0.1: en cualquier aplicación del laboratorio que abras desde la misma máquina, y revisa el resultado con docker port.

Olvidar --internal. La red funciona sin la opción y nada te avisa. Ejecuta docker network inspect juicenet --format '{{.Internal}}' y busca true.

Conectar la aplicación a la red por defecto. Un contenedor iniciado sin --network entra al bridge por defecto, que llega al exterior. Si la aplicación debe quedar aislada, nombra la red cada vez.

Confiar en una prueba que falló. Un error en el paso 4 puede venir del DNS, de un proxy, de una VPN o de un error de tipeo. El paso 3 es el control que muestra que la prueba mide lo correcto.

Usar el nombre desde el host. El nombre juice solo se resuelve para contenedores de juicenet. Tu navegador no puede usarlo. Por eso existe la copia para el navegador.

Dejar contenedores en ejecución. Un contenedor en segundo plano sigue consumiendo recursos y mantiene el puerto ocupado. Termina cada sesión con el paso de limpieza y ejecuta docker ps después.

Lo que este laboratorio no demuestra

La opción de red interna es un control, y tiene límites.

No impide que una herramienta en juicenet llegue a todos los demás contenedores de esa red. Los contenedores de una misma red pueden hablar entre sí, y eso es lo que hace funcionar el paso 3. Mantén los servicios sin relación fuera de la red del laboratorio.

La publicación en loopback te protege de otras máquinas de tu LAN. No te protege de programas que ya corren en tu propia computadora, porque esos también pueden conectarse a 127.0.0.1.

La prueba verifica un nombre y un host externo. No demuestra que todos los caminos de salida estén cerrados. El comportamiento de Docker puede variar entre Docker Desktop, motores Linux y configuraciones rootless, así que repite las verificaciones en la máquina que realmente usas.

Por último, este laboratorio solo muestra que el aislamiento funciona. No muestra que Juice Shop sea seguro de exponer. No lo es, y esa es la razón para mantenerlo aislado.

Cómo seguir avanzando

Cuando el laboratorio funcione, unos pocos cambios pequeños lo hacen más útil:

  • Escribe un script corto que ejecute las verificaciones de los pasos 3 a 5 y muestre pasó o falló en cada una. Ejecútalo cada vez que levantes el laboratorio.
  • Agrega un segundo servicio a juicenet, como una base de datos, y confirma que la aplicación lo alcanza por nombre. Así se arman los laboratorios de varios contenedores.
  • Lee la documentación de redes de Docker sobre las opciones de driver que aún no usaste, como los bridges definidos por el usuario con subredes personalizadas.
  • Aprende lo básico del descubrimiento de hosts antes de escanear nada. El siguiente artículo trata de Nmap básico en tu propio laboratorio. Escanea solo redes y hosts que sean tuyos o para los que tengas permiso por escrito. La red juicenet es un buen primer objetivo, porque controlas todos sus contenedores.

Fuentes

Fuentes

  1. Docker Engine networking overview
  2. docker network create (CLI reference)
  3. Docker port publishing and mapping
  4. OWASP Juice Shop (GitHub repository)

Más en Laboratorios de hacking

Laboratorios de hacking

Cómo escribir un buen informe de pentest

Estructure un informe de pentest y escriba un hallazgo que otra persona pueda reproducir, con evidencia, impacto, vector CVSS v4.0 y remediación.

Intermedio9 min de lectura

Laboratorios de hacking

Cómo montar un laboratorio doméstico legal

Monta un laboratorio que puedes atacar: archivo de alcance, puertos de Docker en loopback, red aislada, cuaderno de notas y desmontaje limpio.

Principiante4 min de lectura