Laboratorio · Lección 02

Docker y Compose como plataforma de práctica

Docker se enseña casi siempre como una herramienta de empaquetado, y por eso mucha gente lo usa durante años sin saber qué aísla exactamente. Esta lección lo mira desde el otro lado: qué frontera crea cada primitiva —red, volumen, variable de entorno—, dónde está el borde real de esa frontera y qué pasa cuando alguien lo cruza. Al final vas a poder montar un entorno reproducible y, más importante, decir con precisión de qué te protege y de qué no.

Al terminar sabrás

  • Explicar qué aísla un contenedor (namespace, cgroup) y qué comparte con tu máquina.
  • Elegir entre red bridge, red interna y network_mode: host sabiendo qué frontera destruyes en cada caso.
  • Distinguir volumen nombrado, montaje de enlace y tmpfs, y decir cuál deja rastro en tu disco.
  • Explicar por qué una variable de entorno no es un mecanismo de secretos, con las tres formas de leerla.
  • Usar docker compose config para auditar lo que realmente se va a levantar.
  • Montar un entorno que se reproduce igual dentro de un año, y saber qué lo rompería.

1Por qué importa

Todo el material práctico de este curso viaja en archivos docker-compose.yml. Si no entiendes qué hace cada línea, el laboratorio se convierte en una caja negra que unas veces funciona y otras no, y aprender seguridad con una caja negra debajo es aprender supersticiones.

Pero hay una razón mucho más grande que el curso. Docker y Compose son, hoy, la forma en que la mayoría del software se despliega y se prueba. Las mismas tres primitivas que vas a usar para montar tu laboratorio —redes, volúmenes y variables de entorno— son las que deciden, en producción, si un fallo en una aplicación se queda en esa aplicación o se lleva por delante el resto de la máquina. Cada línea de un compose es una decisión de frontera de confianza, aunque nadie la llame así en la revisión de código.

Y hay una tercera razón, incómoda: Docker está lleno de comportamientos por defecto cómodos y poco seguros. Publicar en todas las interfaces, dar salida a internet a todo, ejecutar como root dentro del contenedor, montar rutas del anfitrión sin aviso. Ninguno es un fallo del producto: son decisiones de usabilidad tomadas cuando el caso principal era “que esto arranque”. Saber cuáles son y cómo revertirlos es la mitad del trabajo.

El objetivo real

No estás aprendiendo Docker. Estás aprendiendo a leer un archivo de despliegue y responder: si un atacante consigue ejecutar comandos dentro de este contenedor, ¿qué alcanza? Esa pregunta se responde con tres lecturas —networks, volumes, environment— y con nada más.

2Conceptos

Vocabulario de esta lección
  • Imagen: sistema de archivos de solo lectura, en capas, identificado por una etiqueta o por un digest.
  • Contenedor: un proceso del anfitrión con vistas restringidas del sistema y una capa de escritura efímera encima de la imagen.
  • namespace: mecanismo del núcleo que hace que un proceso vea su propia lista de procesos, su propia red o su propio sistema de archivos.
  • cgroup: mecanismo que limita cuánta memoria o CPU puede consumir. Limita recursos, no visibilidad.
  • Volumen nombrado: almacenamiento gestionado por Docker, fuera del contenedor y fuera de tu árbol de directorios.
  • Montaje de enlace: una ruta de tu máquina expuesta dentro del contenedor. Es una puerta en las dos direcciones.
  • tmpfs: montaje en memoria que desaparece al parar el contenedor. No toca tu disco.
  • Variable de entorno: par nombre-valor visible para el proceso, para todos sus hijos y para cualquiera que pueda inspeccionar el contenedor.

3Explicación profunda

Qué aísla un contenedor, y qué no

Un contenedor no es una máquina. Es un proceso normal de tu sistema al que el núcleo le ha dado vistas propias de algunas cosas. El namespace de procesos hace que solo vea los suyos; el de red, que tenga su propia pila e interfaces; el de montajes, que vea otro árbol de directorios. Los cgroup le ponen techo a la memoria y a la CPU. Y ya.

