El modelo de seguridad: capabilities y minimo privilegio
Revisar las capabilities del nivel 12 a fondo: los cinco conjuntos que porta cada proceso en struct cred, la formula que transforma capabilities al hacer execve, las capabilities ambientales y de fichero, la relacion entre el UID 0 y las capabilities, el papel de ns_capable en los user namespaces, y el principio de minimo privilegio como disciplina de diseno dentro del kernel.
En el nivel 12 conociste las capabilities como piezas en que se parte el poder de root: CAP_NET_ADMIN, CAP_SYS_MODULE, CAP_DAC_OVERRIDE. Aquello era el mapa; ahora bajamos al territorio. Un proceso no porta una lista de capabilities, sino cinco conjuntos distintos que interactúan según una fórmula precisa cada vez que ejecuta un binario. El UID 0 no es lo que parece —root es poderoso no por ser cero, sino por llevar todas las capabilities encendidas—, y en un user namespace un proceso puede ser omnipotente sobre su universo y un don nadie fuera de él. Entender esta mecánica es entender cómo el kernel implementa, en su propio corazón, el principio de mínimo privilegio.
- Distinguir los cinco conjuntos de capabilities que porta un proceso en
struct cred. - Aplicar la fórmula que transforma las capabilities al hacer
execve. - Separar el UID 0 de las capabilities y entender
ns_capableen los user namespaces. - Practicar el mínimo privilegio: exigir la capability específica, nunca
CAP_SYS_ADMINpor pereza.
Cinco conjuntos, no una lista
Las credenciales de un proceso viven en struct cred, y las capabilities ocupan cinco campos, cada uno un mapa de bits sobre las cuarenta y tantas capabilities existentes:
/* include/linux/cred.h (extracto) */
struct cred {
kuid_t uid, euid, suid, fsuid; /* identidades de usuario */
kgid_t gid, egid, sgid, fsgid;
kernel_cap_t cap_inheritable; /* se pueden heredar por execve */
kernel_cap_t cap_permitted; /* techo: lo que PUEDE activar */
kernel_cap_t cap_effective; /* lo que el kernel COMPRUEBA ahora */
kernel_cap_t cap_bset; /* bounding: techo absoluto, solo baja */
kernel_cap_t cap_ambient; /* heredables sin fichero (desde 4.3) */
struct user_namespace *user_ns; /* el ns donde estas capabilities valen */
/* ... */
};
El effective es el que cuenta en cada comprobación. El permitted es el techo desde el que puedes subir capabilities al effective. El inheritable y el ambient gobiernan qué sobrevive a un execve. Y el bounding set es un techo que solo puede bajar: una capability retirada del bounding set no puede recuperarse ni con la ayuda de un binario privilegiado, lo que lo convierte en la herramienta con que un contenedor amputa poderes de forma permanente. En los kernels modernos kernel_cap_t se simplificó a un único entero de 64 bits, suficiente para todas las capabilities definidas:
/* include/linux/cap_types.h: un u64 basta desde 6.3 */
typedef struct { u64 val; } kernel_cap_t;
La fórmula del execve
Cada execve recalcula las capabilities del proceso combinando las que traía con las del fichero ejecutado (F, fijadas con setcap). Esta es la transformación, tal como la implementa el kernel:
P'(ambient) = (fichero privilegiado) ? 0 : P(ambient)
P'(permitted) = (P(inheritable) & F(inheritable)) |
(F(permitted) & P(bounding)) | P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable)
Léela con calma, porque explica una frustración histórica. Antes de las capabilities ambient (kernel 4.3, 2015), un proceso sin privilegios no podía transmitir una capability a través de execve: aunque la tuviera en su inheritable, el binario ejecutado necesitaba tenerla también en su inheritable de fichero para que sobreviviera, y la mayoría de binarios no llevan capabilities. El inheritable era, en la práctica, inútil sin cooperación del fichero. Las capabilities ambient rompen el candado: lo que está en el conjunto ambient sí pasa a permitted y effective tras execve (siempre que también esté en inheritable), permitiendo por fin lanzar un binario común con una capability concreta sin marcarlo con setcap ni hacerlo setuid root.
El UID 0 es una ilusión de totalidad
Aquí está el giro conceptual. Root no es poderoso porque su UID sea 0; es poderoso porque el proceso de init arranca con todas las capabilities encendidas y las hereda su descendencia privilegiada. El UID 0 solo aporta dos atajos: cuando el euid es 0, el kernel rellena permitted y effective con capabilities completas al no haber fichero privilegiado, y ciertas comprobaciones antiguas miran el UID directamente. Pero puedes disociar ambas cosas. Un proceso con UID 0 y el bounding set vaciado es un root impotente. Un proceso con UID 1000 y CAP_NET_BIND_SERVICE en su effective puede abrir el puerto 80 sin ser root en absoluto:
# dar a un binario solo el poder de abrir puertos privilegiados, nada mas
setcap 'cap_net_bind_service=+ep' /usr/bin/miservidor
getcap /usr/bin/miservidor
# /usr/bin/miservidor cap_net_bind_service=ep
# ping moderno ya no es setuid root: lleva una sola capability
getcap /usr/bin/ping
# /usr/bin/ping cap_net_raw=ep
La comprobación en el kernel es una sola función. capable(cap) pregunta si el proceso actual tiene la capability en su effective set respecto al namespace de usuario inicial; ns_capable(ns, cap) la pregunta respecto a un namespace concreto:
/* kernel/capability.c */
bool capable(int cap)
{
return ns_capable(&init_user_ns, cap);
}
ns_capable: root en tu propio universo
Esta distinción es la que hace posibles los contenedores rootless. Cuando creas un user namespace, tu proceso recibe todas las capabilities dentro de ese namespace: ns_capable(mi_userns, CAP_SYS_ADMIN) es verdadero, y por eso puedes crear los demás namespaces y montar sistemas de ficheros virtuales. Pero ns_capable(&init_user_ns, CAP_SYS_ADMIN) sigue siendo falso: sobre los recursos del host no tienes ningún poder. Cada comprobación del kernel elige contra qué namespace preguntar según a quién pertenezca el objeto que se toca. Ser root en tu universo no te da ningún derecho sobre el universo de al lado.
flowchart TD
C[capable cap] --> N[ns_capable init_user_ns cap]
N --> E{cap en cap_effective}
E -->|si| OK[permitido]
E -->|no| DENY[EPERM]
O[objeto del kernel] --> OWN[pertenece a un user_ns]
OWN --> NSC[ns_capable de ESE ns]
NSC --> EUID 0 (root clasico)
Todo o nada. El proceso obtiene capabilities completas y salta casi toda comprobación. Un solo fallo compromete el sistema entero: no hay granularidad que limite el daño.
Capabilities (minimo privilegio)
Poder troceado. Cada servicio recibe solo las capabilities que su función exige. Un fallo compromete únicamente lo que esa pieza podía tocar, no la máquina completa.
Mínimo privilegio como disciplina de diseño
Cuando escribas código de kernel con una operación privilegiada, la tentación es comprobar capable(CAP_SYS_ADMIN) y seguir. Resístela. CAP_SYS_ADMIN se ha convertido en el nuevo root: más de un tercio de todas las comprobaciones de capability del kernel lo usan, acumulando poderes tan dispares que concederlo equivale casi a dar todo. El mínimo privilegio exige buscar —o proponer, si no existe— la capability específica y más estrecha que corresponde a tu operación:
/* MAL: pide medio root para una operacion de red */
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
/* BIEN: exige exactamente el poder que la operacion necesita */
if (!capable(CAP_NET_ADMIN))
return -EPERM;
Esta disciplina —el principio de mínimo privilegio que viste en C23 aplicado dentro del núcleo— no es purismo académico. Cada capability que tu driver no exige es una capability que un atacante no obtiene al comprometerlo, y cada comprobación estrecha en lugar de amplia reduce la cantidad de código privilegiado alcanzable en todo el sistema.
Si tu diseño acaba pidiendo CAP_SYS_ADMIN, detente y sospecha. Esa capability engloba montar sistemas de ficheros, manipular namespaces, cambiar el hostname, ajustar límites del kernel y decenas de operaciones sin relación entre sí. Concederla a un servicio para una tarea concreta le regala, de propina, el poder de hacer casi cualquier cosa. Casi siempre existe una capability más estrecha; si de verdad no existe, quizá la operación no deba estar tras una simple comprobación de capability, sino tras un LSM.
Durante décadas, generaciones enteras de administradores pensaron en root como una identidad, un quién: el superusuario, el dueño de la máquina, la persona omnipotente. El lenguaje reforzaba la ilusión —hacerse root, ser root, dar root a alguien— como si fuera un trono que se ocupa. Lo que esta lección disuelve es precisamente esa personificación. Root no es un quién; es un cuánto. No es una identidad que posea poder por derecho propio, sino un proceso que casualmente lleva todos los bits de capability encendidos a la vez. El UID 0 no contiene poder: contiene un atajo por el que el kernel, al ver ese cero, rellena los conjuntos de capabilities hasta el tope. Pero el poder no vive en el número, vive en los bits, y los bits se pueden quitar uno a uno hasta dejar un UID 0 tan inofensivo como cualquier usuario. Esta desmitificación tiene una consecuencia que reordena toda la práctica de la seguridad: si root no es una identidad indivisible sino un vector de permisos, entonces la pregunta correcta nunca fue quién es root, sino qué capabilities necesita esta acción concreta. El modelo binario —o eres todopoderoso o eres impotente— era una pobreza conceptual heredada de una época en que el hardware no permitía más matices. Las capabilities restituyen la verdad que siempre estuvo ahí: el privilegio no es un estado, es un espectro; no se posee, se compone; no se concede en bloque, se dosifica gota a gota. Y el mínimo privilegio no es una buena práctica opcional que se añade después, sino la consecuencia lógica de haber entendido qué es realmente el poder en un sistema operativo: no una corona que se hereda, sino una suma de permisos que, como toda suma, se puede acotar término a término hasta que quede exactamente lo justo y ni un bit más.
- Ejecuta
capsh --printygetpcaps $$para leer los cinco conjuntos de capabilities de tu shell y de un proceso de sistema comosystemd. - Usa
setcappara darcap_net_bind_servicea un pequeño servidor propio y comprueba que abre el puerto 80 sin ser root. - Lanza un proceso con
capsh --drop=cap_sys_admin --y razona por qué esa capability no puede recuperarse aunque después ejecutes un binario setuid root. - Explica con la fórmula del
execvepor qué, antes de las capabilities ambient, el conjunto inheritable era inútil para un usuario sin privilegios. - Argumenta por qué
ns_capable(mi_userns, CAP_SYS_ADMIN)puede ser verdadero mientrascapable(CAP_SYS_ADMIN)es falso, y qué hace posible ese hecho en un contenedor rootless.