Fundamentos · Lección 03

Defensa en profundidad, mínimo privilegio y fallo seguro

Los tres principios de esta lección se repiten tanto que se han vuelto ruido de fondo. Todo el mundo asiente cuando se dice “defensa en profundidad”, y muy pocos sistemas la tienen: tienen varios controles que caen a la vez por la misma causa, que no es lo mismo. Aquí vas a aprender la pregunta que distingue una capa real de una capa decorativa, a medir el privilegio de un proceso en vez de suponerlo, y a revisar el camino de error de tu propio código, que es donde vive el fallo abierto más caro de la industria. Todo sobre el lab-00-base, quitándole cosas hasta que se rompa.

Al terminar sabrás

  • Distinguir defensa en profundidad de controles duplicados que fallan por la misma causa.
  • Aplicar mínimo privilegio en sus tres ejes: identidad, alcance y tiempo.
  • Medir el privilegio real de un contenedor y reducirlo hasta que la aplicación empiece a quejarse.
  • Escribir un camino de error que deniegue, y reconocer los tres patrones de fallo abierto más comunes.
  • Explicar cuándo debe fallar abierto un sistema, y por qué esa decisión se escribe, no se hereda.
  • Convertir “secure defaults” en algo comprobable: qué pasa si nadie configura nada.

1Por qué importa

En f01-superficie-confianza dibujaste dónde están las fronteras. La consecuencia incómoda de ese dibujo es que, tarde o temprano, alguna se va a cruzar. No porque el equipo sea malo: porque una frontera es un trozo de código o de configuración y todo el código tiene defectos. La pregunta útil deja de ser “¿se puede cruzar?” y pasa a ser “¿qué pasa el minuto después de que se cruce?”.

Esa pregunta es exactamente la que separa un incidente de una anécdota. Una inyección de comandos en un proceso que corre como root, sin restricciones y con credenciales de despliegue en el entorno, es una pérdida total. La misma inyección en un proceso sin capacidades, con el sistema de archivos en solo lectura, sin credenciales y que solo alcanza a un servicio, es un ticket. El exploit es idéntico. Lo que cambia es todo lo que había detrás.

Los tres principios de esta lección son la respuesta a ese minuto después. Defensa en profundidad asegura que quede algo detrás. Mínimo privilegio reduce lo que ese “algo” concede si también cae. Fallo seguro impide que la forma más barata de saltarse un control sea provocarle un error. Y hay un cuarto que los sostiene: secure defaults, que es lo que decide el resultado cuando nadie configuró nada, que es el estado real de la mayoría de los sistemas.

El objetivo real

Estás aprendiendo a hacer que el trabajo del atacante no escale. Un sistema bien construido no es el que nadie puede tocar: es aquel en el que el esfuerzo de cada paso siguiente es igual o mayor que el del primero. Cuando el segundo paso es gratis, no tienes capas, tienes una capa con adornos.

2Conceptos

Los cinco términos
  • Defensa en profundidad: varios controles independientes en serie, de modo que la caída de uno no implique la de los demás.
  • Mínimo privilegio: cada identidad tiene exactamente los permisos que necesita, sobre exactamente los recursos que necesita, durante exactamente el tiempo que los necesita.
  • Secure defaults: la configuración de fábrica es la restrictiva. Abrir requiere una acción deliberada; cerrar no requiere ninguna.
  • Fallo seguro: cuando algo va mal —error, tiempo agotado, dato ausente— el resultado por defecto es denegar.
  • Security by design: las tres anteriores se deciden mientras se diseña, porque después solo se pueden añadir por encima y eso siempre sale peor y más caro.

3Explicación profunda

La frase “defensa en profundidad” se usa casi siempre mal. Poner dos comprobaciones del mismo tipo no es profundidad: es redundancia. La prueba que distingue una cosa de la otra cabe en una pregunta: ¿existe una causa única capaz de tumbar las dos a la vez? Si la respuesta es sí, tienes una capa contada dos veces.

Los ejemplos son incómodos porque son frecuentes. Autenticación en el proxy y autenticación en la aplicación, ambas resolviendo el token contra el mismo servicio de identidad: si ese servicio se cae o se compromete, caen las dos. Cifrado en tránsito y cifrado en reposo gestionados por el mismo sistema de claves: una clave filtrada anula ambos. Un cortafuegos de red y una lista de direcciones permitidas en la aplicación, las dos alimentadas por el mismo encabezado reenviado que ya viste que puede falsear el cliente. En los tres casos, el segundo control aporta menos de lo que su presencia sugiere, y la sugerencia es la parte peligrosa: el equipo se relaja.