Lo que no hay es separación de núcleo. Tu núcleo y el del contenedor son el mismo, con las mismas llamadas al sistema y los mismos fallos. Un exploit de escalada en el núcleo ejecutado dentro de un contenedor se ejecuta contra tu máquina. Por eso el material realmente destructivo de este curso vive en una máquina virtual, que es la lección siguiente, y no en un contenedor.

Esta distinción tiene una consecuencia práctica que conviene fijar ya: un contenedor es una frontera buena contra accidentes y una frontera mediana contra un atacante. Contiene un proceso mal configurado, un binario que escribe donde no debe o una dependencia con un fallo de aplicación. No contiene, por sí solo, a alguien que trae un exploit de núcleo o al que le has montado el socket de Docker.

Redes: tres opciones y dos de ellas queman una frontera

Compose te da, en la práctica, tres modos de conectar un servicio, y no son variantes del mismo mecanismo: son tres niveles de aislamiento distintos.

ModoQué ve el contenedorCuándo usarlo
bridge con internal: trueSolo a sus vecinos de red. Sin salida.Por defecto en todo laboratorio. Es la opción segura.
bridge normalA sus vecinos y a internet, con NAT.Solo la pieza de borde, y solo si de verdad necesita salir.
network_mode: hostLa pila de red de tu máquina, entera.Casi nunca. Elimina por completo el aislamiento de red.

El tercero merece un párrafo propio porque aparece en muchas respuestas de foro como solución rápida a “no consigo que se conecten”. Con network_mode: host el contenedor no tiene red propia: usa la tuya. Todo lo que escuche dentro escucha en tus interfaces, la sintaxis ports deja de aplicarse —se ignora— y con ella desaparece la protección de 127.0.0.1:. Un servicio vulnerable en modo anfitrión está expuesto exactamente igual que si lo hubieras instalado a mano en tu equipo.

Dentro de una red definida por ti, Compose activa el resolutor interno: el nombre del servicio resuelve al contenedor. Ojo con un matiz que confunde a mucha gente: en la red bridge por defecto de Docker —la que usas cuando no declaras redes— ese resolutor por nombre no funciona igual. Declarar tus redes no es cosmética; cambia el comportamiento.

Volúmenes: dónde acaba viviendo lo que el laboratorio escribe

Los tres tipos de montaje resuelven problemas distintos y tienen riesgos distintos. Elegir mal no da un error: da un laboratorio que deja restos en tu disco o, peor, uno que puede escribir en tus archivos.

# 1) volumen nombrado: lo gestiona Docker, vive fuera de tu arbol
volumes:
  - db_datos:/var/lib/postgresql/data

# 2) montaje de enlace: una ruta TUYA dentro del contenedor
volumes:
  - ./app:/usr/share/nginx/html:ro   # :ro = el contenedor no puede escribir

# 3) tmpfs: en memoria, desaparece al parar. No toca el disco.
tmpfs:
  - /tmp

El montaje de enlace es el peligroso, y no por sutil: lo que montas, lo entregas. Si la aplicación vulnerable tiene montado un directorio tuyo con permiso de escritura, el ejercicio de escritura arbitraria de archivos que vas a hacer en la fase web deja de ser un ejercicio en el momento en que la ruta apunte al montaje. Por eso en los laboratorios del curso los montajes de enlace son siempre de rutas del propio laboratorio y casi siempre con :ro.

Hay un caso especial que merece nombre propio: montar el socket de Docker (/var/run/docker.sock) dentro de un contenedor. Se ve constantemente en ejemplos de herramientas de monitorización. Es equivalente a dar acceso de administrador a tu máquina, porque quien habla con ese socket puede crear un contenedor privilegiado que monte la raíz del anfitrión. No es una escalada de privilegios: es un atajo documentado. En un laboratorio nunca hace falta.

Y el detalle que arruina más sesiones de práctica: docker compose down sin -v conserva los volúmenes nombrados. Crees que reiniciaste el laboratorio y en realidad arrastras el estado del ataque anterior. Cuando un ejercicio “ya no reproduce”, esta es la primera sospechosa.

Variables de entorno: cómodas, reproducibles y no secretas

