1Por qué importa
Reducir privilegio con capabilities es necesario, pero no basta. Un proceso con pocas capabilities todavía ve todos los demás procesos, puede consumir toda la memoria de la máquina, puede invocar cualquier syscall del kernel y puede tocar cualquier archivo que sus permisos le permitan. Cada una de esas cuatro cosas es una superficie distinta, y para cada una hay un mecanismo de confinamiento independiente. Confinar de verdad es usarlos juntos.
Esta es la defensa en profundidad aplicada a un solo proceso. Un fallo en el servicio no debe poder ver los secretos de otro proceso (namespaces), ni tumbar la máquina agotando memoria (cgroups), ni invocar la syscall que necesita un exploit del kernel (seccomp), ni leer un archivo que su política no contempla (AppArmor o SELinux). Ninguno de estos mecanismos evita el fallo de la aplicación; todos reducen lo que ese fallo alcanza. Y todos existen ya en el kernel: son exactamente las piezas con las que un contenedor está construido, aunque casi nadie las mira una a una.
No estás aprendiendo cómo se hace un contenedor por curiosidad. Estás aprendiendo los cuatro ejes en los que se confina un proceso, para poder decidir, ante un servicio, cuánto de cada uno necesita y cuánto le sobra.
2Conceptos
- namespace: aísla la vista que un proceso tiene de un recurso del sistema.
- PID namespace: el proceso solo ve un subconjunto de procesos, con su propia numeración.
- network namespace: su propia pila de red, interfaces y puertos.
- mount namespace: su propia vista del árbol de archivos.
- cgroup: limita y mide los recursos (CPU, memoria, E/S) de un grupo de procesos.
- seccomp: filtra qué syscalls puede invocar el proceso.
- MAC: control de acceso obligatorio; una política del sistema por encima de los permisos de usuario.
- AppArmor / SELinux: las dos implementaciones de MAC más comunes en Linux.
3Explicación profunda
Los namespaces controlan lo que un proceso ve. El kernel mantiene varios tipos,
y cada uno aísla una clase de recurso. Con un PID namespace propio, un proceso solo ve los
procesos de su namespace: para él, el resto de la máquina no existe, y no puede enviarles
señales ni leer su /proc. Con un network namespace tiene su propia pila de red,
sus interfaces y sus puertos, separados de los del host. Con un mount namespace tiene su propia
vista del árbol de archivos, y puede ver un sistema de ficheros por completo distinto. El user
namespace es el más sutil y el más potente para seguridad: permite que un proceso sea "root"
dentro de su namespace mientras es un usuario sin privilegios fuera, de modo
que su root no vale nada en el host. Los namespaces son la razón por la que un contenedor
parece una máquina aparte sin serlo.
Los cgroups (control groups) controlan lo que un proceso consume. Agrupan procesos y les ponen límites: tanta CPU, tanta memoria, tanto ancho de banda de disco. Su valor de seguridad es contener el abuso de recursos, que es una forma de denegación de servicio: un proceso comprometido que intenta agotar la memoria de la máquina —para tumbarla o para provocar comportamientos raros bajo presión— choca con el límite de su cgroup y solo se perjudica a sí mismo. Sin cgroups, un solo proceso puede dejar sin recursos a todos los demás.
seccomp (secure computing) controla lo que un proceso le puede pedir al kernel. Toda interacción de un programa con el sistema pasa por una syscall: abrir un archivo, crear un proceso, montar, cambiar permisos. Un perfil seccomp es una lista blanca (o negra) de syscalls: las permitidas pasan, las prohibidas devuelven un error o matan el proceso. Esto importa muchísimo porque la superficie de ataque del kernel son sus syscalls: muchos exploits de escalada necesitan invocar una syscall concreta y rara. Un perfil que bloquea las syscalls que el servicio no usa —montar, cargar módulos, manipular namespaces— cierra esos exploits aunque el servicio tenga un fallo. Docker aplica por defecto un perfil seccomp que ya bloquea decenas de syscalls peligrosas; endurecerlo es acotarlo aún más a lo que el servicio usa.
AppArmor y SELinux añaden el cuarto eje: el control de acceso obligatorio (MAC). Los permisos de usuario que viste en la lección de permisos son control de acceso discrecional: el dueño de un archivo decide quién accede. El MAC pone por encima una política del sistema que el dueño no puede saltarse: aunque los permisos permitan a un proceso abrir un archivo, si la política de AppArmor de ese proceso no lo contempla, el acceso se deniega. Esto acota un servicio a un perfil explícito: "este proceso solo puede leer estos directorios, escribir en estos otros y abrir estos sockets". AppArmor lo hace por rutas y es más sencillo; SELinux lo hace por etiquetas y es más potente y más complejo. Los dos persiguen lo mismo: que un proceso comprometido, aunque tenga los permisos de usuario, no pueda salirse del guion que su política le fija.
4Ejemplo sencillo
# Un trabajador temporal en una oficina bien organizada
VISTA (namespaces): le dan un despacho desde el que no ve el resto de
la empresa. Para el, la oficina ES su despacho; no sabe que hay mas.
RECURSOS (cgroups): tiene un enchufe con un limite de consumo. Puede
trabajar, pero no puede fundir los plomos de todo el edificio.
PETICIONES (seccomp): solo puede pedir por una lista corta de
formularios. Si intenta pedir "las llaves del edificio", no hay
formulario para eso: la peticion se rechaza en recepcion.
POLITICA (AppArmor/SELinux): un reglamento firmado dice a que salas
puede entrar, aunque tuviera la tarjeta para mas. La tarjeta abre;
el reglamento decide.
5Ejemplo técnico
Los cuatro ejes, qué confinan y con qué se aplican en un contenedor:
| Eje | Confina | En Docker |
|---|---|---|
| namespaces | Qué ve (procesos, red, archivos) | Por defecto; --pid, --network, userns-remap. |
| cgroups | Cuánto consume (CPU, memoria) | mem_limit, cpus, pids_limit. |
| seccomp | Qué syscalls invoca | security_opt: seccomp=perfil.json. |
| MAC | A qué recursos accede por política | security_opt: apparmor=perfil. |
Un fragmento de perfil seccomp que niega la familia chmod: por defecto permite
todo, pero esa syscall devuelve error. Es la forma de cerrar una operación concreta a un
proceso, aunque sus permisos se lo permitieran:
// seccomp: permitir todo salvo cambiar permisos { "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [ { "names": ["chmod", "fchmod", "fchmodat"], "action": "SCMP_ACT_ERRNO" } ] }
6Diagrama
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
P[proceso confinado] --> N[namespaces · qué ve]
P --> C[cgroups · cuánto consume]
P --> S[seccomp · qué syscalls]
P --> A[AppArmor/SELinux · política]
N & C & S & A --> D[daño de un compromiso · recortado]
7Código
En un compose, los cuatro ejes más las capabilities se declaran juntos. Este servicio corre sin root, con memoria y procesos limitados, un perfil seccomp propio, la raíz de solo lectura y sin posibilidad de ganar privilegios:
# docker-compose.yml — un servicio confinado en los cuatro ejes services: web: image: mi-servidor user: "1000:1000" # namespace de usuario: no root read_only: true # mount: raíz de solo lectura mem_limit: 256m # cgroup: tope de memoria pids_limit: 128 # cgroup: tope de procesos cap_drop: [ALL] # capabilities: ninguna de más cap_add: [NET_BIND_SERVICE] # solo la que usa security_opt: - no-new-privileges:true # no puede escalar - seccomp=perfil.json # seccomp: syscalls acotadas
Cada línea cierra un eje distinto, y el efecto se suma. Un atacante que comprometa este servicio se encuentra sin root, sin poder ver otros procesos, sin poder agotar la máquina, sin las syscalls que necesitaría para un exploit del kernel y con la raíz de solo lectura. El fallo sigue existiendo; su alcance es minúsculo.
8Qué puede salir mal
Contenedor sin ningún límite. Un servicio sin mem_limit ni
pids_limit puede agotar la máquina entera; un fallo o un abuso se convierte en
una caída de todo el host.
Desactivar seccomp "porque algo no funciona".
seccomp=unconfined devuelve al proceso todas las syscalls, incluidas las que
necesitan los exploits de kernel. Es tirar una capa entera para ahorrarse afinar el perfil.
Compartir namespaces con el host. --pid=host o
--network=host devuelven al proceso la vista del host: vuelve a ver todos los
procesos o toda la red, y el aislamiento desaparece.
Confiar en una sola capa. Poner capabilities mínimas pero dejar el resto abierto, o un perfil MAC pero sin límites de recursos, deja huecos. El confinamiento es la suma de los ejes, no uno solo.
9Cómo lo abusaría un atacante
Quien cae dentro de un proceso confinado hace, primero, reconocimiento del confinamiento.
Mira qué procesos ve (ps): si ve los del host, no hay PID namespace y puede
atacarlos. Prueba la red: si es la del host, alcanza servicios internos. Intenta syscalls
raras —montar, unshare, cargar un módulo— para ver cuáles pasan: las que pasan
son su superficie hacia el kernel. Y tantea el sistema de archivos buscando qué puede leer y
escribir pese a la política.
Cada eje mal puesto es una vía. Sin cgroups, agota memoria para tumbar la máquina o forzar estados raros. Con seccomp desactivado, tiene todas las syscalls para un exploit de escalada. Con un namespace compartido con el host, ni siquiera necesita escapar: ya está medio fuera. La escalada de contenedor a host casi siempre es esto: un eje que faltaba.
Para ti como defensor, el patrón es claro: el atacante no ataca "el contenedor", ataca las capas de confinamiento una a una buscando la que falta. Poner las cuatro, y no compartir namespaces con el host, es lo que convierte un compromiso del servicio en un compromiso que se queda dentro del servicio.
10Cómo defenderlo
- Confina en los cuatro ejes a la vez: vista (namespaces), recursos (cgroups), syscalls (seccomp) y política (MAC).
- Pon siempre
mem_limitypids_limit: un servicio no debe poder agotar el host. - No desactives seccomp. Mantén el perfil por defecto y acótalo más si puedes; nunca
unconfined. - No compartas namespaces con el host (
--pid=host,--network=host) salvo que sea imprescindible y consciente. - Mantén AppArmor o SELinux activos y con perfil; el control de acceso obligatorio ataja lo que los permisos de usuario dejan pasar.
- Suma
read_only,no-new-privilegesy capabilities mínimas: el confinamiento es la suma, no una capa.
11Laboratorio
El mismo laboratorio de la lección anterior incluye un servicio con-seccomp
con un perfil que bloquea la familia de syscalls chmod. Levántalo y observa
cómo el kernel niega una operación que, por permisos, el proceso podría hacer:
cd cursos/seguridad/labs/lab-linux-caps
docker compose up -d
docker compose logs con-seccomp
Observa el filtrado de syscalls en acción:
Abre labs/lab-linux-caps/seccomp-sin-chmod.json y lee el perfil: por defecto
permite todo (SCMP_ACT_ALLOW) y solo la familia chmod devuelve
error. Como ejercicio de defensa, añade otra syscall al bloqueo (por ejemplo
unshare, para impedir crear namespaces) y vuelve a levantar el servicio.
Cuando termines:
docker compose down -v
12Ejercicios
Asocia cada eje con su límite
Di qué mecanismo confina cada cosa: (1) que un proceso no vea los demás procesos de la
máquina; (2) que no pueda consumir más de 256 MB de memoria; (3) que no pueda invocar la
syscall mount; (4) que no pueda leer un archivo aunque sus permisos se lo
permitan.
(1) namespaces (un PID namespace). (2) cgroups. (3) seccomp. (4) control de acceso obligatorio: AppArmor o SELinux. Los cuatro son independientes y se combinan.
Clave: ver → namespaces; consumir → cgroups; syscalls → seccomp; acceso por política → AppArmor/SELinux.
Confina un servicio en los cuatro ejes
Tienes un servicio de compose que solo declara image. Añádele las líneas que lo
confinan en los cuatro ejes más las capabilities: sin root, con límites de memoria y
procesos, con seccomp y sin poder escalar. Justifica qué eje cierra cada línea.
user:, mem_limit:, pids_limit:, security_opt:, cap_drop:.read_only: true y no-new-privileges:true completan el confinamiento.services:
web:
image: mi-servidor
user: "1000:1000"
read_only: true
mem_limit: 256m
pids_limit: 128
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
- seccomp=perfil.json
Clave: user+read_only (vista), mem_limit+pids_limit (recursos), seccomp (syscalls), cap_drop+no-new-privileges (privilegios). La suma es el confinamiento.
AppArmor frente a SELinux
Los dos son control de acceso obligatorio, pero funcionan distinto. Investiga y explica la diferencia principal entre cómo AppArmor y SELinux deciden qué puede tocar un proceso, y en qué situación preferirías cada uno.
AppArmor define la política por rutas: "este proceso puede leer
/etc/app/". Es sencillo de leer y escribir, pero depende de la ruta: si al
mismo archivo se llega por otra ruta o se mueve, la política puede no aplicar igual.
SELinux define la política por etiquetas (contextos) que viajan con el objeto: el
archivo lleva su etiqueta independientemente de por dónde se acceda, lo que lo hace más
robusto y granular, a costa de bastante más complejidad. Se prefiere AppArmor cuando se
quiere una política clara y mantenible con esfuerzo moderado; SELinux cuando se necesita
el máximo control y granularidad y se puede asumir su curva.
Clave: AppArmor razona por rutas (simple, dependiente de la ruta); SELinux por etiquetas que viajan con el objeto (robusto y granular, más complejo). La elección es control frente a simplicidad.
Red Team · encuentra la syscall que pasa
Dentro del lab-linux-caps, y solo dentro de él: en el servicio
con-seccomp, comprueba que chmod está bloqueado pero otras
operaciones no. Documenta una syscall que el perfil sí deja pasar y que un servicio de
verdad no debería necesitar, y argumenta por qué convendría añadirla al bloqueo.
chmod.unshare o mount raramente hacen falta en un servicio y ayudan a escapar.
El perfil de laboratorio solo bloquea chmod, así que syscalls como
unshare, mount o ptrace pasan. Un servicio web
normal no crea namespaces, ni monta sistemas de archivos, ni se engancha a otros
procesos; dejarlas abiertas le da a un atacante justo las herramientas que usan muchos
exploits de escalada y de escape de contenedor. Convendría añadirlas al bloqueo
(SCMP_ACT_ERRNO) para que el proceso no pueda ni intentarlas.
Clave: unshare/mount/ptrace pasan con este perfil y un servicio no las necesita; bloquearlas cierra vías de escalada sin afectar al servicio. Minimizar la superficie de syscalls es el objetivo.
Blue Team · endurece el perfil y verifica
Modifica seccomp-sin-chmod.json para bloquear también unshare.
Vuelve a levantar el laboratorio y demuestra, con comandos, que chmod y
unshare están ahora bloqueados y que el servicio sigue vivo. Escríbelo como un
check que confirme las dos cosas.
unshare al array names del perfil.docker compose up -d --force-recreate con-seccomp recarga el perfil.
En el perfil, la entrada queda
"names": ["chmod","fchmod","fchmodat","unshare"]. Tras recrear el servicio:
# ambos deben fallar; el proceso sigue vivo docker compose exec con-seccomp sh -c \ 'chmod 777 /etc/hostname 2>&1; unshare -U 2>&1; echo "servicio vivo"'
Clave: añadir la syscall al perfil, recrear el contenedor y verificar que ambas dan "Operation not permitted" y que el servicio sigue funcionando. Endurecer seccomp es acotar la lista, y verificarlo es reintentar lo bloqueado.
Architecture Challenge · confinar un servicio no confiable
Diseña el confinamiento de un servicio que ejecuta código poco confiable (por ejemplo, un procesador de archivos subidos por usuarios). Define cómo lo confinas en los cuatro ejes más capabilities, qué le quitas por completo, qué le dejas, y cómo verificarías que un fallo del servicio no puede ver otros procesos, agotar la máquina ni escalar.
Una propuesta razonable: corre sin root (user: no privilegiado), con su
propio PID y network namespace (por defecto en Docker), sin red de salida si no la
necesita (internal), con read_only: true y un
tmpfs acotado para su trabajo. Límites estrictos: mem_limit,
pids_limit y cpus. cap_drop: ALL y ninguna
capability añadida si no la necesita. Un perfil seccomp que bloquee montaje, namespaces y
ptrace, más no-new-privileges:true. Un perfil de AppArmor que
limite las rutas que puede tocar. La verificación va por ejes: ps dentro no
debe ver procesos del host; un intento de reservar más memoria que el límite debe fallar;
las syscalls bloqueadas deben dar error; y un intento de escalar (SUID, nuevos
privilegios) no debe funcionar.
Clave: no hay una única respuesta. Se evalúa que parta de cap_drop: ALL sin root, que ponga límites de recursos, que acote syscalls con seccomp y política con MAC, y que la verificación cubra los cuatro ejes por separado.
13Preguntas de comprensión
¿Qué aísla un namespace?
Un namespace limita lo que el proceso ve: sus procesos, su red o su árbol de archivos. Es el eje de la vista.
¿Qué controla un cgroup?
Los cgroups limitan y miden el consumo de recursos. Contienen el abuso, que es una forma de denegación de servicio.
¿Qué hace un perfil seccomp?
seccomp filtra syscalls: las permitidas pasan, las prohibidas dan error o matan el proceso. Recorta la superficie de ataque hacia el kernel.
AppArmor y SELinux, ¿qué tipo de control añaden?
El MAC impone una política del sistema que el dueño del archivo no puede saltarse: aunque los permisos permitan el acceso, si la política no lo contempla, se deniega.
14Reto adicional
Explica un contenedor con las cuatro piezas
Toma un contenedor cualquiera que tengas corriendo (o el del laboratorio) y explica, pieza a pieza, cómo lo confina el kernel: qué namespaces tiene, qué límites de cgroup, qué perfil seccomp y qué política MAC. Señala qué eje está bien puesto y cuál podrías endurecer.
docker inspect muestra límites, capabilities y opciones de seguridad.mem_limit, si seccomp es el perfil por defecto o unconfined, y qué capabilities lleva.
Un contenedor típico tiene por defecto namespaces propios de PID, red, montaje, IPC y
UTS (por eso ve su propia lista de procesos y su propia red); un perfil seccomp por
defecto que bloquea decenas de syscalls; y, si el host lo tiene, un perfil de AppArmor o
SELinux por defecto. Lo que suele faltar es lo que hay que endurecer: límites de recursos
(muchos contenedores corren sin mem_limit ni pids_limit),
capabilities acotadas (a menudo se dejan las por defecto en vez de
cap_drop: ALL) y no-new-privileges. Revisando con
docker inspect se ve qué eje está por defecto y cuál se puede acotar más.
Clave: namespaces y seccomp suelen venir de fábrica; los ejes que más se descuidan son los límites de cgroup y las capabilities. Endurecer es acotar esos, no reinventar el contenedor.
15Resumen
- Un proceso se confina en cuatro ejes independientes: qué ve, cuánto consume, qué syscalls invoca y a qué accede por política.
- namespaces aíslan la vista (procesos, red, montajes, usuarios); son la base de los contenedores.
- cgroups limitan el consumo de recursos y contienen el abuso, que es denegación de servicio.
- seccomp filtra syscalls y recorta la superficie de ataque hacia el kernel; nunca
unconfined. - AppArmor y SELinux imponen una política del sistema por encima de los permisos de usuario.
- El confinamiento es la suma de los cuatro ejes más las capabilities; una sola capa deja huecos.
--network=host en un contenedor?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.