Las capas que sí funcionan tienden a ser de naturaleza distinta: una de red, una de identidad, una de datos, una de ejecución. En el laboratorio, que la red interna no tenga salida y que un contenedor no tenga capacidades del kernel son controles que no comparten ni implementación ni administrador ni modo de fallo. Ahí hay profundidad de verdad.

El mínimo privilegio se enseña como un eje y son tres. El primero es la identidad: quién eres. El segundo es el alcance: sobre qué objetos, y con qué verbos. El tercero, el que casi todo el mundo olvida, es el tiempo: durante cuánto. Un permiso de administrador concedido “para el despliegue del jueves” y nunca retirado es el defecto más común de cualquier auditoría de accesos, y no se detecta mirando la lista de permisos —donde todo parece justificado— sino mirando la fecha en que se concedieron.

Aplicado a procesos, el mínimo privilegio tiene una versión muy concreta y muy medible en contenedores. Un contenedor por defecto no es una caja sellada: arranca como root dentro de su namespace, con un conjunto de capacidades del kernel que le permiten cambiar propietarios de ficheros, abrir sockets en crudo y algunas cosas más, y con el sistema de archivos escribible. Cada una de esas concesiones es un permiso que alguien te dio sin que lo pidieras. La forma correcta de decidir cuáles necesitas es empírica: quítalas todas y devuelve solo las que la aplicación reclame al romperse.

El fallo seguro es el principio que más dinero cuesta cuando se ignora, porque el camino de error es el camino menos probado de cualquier sistema. Tiene tres patrones típicos. El primero: una excepción capturada de forma amplia cuyo catch devuelve permiso concedido para “no romper la experiencia”. El segundo: un valor por defecto permisivo cuando un dato falta —el clásico rol = usuario.rol o "admin" escrito al revés por prisa—. El tercero, el más sutil: una comprobación que solo se aplica a lo que coincide con un patrón, de modo que todo lo que no coincide pasa sin revisar. Los tres tienen la misma forma: la ausencia de una respuesta se interpreta como un sí.

Aquí toca el contraejemplo honesto, porque “fallo seguro siempre” es falso. Una puerta de cámara acorazada debe quedarse cerrada si se va la luz; una puerta de emergencia debe abrirse. Cuando lo que está en juego es la vida o la continuidad de algo crítico, fallar abierto puede ser la decisión correcta. Lo que no es aceptable es que esa decisión se tome sola, por descuido, dentro de un catch. La regla es: cerrado por defecto; abierto solo donde alguien lo escribió, lo firmó y lo puso a producir una alerta.

4Ejemplo sencillo

# Un banco. Cuatro capas, y ninguna comparte modo de fallo.

capa 1   puerta con horario           control de acceso fisico
capa 2   camara y grabacion           deteccion, no prevencion
capa 3   camara acorazada con clave   control de acceso al activo
capa 4   apertura con retardo         gana tiempo aunque caigan 1, 2 y 3

# Minimo privilegio, en el mismo banco

cajero    abre su caja, no la camara            alcance
director  abre la camara, en horario de oficina alcance + tiempo
tecnico   entra acompanado, un dia concreto     identidad + tiempo

# Fallo seguro y su contraejemplo, puerta contra puerta

se va la luz  ->  camara acorazada  se queda CERRADA   fallo seguro
se va la luz  ->  puerta de incendios se ABRE          fallo abierto,
                                                       y es lo correcto
Las dos puertas están en el mismo edificio, se quedan sin corriente a la vez y hacen lo contrario. La diferencia no es técnica: es que alguien decidió por escrito qué se pierde en cada caso.

Fíjate en la capa 2. Una cámara no impide ningún robo; solo lo registra. Y sin embargo es una capa legítima, porque cambia el cálculo del atacante y porque es la única que sigue funcionando cuando las otras ya han fallado. Las capas de detección cuentan, siempre que alguien mire lo que graban.

5Ejemplo técnico