Compose ofrece varias vías para inyectar configuración, y conviene tener clara la precedencia porque cuando dos se contradicen no hay ningún aviso:

OrigenPara qué sirvePrioridad
environment: en el servicioValor fijo para ese contenedor.La más alta de las declaradas en el archivo.
env_file:Un archivo con muchos valores; se pasan al contenedor.Por debajo de environment.
Archivo .env del directorioSustituir ${VARIABLES} dentro del propio compose.Es otra cosa: interpola el YAML, no llega solo al contenedor.
Entorno de tu terminalSustitución en el compose y valores de environment sin valor.Gana sobre .env en la interpolación.

La confusión clásica es creer que .env y env_file son lo mismo. El primero rellena huecos ${ASI} del propio YAML; el segundo define variables dentro del contenedor. Un laboratorio que mezcla ambos sin saberlo produce exactamente el peor error posible: funciona en tu máquina y no en la siguiente.

Ahora la parte de seguridad, que es la importante. Una variable de entorno no es un secreto, y hay tres formas de leerla que no requieren nada especial:

# 1) desde el anfitrion, sin entrar al contenedor
docker inspect lab00-db --format '{{json .Config.Env}}'

# 2) desde dentro, en cualquier proceso: el entorno esta en /proc
cat /proc/1/environ | tr '\0' '\n'

# 3) por herencia: cualquier proceso hijo lo recibe, incluido
#    el que se ejecuta por una inyeccion de comandos
env
Tres lecturas, ningún privilegio especial. Una variable de entorno se propaga hacia abajo por diseño: eso la hace cómoda y por eso mismo no sirve para guardar secretos.

Añade a eso que las variables suelen acabar en volcados de error, en trazas de depuración y en los informes de fallos de muchas librerías. En este curso las contraseñas de laboratorio se escriben en claro a propósito, para que se vean y para que en la fase de gestión de secretos tengas un antes y un después concreto que comparar. Lo que no se puede es escribirlas en claro y llamarlo seguro.

4Ejemplo sencillo

# Las cuatro preguntas que responde cualquier compose

networks     ¿con quien puede hablar?      -> alcance
ports        ¿quien puede hablarle?        -> exposicion
volumes      ¿que puede leer y escribir?   -> datos
environment  ¿que sabe?                    -> credenciales

# Y las cuatro respuestas seguras por defecto en un laboratorio

networks     una sola red, con internal: true
ports        ninguno, o uno con 127.0.0.1: delante
volumes      solo rutas del laboratorio, con :ro donde se pueda
environment  nada que no puedas publicar en el repositorio
Un compose es una tabla de cuatro filas disfrazada de configuración. Si sabes leerla así, revisas un despliegue ajeno en un minuto.

5Ejemplo técnico

El archivo que escribes no es el que se ejecuta. Compose expande formas cortas, interpola variables y aplica valores por defecto. docker compose config imprime el resultado real, y esa es la única versión que merece la pena auditar:

lo escrito contra lo que se ejecuta
grep -A2 ports docker-compose.yml ports: - "127.0.0.1:8080:80" docker compose config | grep -A5 ports ports: - mode: ingress host_ip: 127.0.0.1 target: 80 published: "8080" protocol: tcp # la forma larga hace explicito el host_ip: eso es lo que hay que comprobar

El campo host_ip es la comprobación que buscas. Si falta, o si vale 0.0.0.0, el servicio está publicado en todas las interfaces. Un verificador automático es media docena de líneas sobre esta salida, y es infinitamente más fiable que buscar texto en el archivo escrito a mano, donde la misma cosa se puede expresar de tres formas distintas.

Prueba también la otra mitad: pídele a Docker el entorno real de un contenedor y compáralo con lo que creías haber puesto. Es habitual descubrir variables heredadas de la imagen base que nadie declaró.

6Diagrama

