Linux y seguridad de sistemas · Lección 10

Namespaces, cgroups, seccomp y AppArmor

Las capabilities recortan qué operaciones privilegiadas puede hacer un proceso. Esta lección añade los otros ejes del confinamiento, independientes entre sí: qué ve el proceso (namespaces), cuánto puede consumir (cgroups), qué le puede pedir al kernel (seccomp) y qué recursos le deja tocar una política externa (AppArmor y SELinux, el control de acceso obligatorio). Son las piezas con las que están hechos los contenedores, y las que convierten un proceso comprometido en un proceso comprometido que casi no puede hacer daño. El objetivo de la lección es saber combinarlas para confinar de verdad.

Al terminar sabrás

  • Distinguir los cuatro ejes de confinamiento: vista, recursos, syscalls y política de acceso.
  • Explicar qué aísla cada tipo de namespace: PID, red, montajes, usuarios.
  • Usar cgroups para limitar CPU y memoria y contener un abuso de recursos.
  • Escribir la idea de un perfil seccomp que bloquee syscalls peligrosas.
  • Entender qué añade el control de acceso obligatorio de AppArmor y SELinux sobre los permisos de usuario.
  • Combinar los cuatro ejes más las capabilities para confinar un servicio en un contenedor.

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.

El objetivo real

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

Vocabulario mínimo
  • 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.
Los cuatro ejes son independientes y se suman: recortan lo que el trabajador ve, lo que gasta, lo que puede pedir y a dónde puede entrar. Un temporal con mala intención choca con cuatro límites, no con uno.

5Ejemplo técnico

Los cuatro ejes, qué confinan y con qué se aplican en un contenedor:

EjeConfinaEn Docker
namespacesQué ve (procesos, red, archivos)Por defecto; --pid, --network, userns-remap.
cgroupsCuánto consume (CPU, memoria)mem_limit, cpus, pids_limit.
seccompQué syscalls invocasecurity_opt: seccomp=perfil.json.
MACA qué recursos accede por políticasecurity_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

Un proceso confinado por cuatro capas independientes: namespaces limitan lo que ve, cgroups lo que consume, seccomp las syscalls que invoca y AppArmor los recursos que su política le permite proceso confinado namespaces qué ve cgroups cuánto consume seccomp qué syscalls AppArmor / SELinux daño de un compromiso recortado por las cuatro capas
Los cuatro ejes son capas independientes sobre el mismo proceso. Ninguna evita el fallo; cada una recorta una dimensión distinta de lo que ese fallo puede alcanzar.
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

Los errores clásicos

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

Vista desde el otro lado

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

Qué hacer con esto mañana
  • Confina en los cuatro ejes a la vez: vista (namespaces), recursos (cgroups), syscalls (seccomp) y política (MAC).
  • Pon siempre mem_limit y pids_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-privileges y capabilities mínimas: el confinamiento es la suma, no una capa.

11Laboratorio

Estado: laboratorio local lab-linux-caps
Targetcontenedores caps-*, sin puertos publicados
Autorizaciónsistema local creado por ti para entrenamiento
Este laboratorio es tuyo, está aislado y no tiene salida a internet. Todo lo que se practica aquí se practica aquí dentro. Ninguna de estas técnicas debe usarse contra un sistema que no sea tuyo o para el que no tengas autorización escrita.

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:

construir → atacar → observar → defender → verificar
docker compose logs con-seccomp == intento de chmod con la syscall bloqueada por seccomp == chmod: changing permissions of '/etc/hostname': Operation not permitted == el proceso sigue vivo; el resto de syscalls funciona == # seccomp negó la syscall chmod aunque el proceso podía por permisos

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.

Los cuatro ejes son: vista, recursos, syscalls y política.
"Ver" es namespaces; "consumir" es cgroups.
Filtrar syscalls es seccomp; una política por encima de los permisos es MAC.
Solución

(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.

Necesitas tocar vista, recursos, syscalls y privilegios.
user:, mem_limit:, pids_limit:, security_opt:, cap_drop:.
read_only: true y no-new-privileges:true completan el confinamiento.
Solución
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.

Uno razona por rutas de archivo; el otro, por etiquetas.
Piensa qué pasa cuando un archivo se mueve o se accede por otra ruta.
Las etiquetas viajan con el objeto; las rutas, no. Eso cambia la potencia y la complejidad.
Solución

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 deja pasar y que un servicio de verdad no debería necesitar, y argumenta por qué convendría añadirla al bloqueo.

El perfil por defecto permite todo salvo la familia chmod.
Piensa en syscalls de creación de namespaces o de montaje.
unshare o mount raramente hacen falta en un servicio y ayudan a escapar.
Solución

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.

Añade unshare al array names del perfil.
docker compose up -d --force-recreate con-seccomp recarga el perfil.
Verifica intentando cada syscall dentro del contenedor y comprobando el error.
Solución

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.

Un servicio así debe partir de "casi nada" y recibir solo lo justo.
Piensa qué namespaces, límites, syscalls y política necesita realmente.
La verificación se hace por cada eje: procesos que ve, memoria que puede, syscalls que pasan.
Solución

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

Comprobació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.

0 / 4

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.

Un contenedor es exactamente la suma de los cuatro ejes más capabilities.
docker inspect muestra límites, capabilities y opciones de seguridad.
Comprueba si tiene mem_limit, si seccomp es el perfil por defecto o unconfined, y qué capabilities lleva.
Solución

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

Puntos clave
  • 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.
¿Cuáles son los cuatro ejes en los que se confina un proceso?
Vista (namespaces), recursos (cgroups), syscalls (seccomp) y política de acceso (AppArmor/SELinux). Son independientes y se suman.
¿Por qué importa para la seguridad limitar las syscalls con seccomp?
Porque la superficie de ataque del kernel son sus syscalls; muchos exploits de escalada necesitan una concreta, y bloquearla los cierra.
¿Qué diferencia hay entre control de acceso discrecional y obligatorio?
El discrecional (permisos de usuario) lo decide el dueño del archivo; el obligatorio (MAC) lo impone una política del sistema que el dueño no puede saltarse.
¿Por qué es grave --network=host en un contenedor?
Devuelve al proceso el network namespace del host: vuelve a ver toda la red y los servicios internos, y el aislamiento de red desaparece.

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.