El servicio app del lab-00-base es un nginx que sirve un fichero estático montado en solo lectura. Es lo más inofensivo que hay en el laboratorio, y aun así arranca con más privilegio del que necesita. Mide primero, antes de opinar:

medir el privilegio de partida
docker compose exec app id uid=0(root) gid=0(root) groups=0(root) docker compose exec app sh -c "grep CapEff /proc/1/status" CapEff: 00000000a80425fb docker compose exec app sh -c "touch /prueba && echo escribible" escribible

Tres hechos: el proceso principal es root, tiene un conjunto de capacidades del kernel que nadie pidió, y puede escribir en cualquier parte de su sistema de archivos. Ninguno de los tres hace falta para servir un HTML. Cada uno es un multiplicador del impacto de cualquier fallo futuro en esa imagen.

Concesión por defectoPara qué se usa aquíQué habilita si algo falla
Proceso principal como rootSolo para abrir el puerto 80 y cambiar a otro usuarioModificar cualquier fichero de la imagen, incluido el binario
Conjunto de capacidades por defectoCambiar propietario de las cachés y ceder privilegiosSockets en crudo, cambio de propietarios, escaneos desde dentro
Sistema de archivos escribibleNada: el contenido va montado en solo lecturaPersistir un payload que sobreviva a un reinicio del proceso
Escalada por setuid permitidaNadaUn binario con el bit activo convierte una ejecución cualquiera en root

Ahora la otra mitad, el fallo seguro, en el fichero conf/proxy.conf. La directiva location / reenvía absolutamente todo a la aplicación. Es un default permisivo: la regla de fábrica dice “sí” y para decir “no” hay que escribir algo. La versión con secure defaults invierte la carga:

# permisivo por defecto: todo pasa, y lo que no quieras hay que ir negandolo
location / {
    proxy_pass http://app/;
}

# restrictivo por defecto: nada pasa salvo lo declarado
location / {
    return 404;                       # nota: 404, no 403
}
location = / {
    proxy_pass http://app/;
}
location ^~ /estatico/ {
    proxy_pass http://app/estatico/;
}

El detalle del 404 frente al 403 importa y es el mismo que aparecía en el registro de riesgos de la primera lección: un 403 confirma que el recurso existe. Denegar por defecto y además no confirmar la existencia son dos decisiones distintas, y las dos son gratis en el momento de escribir la configuración.

6Diagrama

Cuatro capas en serie: las dos primeras caen porque comparten una causa común, la tercera detiene el ataque por ser de otra naturaleza y la cuarta limita lo que se consigue 1 · red interna sin salida 2 · lista de origen por encabezado 3 · autorizacion por objeto 4 · sin capacidades, solo lectura ataque se detiene en la 3 causa comun el encabezado reenviado decide 1 y 2 a la vez 3 y 4 no comparten implementacion, dueno ni modo de fallo cuatro cajas apiladas no son cuatro capas: son dos
Lo que convierte una pila de controles en defensa en profundidad no es el número, es la ausencia de causa común. Dos de estas cuatro cajas caen con la misma acción del atacante.
Ver el mismo diagrama como código Mermaid (editable)
flowchart TD
    AT[ataque] --> C1[1 red interna sin salida]
    C1 --> C2[2 lista de origen por encabezado]
    C2 --> C3[3 autorizacion por objeto]
    C3 --> C4[4 sin capacidades solo lectura]
    CC{{causa comun: encabezado reenviado}} --> C1
    CC --> C2

7Código

Primero el fallo abierto, en su forma más común y más inocente. Este código se escribe con buena intención, pasa la revisión y funciona perfectamente durante meses, hasta el día en que el servicio de permisos tarda de más:

# MAL: el camino de error concede el acceso
def puede_ver_factura(usuario, factura_id):
    try:
        permisos = servicio_permisos.consultar(usuario)
    except Exception:                # captura amplia: tapa hasta los fallos ajenos
        return True                  # "para no romper la experiencia"
    return factura_id in permisos.facturas

La versión con fallo seguro cambia tres cosas, y ninguna es el return:

# BIEN: deniega, distingue el porque, y hace ruido
def puede_ver_factura(usuario, factura_id):
    try:
        permisos = servicio_permisos.consultar(usuario, timeout=2)
    except TiempoAgotado:            #  1. excepcion concreta, no Exception
        metricas.incr("autz.indisponible")  #  2. un fallo silencioso no es un fallo seguro
        raise ServicioNoDisponible()  #  3. 503, no 200 con contenido vacio
    return factura_id in permisos.facturas