Un contenedor comparte el núcleo del anfitrión; sus fronteras son los namespaces y los cgroups, y las tres primitivas que las perforan son la red publicada, el montaje de enlace y las variables de entorno nucleo del anfitrion · COMPARTIDO un exploit de nucleo dentro del contenedor se ejecuta aqui namespaces + cgroups · la frontera real vistas propias de procesos, red y montajes · limites de memoria y CPU ports agujero hacia tu maquina volumes (enlace) agujero hacia tus archivos environment legible desde dentro y fuera
El aislamiento no lo rompe un fallo exótico: lo rompen las tres líneas que tú escribes para que el contenedor sea útil.
Ver el mismo diagrama como código Mermaid (editable)
flowchart TB
    P[ports] --> NS
    V[volumes de enlace] --> NS
    E[environment] --> NS
    NS[namespaces + cgroups] --> K[nucleo compartido]

7Código

Un servicio escrito con las cuatro decisiones explícitas. Cada comentario responde una de las cuatro preguntas de la sección 4:

services:
  app:
    image: nginx:1.27-alpine      # etiqueta fija, no "latest"

    # alcance: una sola red, sin salida
    networks:
      - lab_interna

    # exposicion: ningun puerto. Se llega por el proxy o no se llega.

    # datos: solo rutas del laboratorio, y de solo lectura
    volumes:
      - ./app:/usr/share/nginx/html:ro
    tmpfs:
      - /tmp                       # lo temporal no toca tu disco

    # credenciales: nada que no puedas publicar
    environment:
      APP_ENTORNO: laboratorio

    # endurecimiento barato y casi siempre olvidado
    read_only: true              # raiz del contenedor inmutable
    security_opt:
      - no-new-privileges:true     # nada de escalar por setuid
    cap_drop:
      - ALL                        # sin capacidades del nucleo

Las tres últimas claves no aparecen en casi ningún tutorial y cuestan una línea cada una. read_only convierte la raíz del contenedor en inmutable, así que un payload que quiera dejar un archivo se queda sin sitio. no-new-privileges impide que un binario con bit setuid gane privilegios. cap_drop: ALL quita las capacidades del núcleo que casi ningún servicio necesita. Ninguna de las tres arregla un fallo de aplicación; las tres encarecen mucho lo que viene después del fallo.

8Qué puede salir mal

Los errores que este curso va a explotar

Etiqueta móvil. latest hace que el laboratorio de hoy no sea el de mañana. Cuando un ejercicio deje de reproducirse no vas a poder distinguir tu error de un cambio en la imagen. Para reproducibilidad de verdad, fija el digest.

Creer que down reinicia. Sin -v, los volúmenes nombrados sobreviven. El “estado limpio” que crees tener arrastra los datos que escribiste atacando.

Montar de más. Un montaje de enlace con permiso de escritura sobre una ruta tuya convierte cualquier escritura arbitraria del contenedor en escritura en tu disco. El caso extremo es el socket de Docker, que es acceso de administrador al anfitrión disfrazado de volumen.

Usar network_mode: host para salir del paso. Elimina el aislamiento de red completo y hace que ports se ignore en silencio, con lo que la protección de 127.0.0.1: desaparece sin que nada avise.

Tratar environment como un almacén de secretos. Es legible con docker inspect, desde /proc y por cualquier proceso hijo, incluido el que abre una inyección de comandos.

9Cómo lo abusaría un atacante

Vista desde el otro lado

Supón que alguien consigue ejecutar un comando dentro de un contenedor, por la vía que sea. Lo primero que hace no es buscar un exploit exótico: hace inventario, y ese inventario son exactamente las primitivas de esta lección. env para las credenciales, mount para ver qué hay montado, y un vistazo a la red para saber a quién puede hablar.

Las variables de entorno suelen dar el mejor botín por unidad de esfuerzo. En un despliegue típico contienen la cadena de conexión a la base de datos, alguna clave de API y a veces el token de un servicio de nube. No hay que romper nada: están ahí porque alguien decidió que era la forma cómoda de configurar.

Los montajes son el segundo objetivo. Un montaje de escritura hacia una ruta del anfitrión es una vía directa: escribir en un directorio de tareas programadas, en un script que se ejecuta periódicamente o en la configuración de un servicio da ejecución fuera del contenedor sin tocar el núcleo. Y si lo montado es el socket de Docker, el trabajo se acaba ahí: se pide un contenedor privilegiado con la raíz del anfitrión dentro y ya está.

