1Por qué importa
Cada vez que un proceso corre como root "para una cosa", el alcance de cualquier fallo suyo es la máquina entera. Un servidor que corre como root y sufre una inyección de comandos no da al atacante "acceso al servidor": da root. La causa suele ser trivial —necesitaba escuchar en un puerto por debajo de 1024, o ajustar una prioridad, o abrir un socket especial— y para eso se le concedió todo. Es el opuesto exacto del mínimo privilegio.
Las capabilities existen para romper esa disyuntiva entre "root" y "nada". El poder de root no
es una cosa indivisible: es un conjunto de permisos concretos que el kernel comprueba por
separado. Reservar un puerto bajo es CAP_NET_BIND_SERVICE. Saltarse los permisos
de archivo es CAP_DAC_OVERRIDE. Cambiar el dueño de un archivo es
CAP_CHOWN. Si el proceso solo necesita una, se le da una y se le niegan las demás.
El fallo, cuando ocurra, tendrá el alcance de esa única capability, no el de root. Esta es la
herramienta con la que se convierte "corre como root" en "corre casi sin privilegios, salvo
exactamente lo que necesita".
No estás aprendiendo una lista de capabilities. Estás aprendiendo a hacerte, ante cualquier proceso que "necesita root", la pregunta correcta: ¿qué operación privilegiada concreta necesita? Casi siempre la respuesta es una capability, no todas.
2Conceptos
- capability: un permiso concreto en que se divide el poder de root.
- CAP_NET_BIND_SERVICE: reservar puertos por debajo de 1024.
- CAP_DAC_OVERRIDE: saltarse las comprobaciones de permisos de archivo.
- CAP_CHOWN: cambiar el dueño de un archivo.
- CAP_SYS_ADMIN: un cajón de sastre de operaciones privilegiadas; casi root completo.
- capability de fichero: una capability puesta sobre un binario (
setcap), alternativa acotada al SUID. - bounding set: el límite de qué capabilities puede llegar a tener un proceso.
- cap_drop / cap_add: en Docker, quitar todas y añadir solo las necesarias.
3Explicación profunda
Históricamente, el kernel decidía si una operación privilegiada estaba permitida con una única
pregunta: ¿el proceso tiene UID 0 (root)? Todo o nada. Las capabilities cambiaron esa pregunta
por muchas preguntas pequeñas. Hoy hay unas cuarenta capabilities, y para cada operación
privilegiada el kernel comprueba una en concreto. Reservar el puerto 80 no pregunta "¿eres
root?", pregunta "¿tienes CAP_NET_BIND_SERVICE?". Ser root, en la práctica, no es
más que tener todas las capabilities a la vez; darlas una a una es lo que permite el mínimo
privilegio.
El ejemplo canónico es el puerto privilegiado. Por diseño, los puertos por debajo de 1024 solo
los puede abrir un proceso con CAP_NET_BIND_SERVICE, porque escuchar en ellos es
hacerse pasar por un servicio de sistema (un servidor en el 80 es "el web de la máquina"). La
gente resuelve esto corriendo el servidor como root, que tiene esa capability... junto con
todas las demás. La solución correcta es dar solo CAP_NET_BIND_SERVICE: el proceso
escucha en el 80 sin ser root, y no puede leer archivos que no le tocan, ni montar, ni cambiar
dueños. Si lo comprometen, el atacante gana la capacidad de abrir puertos bajos, que es casi
nada, en lugar de root.
No todas las capabilities son igual de acotadas, y aquí está el matiz importante.
CAP_NET_BIND_SERVICE hace una cosa. CAP_SYS_ADMIN, en cambio, es un
cajón de sastre: agrupa tantas operaciones privilegiadas —montar sistemas de archivos, cambiar
espacios de nombres, y decenas más— que concederla es casi tan peligroso como dar root
completo, porque desde varias de esas operaciones se llega a root de todas formas. La regla
práctica es doble: da la capability más pequeña que resuelva la tarea, y desconfía de
CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, CAP_SYS_PTRACE y
CAP_SYS_MODULE, que abren caminos amplios.
Las capabilities también se ponen sobre binarios, y ahí sustituyen al SUID de forma más
segura. En la lección de permisos viste que un binario SUID root corre con todo el
poder de root, y que ese exceso es el problema. Con setcap 'cap_net_bind_service=+ep'
/usr/bin/miservidor, el binario obtiene solo esa capability al ejecutarse, no root
entero. Es la versión moderna y acotada de "necesita privilegios para una cosa": en vez de
prestar toda la identidad de root (SUID), presta el permiso exacto. En contenedores, Docker
expone lo mismo con cap_drop y cap_add: lo correcto es
cap_drop: ALL y luego cap_add solo de lo que el servicio use.
4Ejemplo sencillo
# Un llavero maestro contra una sola llave
"Ser root" es entregar el LLAVERO MAESTRO del edificio para que
alguien pueda abrir UNA puerta. Si pierde el llavero (lo comprometen),
quien lo encuentre abre TODAS las puertas.
Una capability es entregar SOLO la llave de esa puerta. El del
puerto 80 recibe la llave "abrir puertas de recepcion" (CAP_NET_BIND_
SERVICE) y ninguna mas. Si la pierde, el que la encuentre abre la
recepcion... y se queda ahi. Ni el almacen, ni la caja fuerte.
Mismo trabajo hecho. Muchisimo menos que perder si algo sale mal.
5Ejemplo técnico
Algunas capabilities y para qué sirven, con su nivel de riesgo si se conceden de más:
| Capability | Permite | Riesgo si sobra |
|---|---|---|
CAP_NET_BIND_SERVICE | Escuchar en puertos <1024 | Bajo: solo abrir puertos bajos. |
CAP_CHOWN | Cambiar el dueño de archivos | Medio: puede reasignar propiedad. |
CAP_DAC_OVERRIDE | Saltarse los permisos de archivo | Alto: lee y escribe cualquier archivo. |
CAP_SYS_ADMIN | Montar, namespaces, y mucho más | Crítico: casi equivale a root. |
Los comandos para leer y conceder capabilities. El primero mira las de un proceso; el segundo, las de un binario; el tercero pone la capability justa sobre un binario:
# capabilities del proceso con PID dado getpcaps <pid> # capabilities puestas sobre un binario (alternativa al SUID) getcap /usr/bin/miservidor # conceder SOLO la de puerto bajo a un binario, sin SUID root setcap 'cap_net_bind_service=+ep' /usr/bin/miservidor
6Diagrama
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
R[servidor como root] --> B[escucha en :80]
C[servidor + NET_BIND_SERVICE] --> B
R --> XR[compromiso = root · toda la máquina]
C --> XC[compromiso = solo puertos bajos]
7Código
En un contenedor, el mínimo privilegio con capabilities se escribe como cap_drop: ALL
y luego el cap_add justo. El servicio corre sin ser root y sin ninguna capability
salvo la que usa:
# docker-compose.yml — un servidor web en el puerto 80, sin root services: web: image: mi-servidor user: "1000:1000" # no root dentro del contenedor cap_drop: - ALL # quita TODAS las capabilities cap_add: - NET_BIND_SERVICE # devuelve solo la del puerto bajo
Fuera de contenedores, la misma idea sobre el binario evita el SUID root. En vez de prestar toda la identidad de root, presta la capability exacta:
# en vez de: chmod u+s /usr/bin/miservidor (SUID root: demasiado) setcap 'cap_net_bind_service=+ep' /usr/bin/miservidor getcap /usr/bin/miservidor # verifica: solo cap_net_bind_service
La comprobación siempre es la misma: el servicio hace su tarea (escucha en el 80) y, si intentas
con él una operación privilegiada distinta (leer /etc/shadow, por ejemplo), falla.
Un mínimo privilegio que no se verifica es una suposición.
8Qué puede salir mal
Correr como root "porque necesita el puerto 80". Es el caso más común y casi siempre
se resuelve con CAP_NET_BIND_SERVICE. Root da esa capacidad y, de propina,
todas las demás.
Conceder CAP_SYS_ADMIN por comodidad. Aparece cuando algo falla y alguien
prueba capabilities hasta que funciona. CAP_SYS_ADMIN "arregla" casi todo
porque es casi root; concederla es no haber acotado nada.
Usar --privileged en Docker. Da todas las capabilities y quita casi
todas las protecciones. Es lo contrario del mínimo privilegio y rara vez hace falta de
verdad.
No verificar. Añadir cap_add: NET_BIND_SERVICE pero seguir corriendo como
root, o no comprobar que el proceso ya no puede hacer lo demás, deja el confinamiento sin
efecto y sin que nadie lo note.
9Cómo lo abusaría un atacante
Quien compromete un proceso mira, antes que nada, con qué privilegios corre. Si el proceso
es root, ya terminó: tiene la máquina. Por eso la primera pregunta que se hace un atacante
al caer dentro de un servicio es id y getpcaps $$: quiere saber si
heredó root o un conjunto de capabilities, y cuáles.
Si el proceso tiene capabilities acotadas, el atacante busca las peligrosas.
CAP_DAC_OVERRIDE le deja leer y escribir cualquier archivo pese a los permisos:
/etc/shadow, claves. CAP_SYS_ADMIN le abre montajes y namespaces
desde los que escapar. CAP_SYS_PTRACE le deja engancharse a otros procesos. Cada
capability de más es un camino; una capability acotada como CAP_NET_BIND_SERVICE,
en cambio, no le sirve casi para nada.
Para ti como defensor, la conclusión es directa: el daño de un compromiso es exactamente el conjunto de capabilities del proceso. Reducir ese conjunto a la capability mínima no evita el compromiso, pero convierte "el atacante tiene root" en "el atacante puede abrir puertos bajos". Esa diferencia es toda la lección.
10Cómo defenderlo
- Ante cualquier "necesita root", pregunta qué operación privilegiada concreta necesita, y dale solo esa capability.
- Para escuchar en puertos bajos sin root, usa
CAP_NET_BIND_SERVICE. - En contenedores:
cap_drop: ALLy luegocap_addsolo lo necesario. Nunca--privilegedpor defecto. - Desconfía de
CAP_SYS_ADMIN,CAP_DAC_OVERRIDE,CAP_SYS_PTRACEyCAP_SYS_MODULE: abren caminos amplios a root. - En binarios, prefiere
setcapcon la capability exacta al SUID root. - Verifica con
getpcapsque el proceso hace su tarea y no puede hacer nada más.
11Laboratorio
El laboratorio corre el mismo programa —un socket que intenta escuchar en el puerto 80— con cuatro niveles de privilegio. Levántalo y compara los logs:
cd cursos/seguridad/labs/lab-linux-caps
docker compose up -d
docker compose logs sin-root
docker compose logs con-cap
Observa que la capability justa hace el mismo trabajo que root, sin ser root:
El servicio con-cap es la defensa hecha configuración: cap_drop: ALL
más cap_add: NET_BIND_SERVICE y user: "1000:1000". Compáralo
con como-root y razona qué gana cada uno un atacante que los comprometa.
Cuando termines:
docker compose down -v
12Ejercicios
Elige la capability
Para cada tarea, di qué capability basta (o si de verdad hace falta root): (1) un servidor que escucha en el puerto 443; (2) un proceso que necesita cambiar el dueño de archivos que crea; (3) un proceso que quiere montar un sistema de archivos; (4) un servidor que escucha en el puerto 8080.
(1) CAP_NET_BIND_SERVICE: el 443 es <1024. (2) CAP_CHOWN. (3)
CAP_SYS_ADMIN (montar es de las operaciones que agrupa; por eso es tan
peligrosa). (4) Ninguna: el 8080 es >1024, cualquier usuario lo abre sin privilegios.
Clave: puerto bajo → NET_BIND_SERVICE; chown → CHOWN; montar → SYS_ADMIN (amplia); puerto alto → nada.
Convierte un servicio root en uno con capability
Tienes un servicio de compose que corre como root solo para escuchar en el puerto 80. Reescríbelo para que corra sin ser root y con la capability mínima. Justifica cada línea y di qué gana un atacante que comprometa cada versión.
user:, cap_drop: y cap_add:.cap_drop: [ALL] primero, luego cap_add: [NET_BIND_SERVICE].services:
web:
image: mi-servidor
user: "1000:1000"
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]
Clave: no root, todas las capabilities quitadas, solo la del puerto bajo devuelta. Comprometer la versión root da la máquina; comprometer esta da solo abrir puertos bajos.
Por qué CAP_SYS_ADMIN es casi root
Investiga por qué CAP_SYS_ADMIN se considera casi equivalente a root completo,
a diferencia de CAP_NET_BIND_SERVICE. Nombra al menos dos operaciones que
agrupa y explica cómo, desde una de ellas, se puede llegar a privilegios totales.
CAP_SYS_ADMIN agrupa decenas de operaciones dispares: montar y desmontar
sistemas de archivos, manipular namespaces, cambiar parámetros del kernel, y muchas más.
Varias de ellas bastan para escalar: si puedes montar un sistema de archivos donde
quieras, montas sobre rutas de confianza y sustituyes binarios o configuraciones que un
proceso privilegiado usará; si puedes crear namespaces, reconfiguras la vista del
sistema a tu favor. Por eso concederla equivale, en la práctica, a dar root.
CAP_NET_BIND_SERVICE hace una sola cosa acotada y no abre ninguno de esos
caminos.
Clave: agrupa tantas operaciones privilegiadas (montar, namespaces…) que desde varias de ellas se llega a root. Su amplitud, no una operación concreta, es lo que la hace peligrosa.
Red Team · mide qué gana un compromiso
Dentro del lab-linux-caps, y solo dentro de él: entra en el contenedor
como-root y en el con-cap, mira las capabilities de cada uno y
prueba una operación privilegiada distinta de escuchar en el 80 (por ejemplo, leer un
archivo sin permisos o cambiar un dueño). Documenta qué puede cada uno y concluye qué gana
un atacante en cada caso.
docker compose exec <servicio> sh.id y las capabilities del proceso 1 en cada contenedor.con-cap, intenta algo que requiera otra capability y observa que falla.
En como-root, id da uid 0 y el proceso tiene el conjunto de
capabilities de root: puede leer archivos ajenos, cambiar dueños, y desde ahí hacer casi
cualquier cosa. En con-cap, id da uid 1000 y el proceso solo
tiene NET_BIND_SERVICE: escucha en el 80, pero al intentar leer un archivo
sin permisos o un chown recibe "Operation not permitted". Conclusión:
comprometer como-root entrega la máquina; comprometer con-cap
entrega solo la capacidad de abrir puertos bajos.
Clave: el daño de un compromiso es igual al conjunto de capabilities del proceso. Root las tiene todas; con-cap tiene una acotada, así que un compromiso no da casi nada.
Blue Team · verifica el mínimo privilegio
Demuestra, con comandos, que el servicio con-cap del laboratorio cumple el
mínimo privilegio: que hace su tarea (escucha en el 80) y que no puede una operación
privilegiada distinta. Escríbelo como un check que confirme las dos cosas.
con-cap e intenta leer un archivo que no te toque o hacer chown.# 1) hace su tarea: docker compose logs con-cap | grep "escuchando en el puerto 80" # 2) NO puede otra operacion privilegiada (dentro del contenedor): docker compose exec con-cap sh -c \ 'chown 0:0 /etc/hostname 2>&1 || echo "OK: sin CAP_CHOWN"'
Clave: mínimo privilegio se verifica por las dos caras: el servicio hace lo suyo y falla en todo lo demás. Solo comprobar que "funciona" no demuestra que esté acotado.
Architecture Challenge · el conjunto de capabilities de un stack
Diseña las capabilities de un stack de tres servicios en contenedores: un proxy que escucha en el 80 y el 443, una aplicación que solo habla con la base de datos por un puerto alto, y una base de datos. Define qué capabilities lleva cada uno, cuáles se quitan, y cómo verificarías que ninguno tiene más de lo que necesita.
NET_BIND_SERVICE.cap_drop: ALL y añade solo donde haga falta.
Una propuesta razonable: los tres arrancan con cap_drop: ALL y
user: sin root. El proxy es el único con cap_add: NET_BIND_SERVICE,
porque escucha en el 80 y el 443. La aplicación y la base de datos no reciben ninguna
capability: hablan por puertos altos, que no requieren privilegio. La base de datos, si
necesita ajustar memoria bloqueada, podría requerir CAP_IPC_LOCK, y solo esa.
La verificación es getpcaps sobre el proceso principal de cada contenedor:
el proxy debe mostrar solo net_bind_service, y los otros dos, ninguna.
Clave: no hay una única respuesta. Se evalúa que cada servicio parta de cap_drop: ALL, que solo el proxy tenga NET_BIND_SERVICE y que la verificación con getpcaps confirme que ninguno tiene de más.
13Preguntas de comprensión
¿Qué problema resuelven las capabilities?
Las capabilities dividen el "todo o nada" de root en permisos concretos que el kernel comprueba por separado, para conceder solo el necesario.
Para escuchar en el puerto 80 sin ser root, ¿qué capability basta?
Reservar puertos por debajo de 1024 es justo lo que concede CAP_NET_BIND_SERVICE. Es acotada: no da ningún otro poder.
¿Por qué CAP_SYS_ADMIN se considera casi equivalente a root?
Es un cajón de sastre: montar, namespaces y decenas de operaciones más. Desde varias de ellas se escala a root, así que concederla equivale casi a darlo.
En Docker, ¿cómo se aplica mínimo privilegio con capabilities?
Quitar todas las capabilities y devolver solo la que el servicio usa deja el proceso con el privilegio mínimo. --privileged hace justo lo contrario.
14Reto adicional
Sustituye un SUID por una capability
Toma un caso donde alguien usaría SUID root para un binario propio que solo necesita una
operación privilegiada (por ejemplo, escuchar en un puerto bajo). Explica cómo lo
resolverías con setcap en vez de SUID, qué capability pondrías, y por qué la
versión con capability es más segura que la SUID.
setcap pone la capability sobre el binario, como viste en la sección de código.cap_net_bind_service.
En vez de chmod u+s (SUID root, que hace que el binario corra con toda la
identidad de root), se usa
setcap 'cap_net_bind_service=+ep' /usr/bin/miservidor. El binario obtiene
solo la capacidad de reservar puertos bajos, sin ninguna otra. Es más seguro porque si
el binario tiene un fallo, el atacante gana esa única capability en lugar de root
completo. La regla general: sustituir SUID root por la capability exacta convierte "todo
el poder" en "el permiso justo".
Clave: setcap cap_net_bind_service=+ep en lugar de SUID root. El SUID presta toda la identidad de root; la capability presta solo una operación, así que un fallo vale muchísimo menos.
15Resumen
- El poder de root no es indivisible: las capabilities lo parten en permisos concretos que el kernel comprueba por separado.
- Escuchar en un puerto <1024 sin root es
CAP_NET_BIND_SERVICE, no root entero. - El daño de un compromiso es exactamente el conjunto de capabilities del proceso; reducirlo reduce el daño.
CAP_SYS_ADMIN,CAP_DAC_OVERRIDEy otras amplias son casi root: hay que desconfiar de ellas.- En binarios,
setcapcon la capability exacta es una alternativa más segura que el SUID root. - En contenedores:
cap_drop: ALLmás elcap_addjusto, y verificar congetpcaps.
CAP_NET_BIND_SERVICE. Escucha en el 80 sin ser root y sin ningún otro poder.CAP_SYS_ADMIN?cap_drop: ALL y luego cap_add solo de lo necesario, con user: sin root. Verificado con getpcaps.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.