El punto 3 es el que más discusión genera. Devolver una lista vacía en vez de un error “deniega”, sí, pero deniega en silencio y con aspecto de respuesta correcta: el usuario ve una pantalla sin facturas y llama a soporte, y nadie relaciona eso con el servicio de permisos. Denegar sin decir que has denegado es una avería, no un control.

Y ahora el mínimo privilegio, en el único formato que aquí se puede comprobar ejecutando. Este fragmento sustituye al servicio app del laboratorio:

# docker-compose.yml — servicio app con el privilegio recortado
  app:
    image: nginx:1.27-alpine
    volumes:
      - ./app:/usr/share/nginx/html:ro
    networks:
      - lab_interna
    read_only: true              # nada persiste dentro del contenedor
    tmpfs:                        # salvo lo que nginx necesita en memoria
      - /var/cache/nginx
      - /var/run
      - /tmp
    cap_drop:
      - ALL                       # se quitan TODAS y se devuelven las que rompan
    cap_add:
      - CHOWN
      - SETUID
      - SETGID
      - NET_BIND_SERVICE          # abrir el 80, que es puerto privilegiado
    security_opt:
      - no-new-privileges:true    # ningun setuid podra escalar desde aqui
    restart: unless-stopped

Ese bloque no es una receta para copiar sin pensar: la lista de cap_add depende de la imagen. Lo que sí se puede copiar es el método: empezar por ALL en cap_drop, arrancar, leer el error, devolver una capacidad, repetir. Cinco minutos de bucle y tienes un privilegio justificado línea por línea, que es algo que casi ningún despliegue puede enseñar.

8Qué puede salir mal

Los cuatro errores clásicos

Capas con causa común. Cuatro controles que dependen del mismo servicio de identidad, del mismo encabezado o de la misma clave son un control con cuatro nombres. Antes de contar capas, escribe al lado de cada una de qué depende.

Mínimo privilegio sin eje temporal. Los permisos se conceden con justificación y se retiran sin ninguna, así que nadie los retira. Un acceso elevado sin fecha de caducidad es un acceso permanente disfrazado de excepción.

El fallo abierto por disponibilidad. “Si el servicio de permisos no responde, dejamos pasar para no cortar el negocio” es una decisión legítima si está escrita, tiene dueño y genera una alerta. Escondida en un catch, es una puerta abierta que además se puede provocar: basta con hacer que ese servicio tarde.

Usar la profundidad como excusa. “No pasa nada porque hay otra capa detrás” es la frase con la que se cierran hallazgos que deberían arreglarse. La profundidad existe para el fallo que no conoces; gastarla en el que sí conoces es quedarse sin margen justo cuando hace falta.

9Cómo lo abusaría un atacante

Vista desde el otro lado

Quien ataca no intenta romper cuatro capas: busca la causa común que derriba varias con una sola acción. Por eso los objetivos preferidos son los sistemas transversales —el proveedor de identidad, el gestor de secretos, el pipeline, el agente que corre en todas las máquinas—. Un control que está en todas partes es también un fallo que está en todas partes.

La segunda táctica es forzar el camino de error en vez de superar la comprobación. Si autorizar requiere hablar con un servicio, agotarlo o hacerlo lento es más barato que falsificar un token. La pregunta que se hace un atacante ante cualquier control es literalmente “¿qué pasa si esto no responde?”, y demasiadas veces la respuesta la escribió alguien con prisa a las siete de la tarde.

Y la tercera: usar el privilegio sobrante para hacer permanente un acceso temporal. Una ejecución de comandos dentro de un contenedor efímero se pierde en el siguiente despliegue… salvo que el sistema de archivos sea escribible, en cuyo caso se deja algo escrito; o salvo que haya capacidades de sobra, en cuyo caso se pasa al host o a la red. Cada privilegio que no quitaste es un método de persistencia que no tuvo que inventar.

10Cómo defenderlo