Lo tercero es la red, y es lo que más subestima la gente. Un contenedor comprometido en una red con salida sirve para exfiltrar y para atacar a terceros desde tu conexión. En una red con internal: true, ninguna de las dos cosas está disponible, y eso convierte una intrusión completa en una molestia local.

10Cómo defenderlo

Qué hacer con esto mañana
  • Audita la forma expandida, no el archivo escrito: docker compose config es la verdad y hace explícito el host_ip.
  • Etiquetas fijas, y digest cuando la reproducibilidad importe. Un entorno que no se reproduce no se puede investigar.
  • Volúmenes nombrados por defecto; montajes de enlace solo del propio laboratorio y con :ro; tmpfs para lo temporal.
  • Nunca el socket de Docker dentro de un contenedor. Si una herramienta lo pide, la respuesta correcta es cambiar de herramienta o darle un intermediario con permisos mínimos.
  • Las tres líneas baratas en cada servicio: read_only, no-new-privileges, cap_drop: ALL. Quita las que rompan algo, y anota por qué.
  • Trata environment como público. Para lo que no puede serlo, la solución es un gestor de secretos, y llega en su propia fase del curso.
  • Reinicia con down -v siempre que quieras estado limpio, y comprueba con docker volume ls que no quedó nada.

11Laboratorio

Estado: laboratorio local lab-00-base
Targethttp://localhost:8080
Autorizaciónsistema local creado por ti para entrenamiento
Todo lo que se practica en este curso se practica aquí dentro. Ninguna técnica de este material debe apuntarse a un sistema que no sea tuyo o para el que no tengas autorización escrita.

Levanta el laboratorio y recorre las tres primitivas de la lección:

cd cursos/seguridad/labs/lab-00-base docker compose up -d docker compose config | head -40

Primero, la contraseña de la base de datos. Está en claro en el compose a propósito: compruébalo desde los dos lados de la frontera.

una variable de entorno no es un secreto
docker inspect lab00-db --format '{{json .Config.Env}}' | tr ',' '\n' | grep -i pass "POSTGRES_PASSWORD=laboratorio_local" # desde fuera, sin entrar al contenedor docker compose exec db sh -c "tr '\0' '\n' < /proc/1/environ | grep -i pass" POSTGRES_PASSWORD=laboratorio_local # y desde dentro, con cualquier proceso

Segundo, los volúmenes. Comprueba que down sin -v no limpia:

docker compose exec db sh -c "touch /var/lib/postgresql/data/marca_de_ayer" docker compose down docker compose up -d docker compose exec db ls /var/lib/postgresql/data/marca_de_ayer

El archivo sigue ahí. Ese es el estado fantasma que arruina la reproducibilidad de un ejercicio. Repite el ciclo con down -v y verifica que desaparece. Cuando termines:

docker compose down -v docker volume ls

12Ejercicios

Clasifica seis montajes

Para cada uno di el tipo (volumen nombrado, montaje de enlace o tmpfs) y si te parece aceptable en un laboratorio: (1) db_datos:/var/lib/postgresql/data; (2) ./app:/usr/share/nginx/html:ro; (3) /var/run/docker.sock:/var/run/docker.sock; (4) ~/:/host; (5) /tmp como tmpfs; (6) ../../:/repo.

Mira el lado izquierdo: si empieza por una ruta, es un enlace; si es un identificador, es un volumen nombrado.
La pregunta útil para cada enlace es: si el contenedor pudiera escribir donde quisiera dentro de esa ruta, ¿qué se rompe?
Dos de los seis son inaceptables sin discusión posible, y uno de ellos ni siquiera necesita escritura para ser grave.
Solución

(1) volumen nombrado, correcto. (2) montaje de enlace de solo lectura sobre una ruta del laboratorio, correcto. (3) montaje de enlace del socket de Docker: inaceptable, equivale a acceso de administrador al anfitrión. (4) montaje de tu directorio personal con escritura: inaceptable, entrega tus claves y tu historial. (5) tmpfs, correcto y además deseable. (6) montaje del repositorio entero: mala idea, sale del directorio del laboratorio y suele arrastrar el directorio de control de versiones.