Qué hacer con esto mañana
  • Escribe al lado de cada control de qué depende. Dos controles con la misma dependencia cuentan como uno solo.
  • Recorta el privilegio por método, no por copia: quita todo, arranca, lee el error, devuelve lo mínimo. Deja el bucle documentado en el compose.
  • Pon fecha de caducidad a todo permiso elevado. Si el sistema no soporta caducidad, el recordatorio de retirada es parte del ticket de concesión.
  • Audita los catch que rodean decisiones de seguridad. Excepción concreta, denegación, métrica y error visible: los cuatro, o no es fallo seguro.
  • Haz que el valor por defecto sea el restrictivo, incluyendo el de configuración ausente. Un sistema sin configurar debe ser inútil, no permisivo.
  • Prueba el camino de error en los tests: un test que simule el servicio de permisos caído y exija un 503 vale más que tres tests del camino feliz.

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.

El mismo laboratorio de siempre. Aquí se usa al revés que en las lecciones anteriores: en vez de enumerar lo que hay, vas a quitar cosas hasta que algo se queje.

cd cursos/seguridad/labs/lab-00-base docker compose up -d docker compose exec atacante sh -c "id; grep CapEff /proc/1/status"

Ese es el privilegio de partida de un contenedor cualquiera. Comprueba qué habilita en la práctica, sin cambiar nada todavía:

qué permite el privilegio por defecto
docker compose exec atacante sh -c "nmap -sS -p 5432 db" PORT STATE SERVICE 5432/tcp open postgresql # el escaneo a medio abrir necesita sockets en crudo: hay capacidad de sobra docker compose exec atacante sh -c "touch /persistente && echo puedo escribir" puedo escribir # y el sistema de archivos admite dejar cosas puestas

Ahora recorta. Edita el servicio app del compose con el bloque de la sección de código, levanta de nuevo y comprueba que la aplicación sigue sirviendo exactamente igual con el privilegio recortado:

docker compose up -d --force-recreate app docker compose exec app sh -c "touch /prueba || echo solo lectura, correcto" curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/

Si el último comando devuelve 200, acabas de quitar cuatro privilegios sin coste funcional ninguno. Cuando termines, revierte el compose y destruye el laboratorio:

docker compose down -v

12Ejercicios

¿Cuántas capas hay de verdad?

Un sistema declara cinco controles: (1) cortafuegos que solo permite el 443; (2) proxy que valida el token contra el proveedor de identidad; (3) la aplicación que vuelve a validar el mismo token contra el mismo proveedor; (4) cifrado en tránsito con certificados emitidos por la autoridad interna; (5) contenedores sin capacidades y en solo lectura. Di cuántas capas independientes hay realmente y qué causa común agrupa a las que no lo son.

No cuentes controles: escribe debajo de cada uno de qué depende para funcionar, y agrupa los que repitan dependencia.
Dos de los cinco dependen literalmente del mismo servicio externo, y caen juntos si ese servicio se cae o se compromete.
El control (5) es el único que sigue haciendo algo cuando todos los demás han fallado, porque no depende de ninguna decisión en tiempo de petición.
Solución

Tres capas independientes: red (1), identidad (2 y 3 juntas) y ejecución (5). El control (4) protege una propiedad distinta —confidencialidad en tránsito— y no aporta profundidad frente a un atacante autenticado, aunque sí frente a uno que escucha la red. Los controles (2) y (3) comparten proveedor de identidad: son una sola capa mirada dos veces.

Clave: 3, no 5. Si tu respuesta fue 5, el error no es de cuenta: es que estabas contando cajas en vez de dependencias.

Recorta el privilegio hasta que se rompa

Aplica al servicio app del lab-00-base el bloque de cap_drop, read_only y tmpfs de la lección, pero sin ningún cap_add. Levanta, mira los logs, y ve devolviendo capacidades de una en una hasta que sirva el HTML. Documenta la lista final con una línea por capacidad explicando qué se rompía sin ella. Después revierte el compose.

Con read_only: true nginx falla antes de arrancar si no le das memoria temporal donde escribir; los logs dicen exactamente qué ruta no pudo abrir.
Piensa en qué hace el proceso principal al arrancar: abre un puerto por debajo del 1024 y luego cambia de usuario para los procesos de trabajo. Cada una de esas dos acciones necesita su propia capacidad.
Empieza con cap_add: [NET_BIND_SERVICE], levanta y lee el error siguiente. El bucle es leer-añadir-repetir, no adivinar la lista entera.
Solución

Con la imagen oficial de nginx sirviendo en el puerto 80, la lista mínima habitual es NET_BIND_SERVICE para abrir el puerto privilegiado, SETUID y SETGID para que el proceso principal ceda privilegios a los de trabajo, y CHOWN para ajustar la propiedad de los directorios temporales al arrancar. Todo lo demás sobra.

El resultado interesante no es la lista: es que existe una alternativa que hace innecesarias tres de las cuatro. Si la imagen sirviera en un puerto por encima del 1024 y arrancara ya como usuario sin privilegios, bastaría cap_drop: [ALL] a secas. Recortar capacidades es bueno; no necesitarlas es mejor, y esa decisión se toma al elegir la imagen.

Clave: el entregable es la tabla capacidad → qué se rompía sin ella. Una lista copiada de internet sin haber visto el error correspondiente no vale: es exactamente el hábito que esta lección intenta romper.

Qué añade exactamente no-new-privileges

Investiga fuera de esta lección: ¿qué hace la marca del kernel que activa no-new-privileges, y en qué se diferencia de haber quitado todas las capacidades? Explica con un ejemplo concreto un caso en el que un contenedor sin capacidades pueda aun así ganar privilegio, y por qué esa marca lo impide. Termina diciendo si esa marca sustituye o complementa a cap_drop: [ALL], y por qué.

Las capacidades limitan lo que el proceso puede hacer ahora. La marca limita lo que puede llegar a obtener al ejecutar otro programa.
El mecanismo clásico para ganar privilegio ejecutando un binario existe desde hace décadas y se activa con un bit en los permisos del fichero. Busca cómo interactúa con esa marca.
Busca en la imagen del laboratorio qué binarios tienen ese bit activo: docker compose exec app sh -c "find / -perm -4000 -type f 2>/dev/null".
Solución

La marca hace que ninguna ejecución posterior pueda otorgar más privilegio del que ya tiene el proceso: los bits que normalmente elevan al ejecutar un binario dejan de tener efecto, para ese proceso y para todos sus hijos, de forma irreversible. Es distinto de quitar capacidades: sin capacidades, el proceso no puede hacer ciertas cosas, pero si consigue ejecutar un binario con el bit de elevación activo, el nuevo proceso arranca con el privilegio del propietario del fichero, típicamente root, y recupera lo que le quitaste.

Por eso complementa y no sustituye. cap_drop: [ALL] reduce el privilegio actual; no-new-privileges impide recuperarlo. Van juntas, y ninguna de las dos evita que la imagen traiga binarios con ese bit que no debería traer: quitarlos de la imagen es la tercera acción, y la que de verdad reduce superficie.

Clave: la respuesta correcta distingue “no puede hacerlo” de “no puede volver a obtenerlo”. Si tu explicación no incluye un mecanismo concreto de recuperación de privilegio, aún no has tocado el punto.

Red Team · demuestra qué capa te está frenando

Dentro del lab-00-base, y solo dentro de él: ejecuta desde atacante un escaneo a medio abrir contra db y anota que funciona. Ahora arranca ese contenedor sin la capacidad que ese tipo de escaneo necesita y repítelo. Demuestra con comandos que (a) el escaneo a medio abrir ya no funciona y (b) el escaneo por conexión completa sí, y explica qué te dice eso sobre el valor real de esa capa.

Los dos modos de escaneo se diferencian en si el paquete lo construye el programa o lo construye el sistema al abrir una conexión normal.
La capacidad que permite construir paquetes a mano es la de sockets en crudo. Quítala con cap_drop en el servicio atacante.
El escaneo por conexión completa se pide con -sT, y no necesita ningún privilegio especial porque solo abre conexiones como cualquier programa.
Solución

Con cap_drop: [NET_RAW] en el servicio atacante, el escaneo a medio abrir falla y nmap avisa de que necesita privilegios; el escaneo con -sT devuelve exactamente el mismo resultado, porque solo abre conexiones TCP normales. También deja de funcionar ping, por la misma razón.

Lo que eso dice del control es lo importante: quitar esa capacidad no impide descubrir la base de datos, solo obliga a usar una técnica más ruidosa y más lenta. Es una capa legítima —encarece y hace visible— pero no es una frontera. Confundir “le he quitado una herramienta” con “le he cerrado el paso” es la forma más común de sobrevalorar un hardening.