Clave: los aceptables son 1, 2 y 5. La regla que se evalúa: un montaje de enlace apunta a una ruta del laboratorio, con :ro salvo justificación escrita.

Endurece un servicio sin romperlo

Copia el lab-00-base a un directorio propio y añade al servicio app las tres líneas de endurecimiento de la sección 7 (read_only, no-new-privileges, cap_drop). Levántalo. Algo va a fallar. Diagnostica qué, arréglalo sin quitar ninguna de las tres, y explica en una frase por qué el arreglo es mejor que la marcha atrás.

Mira los logs del servicio en cuanto arranque: el error dice exactamente qué ruta no se puede escribir.
Un servidor web necesita escribir en un par de sitios muy concretos, y ninguno de ellos contiene datos que importe conservar.
Si la raíz es inmutable pero unas pocas rutas necesitan escritura, hay un tipo de montaje que resuelve justo eso sin tocar tu disco.
Solución

Con la raíz inmutable, el servidor no puede escribir sus archivos temporales ni su fichero de identificador de proceso. El arreglo no es quitar read_only, sino declarar montajes en memoria para esas rutas concretas:

    read_only: true
    tmpfs:
      - /tmp
      - /var/cache/nginx
      - /var/run

Por qué es mejor: la marcha atrás devuelve el sistema de archivos entero a escritura, mientras que esto concede exactamente las tres rutas necesarias, y además en memoria, así que nada de lo que se escriba sobrevive al reinicio.

Clave: el patrón se llama enumerar lo necesario en vez de permitir todo, y es el mismo que vas a aplicar a permisos, a reglas de firewall y a políticas de identidad durante el resto del curso.

Reproducibilidad de verdad

Investiga la diferencia entre referenciar una imagen por etiqueta y por digest. Después convierte tu copia del laboratorio a digests, documenta cómo los obtuviste, y explica qué garantía te da eso exactamente y cuál no te da. Termina respondiendo: si una imagen fijada por digest tiene una vulnerabilidad nueva, ¿en qué te ayuda haberla fijado?

Una etiqueta es un puntero móvil; un digest es una huella del contenido. Empieza por comprobar que la misma etiqueta apunta a digests distintos con el tiempo.
Puedes leer el digest de lo que ya te has descargado sin ir a ningún registro externo.
La sintaxis es imagen@sha256:..., y docker image inspect te da el valor que necesitas.
Solución

El digest identifica un contenido exacto: la misma referencia da siempre los mismos bytes, y si alguien reemplaza la imagen detrás de la etiqueta, tu despliegue no se entera porque ya no depende de la etiqueta. Lo que no te da es seguridad del contenido: fijar el digest de una imagen con un fallo garantiza que seguirás teniendo ese fallo, exactamente igual, para siempre.

En qué ayuda entonces: en que sabes qué tienes. Un inventario de digests se puede cruzar con avisos de vulnerabilidades y decidir la actualización a propósito, en un cambio revisable, en vez de que la imagen cambie sola bajo tus pies. La reproducibilidad no es lo contrario de actualizar: es lo que hace que actualizar sea una decisión y no un accidente.

Clave: la frase que se evalúa es “fijar no es parchear”. Si tu respuesta sugiere que el digest te protege de vulnerabilidades, vuelve a leer el segundo párrafo.

Red Team · saquea el entorno desde dentro

Ponte en el papel del atacante que acaba de conseguir ejecución dentro del contenedor app. Sin salir de él, y solo con lo que hay en la imagen, reúne: todas las variables de entorno, la lista de montajes con sus permisos, y la lista de vecinos alcanzables. Escribe el informe en diez líneas y marca cuál de los tres hallazgos convertirías primero en un ataque.

Todo lo que necesitas está en el sistema de archivos virtual del núcleo y en un par de comandos que trae cualquier imagen mínima.
Los montajes y sus opciones se leen sin privilegios; fíjate en cuáles dicen que son de solo lectura.
Para los vecinos, prueba a resolver los nombres de los otros servicios del compose: el DNS interno te confirma quién existe.
Solución