Clave: revierte el cap_drop del atacante al terminar. El entregable son las dos salidas de nmap, la que falla y la que no, más la frase que explica por qué esto no es una frontera.

Blue Team · audita el camino de error del proxy

Para el servicio app con el laboratorio levantado y pide una página a través del proxy. Observa qué devuelve y qué queda registrado. Responde: ¿el comportamiento del proxy cuando su destino no responde es fallo seguro o fallo abierto? ¿Y el del endpoint /salud, que tiene el registro desactivado? Propón el cambio mínimo para que ambos casos dejen rastro suficiente para investigar.

Para el primero, docker compose stop app y luego una petición normal al proxy. Mira el código de estado y el registro de errores por separado del de accesos.
Un endpoint de salud que responde 200 aunque su dependencia esté caída está mintiendo a quien lo consulta, y además no lo cuenta a nadie porque no registra.
El cambio mínimo tiene dos mitades: que /salud refleje el estado de la dependencia, y que deje de tener el registro apagado o al menos registre los fallos.
Solución

Con app parado, el proxy responde 502 y deja una línea en el registro de errores: eso es fallo seguro, porque el error no sirve contenido ni salta ninguna comprobación. El /salud es el caso interesante: sigue respondiendo 200 proxy ok con la aplicación caída, y con access_log off no queda constancia de quién lo consultó ni cuántas veces. No es un fallo abierto de autorización, pero sí un punto ciego: un atacante puede sondearlo indefinidamente sin aparecer en ningún sitio.

Cambio mínimo: que el endpoint de salud consulte de verdad a su dependencia antes de responder, y que el registro se apague solo para las respuestas correctas, nunca para los errores. Un punto ciego se justifica por volumen; un punto ciego total no se justifica nunca.

Clave: el entregable son las dos clasificaciones —502 seguro, /salud ciego— y la propuesta concreta. Arranca app otra vez al terminar.

Architecture Challenge · el sistema de autorización que nunca falla abierto

Diseña la autorización de un servicio que consulta permisos a un sistema externo. Enumera todos sus modos de fallo —tiempo agotado, respuesta malformada, caché expirada, permiso desconocido, endpoint nuevo que nadie protegió— y decide para cada uno qué se devuelve, qué se registra y qué se alerta. Define además la única excepción en la que aceptarías fallar abierto, con su mecanismo de aprobación y de caducidad.

El modo de fallo más peligroso no es que el sistema externo se caiga: es el endpoint nuevo al que nadie le puso comprobación. Piensa cómo se deniega por omisión.
Un permiso desconocido y un permiso denegado no son lo mismo, y tratarlos igual es correcto para la seguridad y horrible para la depuración. Separa la respuesta del registro.
Si decides usar caché para sobrevivir a caídas, la pregunta clave es cuánto tiempo aceptas servir una decisión vieja, y qué pasa exactamente en el segundo siguiente.
Solución

Una propuesta razonable: denegación por omisión en el enrutador, de forma que un endpoint sin declaración explícita de permiso devuelva error de configuración en el arranque, no en tiempo de petición —así el fallo aparece en el despliegue y no en producción—. Tiempo agotado y respuesta malformada: denegar con 503, métrica y alerta por tasa, nunca por evento suelto. Permiso desconocido: denegar con 404 hacia fuera y un registro detallado hacia dentro. Caché: permitida con una ventana corta y explícita, con la regla de que una decisión servida desde caché expirada se cuenta aparte.

La excepción para fallar abierto, si existe, se limita a un conjunto de operaciones de solo lectura sin datos sensibles, se activa con una marca que caduca sola en horas, requiere dos personas y genera un aviso continuo mientras está activa. Lo que la hace aceptable no es la lógica: es que sea visible y que se apague sin que nadie tenga que acordarse.

Clave: no hay una única respuesta. Se evalúa que hayas incluido el modo de fallo “endpoint sin proteger”, que separes respuesta de registro, y que la excepción tenga caducidad automática en vez de un recordatorio en el calendario de alguien.

13Preguntas de comprensión

Comprobación

Pones autenticación en el proxy y también en la aplicación. ¿Es defensa en profundidad?

Si ambas resuelven el mismo token contra el mismo proveedor de identidad, son una sola capa mirada dos veces.

Una función de autorización captura la excepción del servicio de permisos y devuelve “permitido”. ¿Cómo se llama eso?

Y además es provocable: agotar o ralentizar ese servicio sale más barato que falsificar un token.

¿Qué hace cap_drop: [ALL] en un servicio de un compose?

Cambiar de usuario es user: y el sistema de archivos es read_only:. Son tres controles distintos y se ponen los tres.

¿Cuándo es aceptable que un control falle abierto?

La puerta de emergencia se abre al irse la luz y eso está bien. Lo que no vale es que lo decida un catch a las siete de la tarde.

0 / 4

14Reto adicional

Presupuesto de capas: elige dos de cinco

Tienes una aplicación web con base de datos y tiempo para implementar dos controles este trimestre. Los candidatos: (1) autenticación multifactor para el panel de administración; (2) contenedores sin capacidades y en solo lectura; (3) segmentación de red entre aplicación y base de datos; (4) registro de auditoría con identidad y correlación; (5) revisión obligatoria de dos personas para cambios en producción. Elige dos, justifica por independencia y por lo que cubren, y di explícitamente qué escenario queda sin cubrir con tu elección.

Ordena primero por qué escenario cubre cada uno: acceso inicial, movimiento lateral, persistencia, detección, cambio malicioso. Verás que hay solapes.
Elegir dos que cubran el mismo escenario es el error del ejercicio. Elegir dos que dependan del mismo sistema es el otro.
Fíjate en que (4) no previene nada. Aun así puede ser la elección correcta, y la justificación de por qué es la parte interesante de la respuesta.
Solución

Una elección defendible: (1) y (4). El multifactor ataca el escenario más probable de acceso inicial —credenciales robadas o reutilizadas— y el registro de auditoría es la única de las cinco que sigue sirviendo cuando cualquiera de las otras falla, porque no previene: cuenta. Son independientes en implementación, en dueño y en modo de fallo.

Lo que queda descubierto, y hay que decirlo: el movimiento lateral hacia la base de datos una vez dentro de la aplicación, que solo cubre (3), y la persistencia dentro del contenedor, que solo cubre (2). Una respuesta distinta —(2) y (3), por ejemplo— también es defendible si el modelo de amenaza pone el acceso inicial como inevitable; lo que no es defendible es elegir sin nombrar el hueco.

Clave: no se evalúa qué dos elegiste, sino que las dos sean independientes y que hayas escrito el escenario que dejas descubierto. Una elección sin hueco declarado es una elección que no se ha pensado.

15Resumen

Puntos clave
  • Defensa en profundidad no es apilar controles: es que no exista una causa única capaz de tumbar varios a la vez.
  • Las capas que funcionan son de naturaleza distinta: red, identidad, datos, ejecución. Las que se repiten comparten modo de fallo.
  • Mínimo privilegio tiene tres ejes: identidad, alcance y tiempo. El tercero es el que nadie audita y el que siempre está roto.
  • El privilegio de un proceso se mide, no se supone: usuario, capacidades y escritura son tres comandos.
  • Se recorta por método: quitar todo, arrancar, leer el error, devolver lo mínimo. Una lista copiada no está justificada.
  • Fallo seguro significa que la ausencia de respuesta se interpreta como no. El camino de error es el menos probado y por eso es el más barato de forzar.
  • Fallar abierto es a veces correcto, pero solo si está escrito, tiene dueño, hace ruido y caduca solo.
¿Qué pregunta distingue una capa real de una capa decorativa?
“¿Existe una causa única capaz de tumbar las dos a la vez?”. Si la hay, es un control con dos nombres, no dos capas.
¿Cuáles son los tres ejes del mínimo privilegio?
Identidad, alcance y tiempo. El eje temporal es el que convierte una excepción del jueves en un permiso permanente.
¿Cómo se decide qué capacidades necesita un contenedor?
Quitándolas todas, arrancando, leyendo el error y devolviendo solo la que reclama. Empíricamente, nunca copiando una lista.
¿Qué forma tienen los tres patrones típicos de fallo abierto?
La misma: la ausencia de una respuesta se interpreta como un sí. Excepción capturada, valor por defecto permisivo, o comprobación que solo se aplica a lo que coincide con un patrón.

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.