Entorno: env o leer /proc/1/environ. Montajes: cat /proc/mounts, donde se ve el modo de cada uno. Vecinos: resolver los nombres de servicio y probar los puertos habituales. En el lab-00-base el resultado es pobre a propósito —la app apenas tiene entorno— y el hallazgo valioso está un salto más allá: el nombre db resuelve, y la contraseña vive en el entorno de ese contenedor.

Qué convertir primero en ataque: el vecino. El entorno propio y los montajes dicen lo que ya tienes; el vecino dice a dónde puedes ir. Ese salto —de ejecución en un servicio a acceso al almacén de datos— es el movimiento lateral, y es el patrón que más veces vas a repetir en el curso.

Clave: el orden correcto es entorno, montajes, vecinos, y la prioridad es siempre lo que amplía el alcance. Si empezaste buscando exploits de núcleo, te saltaste el inventario.

Blue Team · el inventario deja rastro, ¿dónde?

Repite el saqueo del ejercicio anterior mientras observas docker compose logs -f y los eventos del propio Docker. Responde: ¿qué parte del inventario es visible para el defensor y qué parte es completamente silenciosa? Después propón dónde habría que instrumentar para que dejara de serlo, y qué coste tendría.

docker events en otra terminal te enseña qué considera Docker un acontecimiento digno de registrar.
Leer un archivo dentro de un contenedor no es una operación de Docker: es una llamada al sistema del núcleo.
Para ver llamadas al sistema hace falta instrumentación en el núcleo, no en el motor de contenedores. Busca por dónde va esa familia de herramientas.
Solución

Visible: la creación del contenedor, los arranques y paradas, y las ejecuciones lanzadas desde fuera con exec. Silencioso: absolutamente todo lo que ocurre dentro del contenedor. Leer el entorno, listar montajes y resolver nombres no genera un solo evento en Docker ni una línea en los logs de aplicación.

La instrumentación que lo hace visible vive en el núcleo, no en el motor de contenedores: recolección de llamadas al sistema con reglas de detección. El coste es real y hay que decirlo: un componente privilegiado más en cada máquina, volumen de eventos alto y una fase de ajuste larga para que las alertas no se ignoren a la semana.

Clave: la conclusión evaluable es que el motor de contenedores no es una fuente de detección de lo que pasa dentro. Guárdala para la fase de detección y respuesta.

Architecture Challenge · una plantilla de compose para toda la organización

Diseña la plantilla base que usarían todos los equipos de una empresa para levantar entornos de desarrollo. Decide qué valores impones, cuáles sugieres y cuáles dejas libres; cómo se comprueba el cumplimiento; qué haces con el equipo que necesita legítimamente una excepción; y cómo evitas que la plantilla se quede obsoleta y todo el mundo la copie mal.

Separa las decisiones en dos montones: las que nunca tienen una excepción legítima y las que la tienen a menudo.
Una plantilla que se copia se desincroniza. Piensa en mecanismos donde lo común se herede en vez de duplicarse.
Compose admite fragmentos reutilizables y combinación de varios archivos; eso cambia por completo cómo se distribuye una base común.
Solución

Una propuesta razonable: un archivo base distribuido como paquete versionado que cada equipo combina con el suyo, en vez de copiarlo. Impuestos y verificados en el pipeline: dirección obligatoria en los puertos publicados, prohibición del socket de Docker, prohibición de network_mode: host, imágenes por digest. Sugeridos con aviso pero no bloqueantes: read_only, cap_drop, límites de recursos, porque rompen servicios legítimos y un control que bloquea de más se termina desactivando entero.

Excepciones: con nombre, con fecha de caducidad y declaradas en el propio repositorio del equipo, para que se puedan contar. Una excepción que no se puede contar es una política que no existe.

Clave: no hay una única respuesta. Se evalúa que distingas qué bloquea de qué avisa, que la base se herede en vez de copiarse, y que las excepciones caduquen.

13Preguntas de comprensión

Comprobación

Pones una contraseña en environment:. ¿Dónde queda legible?

Las variables de entorno se propagan hacia abajo por diseño. Esa comodidad es exactamente lo que las inhabilita como mecanismo de secretos.

¿Cuál es la diferencia relevante, en seguridad, entre un volumen nombrado y un montaje de enlace?

Lo que montas, lo entregas. El volumen nombrado vive fuera de tu árbol de directorios; el enlace es una puerta directa a tus archivos.

Ejecutas docker compose down y vuelves a levantar. ¿En qué estado está la base de datos?

Es la causa más frecuente de un ejercicio que “ya no reproduce”: el estado del ataque anterior sigue en el volumen.

¿Qué aísla exactamente un contenedor respecto a tu máquina?

Un contenedor contiene bien los accidentes y contiene a medias a un atacante. Para lo destructivo hace falta separación de núcleo, es decir, una máquina virtual.

0 / 4

14Reto adicional

Construye el laboratorio mínimo reproducible

Escribe desde cero un docker-compose.yml con dos servicios —un servidor web y un cliente— que cumpla todo esto a la vez: ningún puerto publicado, red interna, imágenes por digest, raíz de solo lectura, sin capacidades, y una variable de entorno que el cliente lea para saber a quién consultar. Añade un LEEME.txt con las comprobaciones que demuestran cada propiedad.

Empieza por el compose más pequeño que funcione y añade una restricción cada vez, comprobando después de cada una.
Sin puertos publicados solo se entra con exec; eso es una propiedad del diseño, no una limitación que haya que sortear.
La raíz inmutable va a chocar con las rutas temporales del servidor; ya viste en el ejercicio medio cómo se resuelve sin ceder.
Solución

El orden que funciona: primero los dos servicios en una red con internal: true y sin ports; después cap_drop: ALL y no-new-privileges, que rara vez rompen nada; después read_only con los tmpfs necesarios; y al final el paso a digests, que es mecánico. El cliente resuelve al servidor por nombre de servicio, tomado de una variable de entorno.

El LEEME.txt es la mitad del ejercicio y es donde se ve si lo entendiste: una comprobación por propiedad, cada una con el resultado que se espera. Sin puertos, la comprobación es que la lista de puertos sale vacía; sin salida, que una petición al exterior falla; raíz inmutable, que un touch en la raíz da error.

Clave: el entregable que importa es el conjunto de comprobaciones. Un laboratorio cuyas propiedades no se pueden verificar con un comando es una declaración de intenciones, no un entorno.

15Resumen

Puntos clave
  • Un contenedor es un proceso con vistas propias (namespace) y límites de consumo (cgroup). Comparte tu núcleo, y eso marca el límite de lo que puede contener.
  • Las tres primitivas que perforan el aislamiento son las que tú escribes: ports, montajes de enlace y environment.
  • network_mode: host elimina la red propia y hace que ports se ignore en silencio: adiós a la protección de 127.0.0.1:.
  • Volumen nombrado por defecto, enlace solo del laboratorio y con :ro, tmpfs para lo temporal. El socket de Docker, nunca.
  • Una variable de entorno se lee desde fuera, desde /proc y por herencia. Es configuración, no un secreto.
  • down sin -v conserva los volúmenes: el estado fantasma es la primera sospecha cuando un ejercicio deja de reproducirse.
  • Audita docker compose config, no el archivo escrito: la forma expandida hace explícito lo que la corta esconde.
¿Qué comparte un contenedor con tu máquina, pase lo que pase?
El núcleo. Por eso contiene bien los accidentes y solo a medias a un atacante, y por eso lo destructivo se practica en una máquina virtual.
Tres formas de leer una variable de entorno de un contenedor.
docker inspect desde el anfitrión, /proc/<pid>/environ desde dentro, y por herencia en cualquier proceso hijo.
¿Por qué montar /var/run/docker.sock es tan grave?
Quien habla con ese socket puede crear un contenedor privilegiado que monte la raíz del anfitrión. Es acceso de administrador disfrazado de volumen.
¿Qué diferencia hay entre el archivo .env y env_file?
.env rellena los ${huecos} del propio compose; env_file define variables dentro del contenedor. Confundirlos da entornos que solo funcionan en una máquina.

16Mis notas

Cierre de la lección

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.