Cerramos el módulo 4 con tres suposiciones colgando en el aire: que el UID 990 es meteora, que quien inicia sesión es quien dice ser, y que un proceso que corre como meteora hace lo que meteora haría. Las dos primeras las resolveremos en la siguiente lección. Esta se dedica entera a la tercera, que es la más incómoda de las tres, porque es la que sigue siendo falsa aunque todos los permisos estén perfectos.
Piénsalo así. meteo-api tiene exactamente los permisos que necesita: lee /var/lib/meteora/lecturas/*.dat, lee /etc/meteora/meteora.conf, escribe en /var/log/meteora/meteo-api.log. Ni uno más. Ahora imagina que alguien encuentra un fallo en el código que analiza las peticiones HTTP y consigue que el proceso ejecute instrucciones suyas. Ese atacante hereda el proceso entero, con sus permisos intactos. Y con esos permisos —los correctos, los mínimos, los que aprobó la revisión de seguridad— puede abrir un socket a Internet, leer las 17.280.000 líneas de datos del día, ejecutar /bin/sh, crear un proceso hijo que sobreviva al reinicio del servicio y montar /dev/md0 en otro sitio si el núcleo se lo permite. Los nueve bits no dijeron nada sobre nada de eso, porque los nueve bits solo hablan de ficheros.
Esta lección construye el marco conceptual que falta. Primero la teoría, que es sorprendentemente pequeña y explica todo lo demás: sujetos, objetos, derechos, dominios y la matriz de acceso. Después los ocho principios de diseño que en 1975 fijaron el vocabulario de la seguridad de sistemas y que siguen siendo la mejor lista de comprobación que existe. Después los cuatro modelos de control de acceso que verás nombrados en cualquier documento profesional —DAC, MAC, RBAC, ABAC—. Y por último los tres mecanismos con los que Linux implementa hoy esas ideas: capabilities, que trocean el poder de root; SELinux y AppArmor, que imponen reglas que ni root puede saltarse; y seccomp, que reduce el juego de llamadas al sistema que un proceso puede siquiera intentar. Al final tendrás la respuesta a la pregunta del párrafo anterior: qué se le pone delante a un meteo-api comprometido para que sus permisos correctos no basten.
Contenido
- Protección frente a seguridad: por qué la distinción importa
- El modelo formal: sujetos, objetos, derechos y dominios de protección
- La matriz de acceso y sus dos implementaciones reales
- Los ocho principios de Saltzer y Schroeder, aplicados a Meteora
- Modelos de control de acceso: DAC, MAC, RBAC y ABAC
- El problema del "todo o nada" de root
- Capabilities de Linux: trocear el poder de root
- Control de acceso obligatorio: SELinux y AppArmor
- Confinamiento y sandboxing: seccomp y la superficie de syscalls
- Base de cómputo confiable y superficie de ataque
- El problema del delegado confuso
Protección frente a seguridad: por qué la distinción importa
En el lenguaje corriente son sinónimos. En sistemas operativos son dos cosas distintas y conviene separarlas desde el principio, porque determinan qué puedes esperar de un mecanismo.
| Protección | Seguridad | |
|---|---|---|
| Qué es | Un mecanismo interno del SO que controla el acceso de procesos y usuarios a los recursos | La propiedad global del sistema frente a un adversario que quiere violarla |
| Ámbito | Dentro de la máquina | La máquina, la red, las personas, los procedimientos |
| Pregunta que responde | ¿Puede este sujeto hacer esta operación sobre este objeto? | ¿Puede alguien conseguir lo que quiere, por la vía que sea? |
| Ejemplo en Meteora | El bit r de 2026-08-31.dat, una capability, un perfil de AppArmor |
Que nadie robe los datos: incluye lo anterior, y también el cifrado, las copias, el cortafuegos y que nadie deje la contraseña en un post-it |
| Se verifica | Con reglas y pruebas: o el mecanismo concede o deniega | Con un modelo de amenazas: quién ataca, con qué recursos y con qué objetivo |
| Falla cuando | Hay un error en el mecanismo | Hay un error en el mecanismo, o en el diseño, o en la operación, o en las personas |
La distinción no es académica; tiene una consecuencia práctica muy concreta. La protección se puede razonar y demostrar; la seguridad, no. Puedes afirmar con certeza que un proceso con UID 990 no puede escribir en un fichero root:root 0644, porque es una regla del núcleo. No puedes afirmar con la misma certeza que los datos de Meteora están seguros, porque eso depende de todo el sistema y de un adversario del que solo tienes hipótesis.
De ahí sale la regla de trabajo del módulo entero:
Un mecanismo de protección nunca es una solución de seguridad; es una capa. La seguridad se construye apilando capas independientes, de modo que el fallo de una no anule a las demás. Es lo que se llama defensa en profundidad.
Los permisos de 04-06 son una capa. Las capabilities son otra. AppArmor es otra. Seccomp otra. Ninguna es suficiente sola, y precisamente por eso ninguna es inútil.
El modelo formal: sujetos, objetos, derechos y dominios de protección
Todo control de acceso, en cualquier sistema operativo, se describe con cuatro conceptos. Aprenderlos ahorra mucho tiempo después, porque cada mecanismo concreto no es más que una implementación particular de este esquema.
Sujeto. La entidad activa que quiere hacer algo. En Linux, el sujeto real es el proceso, no el usuario: el usuario es una etiqueta que el proceso lleva encima. Cuando el agregador abre un fichero, el sujeto es ese proceso concreto con su PID, su UID efectivo 990, sus grupos suplementarios, sus capabilities y su etiqueta de SELinux.
Objeto. La entidad pasiva sobre la que se actúa. En un sistema UNIX la lista es larga y no se limita a ficheros: ficheros y directorios, procesos (se les envían señales), sockets, segmentos de memoria compartida como /dev/shm/meteora-cache, dispositivos, semáforos, colas de mensajes, entradas de /proc, y hasta el propio reloj del sistema o la tabla de rutas.
Derecho de acceso. El permiso para ejecutar una operación concreta sobre un objeto concreto: leer, escribir, ejecutar, borrar, enviar una señal, montar, conectar. El conjunto de derechos depende del tipo de objeto.
Dominio de protección. Es el concepto que suele costar y el que más rendimiento da. Un dominio es el conjunto de pares (objeto, derechos) que un sujeto puede usar en un momento dado. Es decir: no "quién eres", sino "qué puedes hacer ahora mismo".
Dominio D_ingestor = {
(/var/lib/meteora/lecturas/*.dat, {leer, escribir, crear}),
(/run/meteora/lecturas.fifo, {leer}),
(/etc/meteora/meteora.conf, {leer}),
(/dev/shm/meteora-cache, {leer, escribir}),
(socket TCP puerto 9010, {escuchar, aceptar})
}Lo importante es que un proceso cambia de dominio a lo largo de su vida, y esos cambios son la parte interesante de la seguridad. Recuerda el caso de passwd de 04-06: el proceso empieza en el dominio del usuario joan, y al hacer execve sobre un binario setuid salta al dominio de root. Eso es un cambio de dominio, y es exactamente donde se concentran los ataques: si consigues provocar un cambio de dominio que no estaba previsto, has escalado privilegios.
graph LR
A["Dominio 'joan'<br/>UID 1000<br/>lee sus ficheros"] -->|"execve de<br/>/usr/bin/passwd<br/>(setuid root)"| B["Dominio 'root'<br/>UID efectivo 0<br/>escribe /etc/shadow"]
B -->|"el proceso termina"| C["El dominio<br/>desaparece"]
A -->|"execve de<br/>/bin/ls"| D["Dominio 'joan'<br/>sin cambio"]
En Linux, un dominio se define de facto por la combinación de: UID y GID efectivos, grupos suplementarios, conjunto de capabilities, etiqueta de SELinux o perfil de AppArmor, filtro seccomp activo, límites de recursos y —desde el módulo 6— espacios de nombres. Todo lo que veremos a partir de aquí es una forma de hacer los dominios más pequeños y controlar mejor los saltos entre ellos.
La matriz de acceso y sus dos implementaciones reales
Si pones los sujetos en filas y los objetos en columnas, obtienes la matriz de acceso, que es la representación completa y abstracta de la política de protección de un sistema:
lecturas/*.dat |
meteora.conf |
meteo-api.log |
/usr/bin/meteo-api |
|
|---|---|---|---|---|
| ingestor | leer, escribir | leer | — | — |
| agregador | leer | leer | — | — |
| meteo-api | leer | leer | escribir (añadir) | ejecutar |
| nuria (análisis) | leer | — | — | — |
| root | todo | todo | todo | todo |
La matriz es un modelo excelente para pensar y un desastre para implementar: con 500 usuarios y 200.000 ficheros tendría 100 millones de celdas, casi todas vacías. Ningún sistema real la almacena entera. Todos la trocean, y solo hay dos formas de trocear una matriz.
Por columnas: listas de control de acceso (ACL). Cada objeto guarda quién puede hacer qué con él. Es lo que hacen los nueve bits de UNIX (una ACL comprimida de tres entradas) y las ACL POSIX de 04-06 (una lista de verdad). La lista vive con el objeto, en su inodo.
Objeto: /var/lib/meteora/lecturas/2026-08-31.dat ├── meteora : rw- ├── grupo meteora : r-- ├── nuria : r-- (entrada de ACL POSIX) └── otros : ---
Por filas: listas de capacidades (capability lists). Cada sujeto lleva encima una lista de "llaves", y cada llave nombra un objeto y los derechos sobre él. La llave viaja con el sujeto; el objeto no sabe quién la tiene.
Sujeto: proceso meteo-api (PID 1481)
├── llave → fd 3 : /var/lib/meteora/lecturas/2026-08-31.dat {leer}
├── llave → fd 4 : /var/log/meteora/meteo-api.log {añadir}
└── llave → fd 5 : socket TCP 8080 {aceptar}Y aquí una revelación que ordena todo lo aprendido: los descriptores de fichero de 04-04 son capacidades. Cuando open() te devuelve el descriptor 3, el núcleo te ha entregado una llave: un testigo no falsificable —no puedes inventarte un descriptor válido— que concede unos derechos concretos sobre un objeto concreto. Por eso los permisos se comprueban en open() y no en read(): la comprobación ocurre al fabricar la llave, no al usarla. Y por eso un chmod 000 posterior no corta a quien ya la tiene. Es exactamente el comportamiento característico de un sistema de capacidades, y ahora ya sabes por qué es así.
La comparación completa:
| Criterio | ACL (por columnas) | Capacidades (por filas) |
|---|---|---|
| Dónde vive la información | En el objeto (inodo, xattr) | En el sujeto (tabla de descriptores) |
| Pregunta rápida de responder | "¿Quién puede acceder a este fichero?" | "¿A qué puede acceder este proceso?" |
| Coste de la comprobación | Recorrer la lista en cada open(): O(entradas) |
O(1): la llave ya está en la mano |
| Auditar un objeto | Fácil: getfacl fichero |
Difícil: hay que inspeccionar todos los sujetos |
| Auditar un sujeto | Difícil: recorrer todo el sistema de ficheros | Fácil: ls -l /proc/<pid>/fd/ |
| Revocación | Fácil: se edita la lista y afecta a los accesos futuros | Difícil: hay que perseguir las llaves ya repartidas |
| Delegación | Necesita privilegio para editar la ACL | Natural: se pasa la llave (SCM_RIGHTS por un socket UNIX) |
| Riesgo característico | Que la lista crezca y nadie la entienda | Que una llave se filtre a quien no debía tenerla |
| Ejemplos reales | Permisos UNIX, ACL POSIX, ACL de NTFS | Descriptores de fichero, pidfd, tokens OAuth, seL4, Capsicum |
Los dos riesgos característicos merecen un comentario, porque los verás en la vida real. En el mundo ACL, el problema es la acumulación: nadie retira permisos, la lista de acceso a una carpeta compartida acaba con treinta entradas y grupos anidados, y ya nadie sabe quién entra. En el mundo capacidades, el problema es la fuga: si meteo-api hereda por descuido un descriptor abierto de /etc/meteora/meteora.conf que su padre dejó abierto, tiene la llave aunque los permisos del fichero se lo prohibieran. Por eso execve cierra los descriptores marcados con FD_CLOEXEC, y por eso conviene abrir con O_CLOEXEC por defecto (04-04): es higiene de capacidades.
Un aviso de vocabulario, para que no te confundas más adelante: las capabilities de Linux del apartado 7 se llaman así por herencia histórica, pero no son capacidades en este sentido. No nombran un objeto concreto; son permisos globales de tipo "puede hacer X en todo el sistema". Es una desafortunada colisión de nombres que conviene tener presente.
Los ocho principios de Saltzer y Schroeder, aplicados a Meteora
En 1975, Jerome Saltzer y Michael Schroeder publicaron ocho principios de diseño de sistemas protegidos. Medio siglo después no ha hecho falta añadir ninguno, y la mayoría de los desastres de seguridad que leerás en las noticias son la violación de uno de ellos. Los recorremos uno a uno, con su aplicación exacta en meteo-01.
- Mínimo privilegio
Cada sujeto debe operar con el conjunto mínimo de privilegios necesario para su tarea, y durante el mínimo tiempo.
Es el principio del que se derivan casi todos los demás. En Meteora se traduce en decisiones concretas que ya hemos tomado: meteora es una cuenta sin shell (nadie inicia sesión con ella), /etc/meteora/meteora.conf pertenece a root y el servicio solo lo lee, /usr/bin/meteo-api pertenece a root para que el servicio no pueda reescribir su propio binario, y meteo-api escucha en el 8080 para no necesitar privilegios de puerto bajo. Y el que veremos en el apartado 7: si tuviera que escuchar en el 443, la respuesta correcta es CAP_NET_BIND_SERVICE, no root.
La coletilla "y durante el mínimo tiempo" es la que más se olvida. Un proceso que necesita privilegio solo al arrancar debe soltarlo después: abrir el puerto, abrir los ficheros, y entonces bajar a UID 990 y descartar las capabilities. A partir de ese instante, un compromiso ya no las tiene.
- Economía del mecanismo
El mecanismo de protección debe ser lo más pequeño y simple posible, para poder revisarlo exhaustivamente.
El código que no existe no tiene fallos, y el código que nadie entiende no se puede auditar. Es el argumento a favor de los nueve bits de UNIX frente a una ACL de treinta entradas heredadas: un mecanismo comprensible se verifica de un vistazo. En Meteora, aplicarlo significa preferir un perfil de AppArmor de cuarenta líneas que un equipo entero entiende a una política de SELinux de seiscientas que solo entiende quien la escribió —y que acabará en modo permisivo el día que estorbe—.
- Valores por defecto seguros (denegar por defecto)
Lo permitido debe listarse explícitamente; todo lo demás se deniega. La base debe ser la falta de acceso, no su presencia.
La diferencia entre una lista negra ("prohíbe estas rutas") y una lista blanca ("permite solo estas") es que la primera falla en silencio ante todo lo que sus autores no imaginaron. Aplicado a Meteora, es lo que hace other::--- en los datos, lo que hace la política por defecto drop del cortafuegos que montaremos en 05-03, y lo que hace un perfil de AppArmor: enumera lo que meteo-api puede tocar y deniega el resto del sistema de ficheros, incluido lo que se instale mañana.
- Mediación completa
Todo acceso a todo objeto debe comprobarse, siempre, sin excepciones ni cachés que se puedan saltar.
Si el mecanismo comprueba el permiso la primera vez y luego confía, existe una ventana. Es el punto donde los descriptores de fichero de UNIX hacen una concesión deliberada: se comprueba en open() y no en cada read(), por rendimiento. La concesión está estudiada —la llave solo se entrega si el permiso existía— pero explica el efecto del chmod 000 que no corta a nadie. Y el ataque contra la mediación completa tiene nombre y ya lo conoces: TOCTOU (04-04), donde el atacante cambia el objeto entre la comprobación y el uso. La contramedida es siempre la misma: comprobar y actuar sobre el mismo descriptor, no sobre el mismo nombre.
- Diseño abierto
La seguridad no debe depender del secreto del diseño, sino del secreto de las claves.
Es el principio de Kerckhoffs, y explica por qué el algoritmo de cifrado de contraseñas de /etc/shadow está publicado hasta el último detalle y aun así el sistema funciona. Su contrario es la seguridad por oscuridad: mover SSH al puerto 2222, ofuscar el código, esconder la URL de administración. No son inútiles como ruido —quitan escaneos automáticos de encima—, pero no son un control de seguridad, porque su valor desaparece en cuanto alguien mira. Regla operativa para Meteora: cambiar el puerto de SSH está bien; hacerlo en lugar de configurar claves públicas y fail2ban está mal.
- Separación de privilegios
Cuando sea posible, exigir dos condiciones independientes para conceder un acceso, en vez de una sola.
Dos llaves para lanzar el misil. Dos firmas para una transferencia. En Meteora, es la razón de que los registros tengan grupo adm y no meteora: administrar y ejecutar son dos privilegios distintos y no se los damos a la misma identidad. También es la razón de dividir el servicio en tres procesos (ingestor, agregador, meteo-api) en lugar de uno solo: comprometer el que habla con Internet no da lo que puede hacer el que escribe en disco. Y es lo que hace la autenticación en dos factores de 05-02.
- Mínimo mecanismo común
Minimizar los mecanismos compartidos entre usuarios, porque cada recurso compartido es un canal potencial de fuga o de interferencia.
Todo lo que dos sujetos comparten es una vía por la que uno puede afectar o espiar al otro: un /tmp común, una base de datos común, una CPU con caché común —de ahí ataques como Spectre—, un segmento de memoria compartida. En Meteora, /dev/shm/meteora-cache es exactamente un mecanismo común entre los tres procesos, y por eso sus permisos y su tamaño importan tanto: es superficie compartida. La directiva PrivateTmp=yes de systemd (05-03) es este principio hecho configuración: cada servicio recibe su propio /tmp.
- Aceptabilidad psicológica
El mecanismo debe ser fácil de usar correctamente, o los usuarios lo esquivarán.
Es el principio más ignorado y el que más brechas causa. Una política que obliga a cambiar la contraseña cada 30 días produce Meteora2026! seguido de Meteora2026!!, y post-its. Un sudo que pide la contraseña cada treinta segundos produce un NOPASSWD: ALL. Un perfil de AppArmor que rompe el servicio cada vez que se despliega una versión produce aa-complain permanente. Un control que estorba se desactiva, y un control desactivado protege cero. Cuando diseñes seguridad para Meteora, pregúntate siempre cuál es el camino fácil, y asegúrate de que el camino fácil sea el seguro.
Modelos de control de acceso: DAC, MAC, RBAC y ABAC
Con el vocabulario en la mano, ya se pueden nombrar los cuatro modelos que estructuran el control de acceso en cualquier sistema profesional.
| Modelo | Quién decide la política | Idea central | Ejemplo típico | Punto débil |
|---|---|---|---|---|
| DAC Discrecional |
El propietario del objeto | Si el fichero es tuyo, tú decides quién entra | Permisos UNIX, ACL POSIX, ACL de NTFS | El dueño puede regalar el acceso; un proceso comprometido hereda esa discreción |
| MAC Obligatorio |
El administrador, mediante una política global | Ni el dueño puede saltarse la política; el sistema la impone | SELinux, AppArmor, niveles militares de clasificación | Complejo de escribir, de depurar y de mantener |
| RBAC Por roles |
El administrador, asignando roles | Los permisos se dan a roles, y las personas se asignan a roles | sudo con grupos, roles de Kubernetes, grupo adm de Meteora |
Explosión de roles si la granularidad es fina |
| ABAC Por atributos |
Un motor de reglas con atributos de sujeto, objeto y contexto | "Permite si rol=analista Y hora∈laboral Y origen=red interna" | Políticas IAM de la nube, Open Policy Agent | Difícil de auditar: el resultado depende del contexto |
La clave para entender por qué existe MAC está en la casilla "punto débil" de DAC, y merece un ejemplo concreto:
Con DAC,
meteo-apicorre comometeoraymeteoraes el dueño de los.dat. Si un atacante controla el proceso, puede hacerchmod o+rsobre los datos, o copiarlos a/tmp, o enviarlos por un socket. Tiene la discreción del dueño, porque es el dueño.Con MAC, existe además una política del sistema que dice: "un proceso etiquetado
meteo_api_tpuede leer ficheros etiquetadosmeteora_data_ty nada más; no puede escribir en/tmp, no puede ejecutar/bin/sh, no puede abrir sockets salientes". Esa regla no la puede cambiar el proceso, ni siquiera si consigue UID 0, porque no la aplica el dueño del fichero sino el núcleo con su política cargada.
Esa es toda la diferencia, y es la razón por la que el módulo no termina en 04-06. En la práctica, un servidor Linux moderno combina los cuatro: DAC en los permisos de fichero, MAC en AppArmor o SELinux, RBAC en la organización de grupos y reglas de sudo, y ABAC en las políticas de la nube que lo rodean.
El problema del "todo o nada" de root
UNIX nació con un modelo de privilegio binario: UID 0 lo puede todo, cualquier otro UID no puede nada especial. El núcleo estaba lleno de comprobaciones de la forma if (uid == 0) permitir;. Es de una economía de mecanismo admirable, y es también un problema serio.
El problema se ve con un ejemplo cotidiano. ping necesita abrir un socket ICMP en bruto, algo que un usuario normal no puede hacer. La solución clásica fue hacerlo setuid root. Resultado: un programa que cualquiera puede ejecutar, que corre con todos los privilegios del sistema, cuando lo único que necesitaba era uno. Si ping tiene un fallo, el atacante no obtiene "la capacidad de mandar paquetes ICMP": obtiene la máquina entera. La desproporción entre el privilegio necesario y el privilegio concedido es de varios órdenes de magnitud, y es una violación de manual del principio de mínimo privilegio.
Multiplica eso por los quince o veinte binarios setuid de un sistema y tendrás la superficie de ataque histórica de UNIX. La solución de Linux desde 1998 es trocear el poder de root en piezas.
Capabilities de Linux: trocear el poder de root
Linux divide los privilegios de root en unas cuarenta capabilities independientes. Cada comprobación del núcleo que antes decía "¿es UID 0?" ahora dice "¿tiene la capability tal?".
| Capability | Qué permite | Uso legítimo |
|---|---|---|
CAP_NET_BIND_SERVICE |
Escuchar en puertos < 1024 | Un servidor web o meteo-api en el 443 |
CAP_NET_RAW |
Sockets en bruto | ping, tcpdump |
CAP_NET_ADMIN |
Configurar red, interfaces, rutas, cortafuegos | ip, demonios de VPN |
CAP_CHOWN |
Cambiar el propietario de un fichero | Gestores de paquetes |
CAP_DAC_OVERRIDE |
Saltarse todos los permisos de fichero | Copias de seguridad |
CAP_DAC_READ_SEARCH |
Saltarse los permisos de lectura y travesía | Copias de seguridad de solo lectura |
CAP_KILL |
Enviar señales a cualquier proceso | Supervisores |
CAP_SETUID / CAP_SETGID |
Cambiar de identidad | Servidores que bajan de privilegio |
CAP_LINUX_IMMUTABLE |
Quitar chattr +i / +a |
Administración (04-06) |
CAP_SYS_TIME |
Cambiar el reloj del sistema | NTP |
CAP_SYS_PTRACE |
Depurar e inspeccionar la memoria de otros procesos | gdb, strace |
CAP_SYS_MODULE |
Cargar módulos del núcleo | Casi nunca |
CAP_SYS_ADMIN |
Montar, pivot_root, y decenas de operaciones más |
El "cajón de sastre" |
Las cuatro últimas están marcadas por una razón. CAP_SYS_ADMIN, CAP_SYS_MODULE, CAP_SYS_PTRACE y CAP_DAC_OVERRIDE equivalen prácticamente a root, y hay que tratarlas como tal:
CAP_SYS_MODULE: si puedes cargar un módulo, puedes ejecutar código en el núcleo. Es más que root.CAP_SYS_PTRACE: si puedes inspeccionar y modificar la memoria de otros procesos, puedes tomar el control de un proceso privilegiado.CAP_DAC_OVERRIDE: si te saltas todos los permisos de fichero, puedes reescribir/etc/shadowy/etc/sudoers.CAP_SYS_ADMIN: se convirtió en el cajón donde se metió todo lo que no encajaba en otra categoría, y hoy cubre montar sistemas de ficheros, manipularnamespacesy mucho más. Concederla es, en la práctica, conceder root con pasos intermedios.
Los cuatro conjuntos, sin misterio
Cada proceso lleva varios conjuntos de capabilities. Los que hay que entender son estos:
| Conjunto | Qué significa |
|---|---|
| Permitido (permitted) | El límite superior: lo que el proceso podría activar |
| Efectivo (effective) | Lo que el núcleo comprueba ahora mismo en cada operación |
| Heredable (inheritable) | Lo que puede sobrevivir a un execve (con el ambiental, en la práctica) |
| Limitador (bounding set) | Un techo que nunca se puede subir: lo que se quita de aquí no vuelve, ni para los hijos |
| Ambiental (ambient) | El mecanismo moderno para que un proceso no privilegiado conserve capabilities al ejecutar otro binario |
La lógica de uso es sencilla y sigue el principio de mínimo privilegio en el tiempo: un servicio arranca con lo permitido, activa en el efectivo solo lo que necesita en el instante en que lo necesita, y vacía el limitador de todo lo demás para que ni él ni ninguno de sus hijos pueda recuperarlo nunca. Ese vaciado es lo que hace CapabilityBoundingSet= en una unidad de systemd (05-03).
En la práctica: meteo-api en el puerto 443
# Sin capabilities: un servicio no root NO puede escuchar por debajo del 1024
$ sudo -u meteora /usr/bin/meteo-api --puerto 443
error: bind(0.0.0.0:443): Permission denied
# Opción A — conceder la capability al BINARIO (fichero)
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/meteo-api
getcap /usr/bin/meteo-api
# /usr/bin/meteo-api cap_net_bind_service=ep
# Ahora sí, sin ser root y sin setuid
sudo -u meteora /usr/bin/meteo-api --puerto 443 # arranca correctamente
# Ver las capabilities de un proceso en marcha
grep Cap /proc/$(pgrep -f meteo-api)/status
# CapInh: 0000000000000000
# CapPrm: 0000000000000400
# CapEff: 0000000000000400
# CapBnd: 0000003fffffffff
capsh --decode=0000000000000400
# 0x0000000000000400=cap_net_bind_serviceQué ha pasado exactamente. setcap escribe un atributo extendido security.capability en el inodo del binario —los xattr de 04-06—, y al ejecutarlo el núcleo le concede esa capability y solo esa. El proceso corre como UID 990, sin ningún privilegio de root, salvo la capacidad puntual de asociarse a un puerto bajo. Compáralo con la alternativa clásica —chmod u+s y root—: el privilegio concedido pasa de "todo el sistema" a "una operación". capsh --decode traduce la máscara hexadecimal de /proc/<pid>/status a nombres legibles, y CapBnd con todos los bits a uno indica que el limitador está intacto, algo que en un servicio bien configurado no debería ocurrir.
Hay una opción B mejor que la A, y conviene conocerla ya aunque la desarrollemos en 05-03: en lugar de marcar el fichero, se pide la capability en la unidad de systemd:
[Service]
User=meteora
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yesEs superior por tres razones. El binario del disco no queda marcado, así que copiarlo a otro sitio no arrastra privilegio. El limitador se reduce a esa única capability, de modo que ni el proceso ni sus hijos pueden obtener ninguna otra jamás. Y NoNewPrivileges=yes impide cualquier ganancia de privilegio posterior, incluido el efecto de cualquier binario setuid que el proceso ejecute.
Auditar capabilities
# ¿Qué binarios del sistema tienen capabilities asignadas?
sudo getcap -r /usr /bin /sbin 2>/dev/null
# /usr/bin/ping cap_net_raw=ep
# /usr/bin/meteo-api cap_net_bind_service=ep
# Ver las capabilities de la shell actual
capsh --print | head -5Igual que la lista de binarios setuid de 04-06, la lista de binarios con capabilities es un inventario que hay que conocer y comparar periódicamente. Un binario con cap_dac_override o cap_sys_admin aparecido de la nada es una alarma tan roja como un setuid root nuevo, y llama menos la atención porque ls -l no lo muestra: no hay ninguna s que delate el privilegio. Solo aparece con getcap.
Control de acceso obligatorio: SELinux y AppArmor
Volvamos al escenario del principio. meteo-api está comprometido, corre como meteora, y todos los permisos son correctos. Con DAC puede: leer todos los .dat, escribir en /tmp, ejecutar /bin/sh, abrir un socket a Internet y enviarse los datos. Nada de eso viola un solo bit de permiso.
El control de acceso obligatorio añade una segunda comprobación, después de la de DAC y completamente independiente de ella:
graph TB
A["El proceso pide una operación<br/>(open, connect, exec...)"] --> B{"Comprobación DAC<br/>UID, GID, bits, ACL"}
B -->|Deniega| Z["EACCES"]
B -->|Permite| C{"Comprobación MAC<br/>política de SELinux / AppArmor"}
C -->|Deniega| Y["EACCES + entrada en el registro<br/>de auditoría"]
C -->|Permite| X["Operación concedida"]
La propiedad clave está en el diagrama: hay que pasar las dos. DAC puede permitir y MAC denegar; MAC nunca concede lo que DAC deniega. Y la política de MAC la fija el administrador del sistema, no el dueño del fichero, así que un proceso con UID 0 tampoco se la salta.
Las dos implementaciones habituales en Linux:
| SELinux | AppArmor | |
|---|---|---|
| Origen | NSA; por defecto en Red Hat, Fedora, Android | Canonical/SUSE; por defecto en Ubuntu, disponible en Debian |
| Unidad de política | Etiquetas en el objeto (meteora_data_t) y en el sujeto (meteo_api_t) |
Rutas del sistema de ficheros |
| Dónde vive la etiqueta | En el xattr security.selinux del inodo |
En ningún sitio: el perfil nombra rutas |
| Consecuencia práctica | Mover un fichero conserva su etiqueta; copiarlo puede cambiarla | Mover un fichero cambia la regla que se le aplica |
| Expresividad | Muy alta: tipos, roles, MLS/MCS, transiciones | Media: suficiente para confinar servicios |
| Curva de aprendizaje | Empinada | Suave: un perfil se lee como una lista |
| Diagnóstico típico | ausearch -m AVC, sealert, restorecon |
dmesg, journalctl, aa-logprof |
| Modos | enforcing, permissive, disabled |
enforce, complain (por perfil) |
Como meteo-01 es Debian, el ejemplo natural es AppArmor. Así se ve el esqueleto de un perfil para meteo-api —incompleto a propósito, porque escribir una política completa no es el objeto de esta lección—:
# /etc/apparmor.d/usr.bin.meteo-api (esquema ilustrativo, no completo)
#include <tunables/global>
/usr/bin/meteo-api {
#include <abstractions/base>
#include <abstractions/nameservice>
network inet stream, # sockets TCP: sí
network inet6 stream,
/etc/meteora/meteora.conf r, # configuración: SOLO LECTURA
/var/lib/meteora/lecturas/*.dat r, # datos: SOLO LECTURA
/var/log/meteora/meteo-api.log aw, # registro: solo AÑADIR
/run/meteora/api.sock rw,
/dev/shm/meteora-cache rw,
deny /etc/shadow rwx, # explícito, aunque DAC ya lo impida
deny /home/** rwx,
deny /**/.ssh/** rwx,
# No hay ninguna regla 'ix' ni 'px': este perfil NO permite ejecutar nada
}Lee el perfil con atención, porque cada línea es una decisión. Las reglas son una lista blanca: lo que no aparece está denegado, incluido cualquier fichero que se instale mañana —principio 3, valores por defecto seguros—. meteora.conf es r y no rw, de modo que el proceso no puede reescribir su propia configuración aunque los permisos DAC se estropeen algún día. El registro es aw (append), la versión MAC del chattr +a de 04-06. Y lo decisivo es lo que no hay: ninguna regla de ejecución. Un meteo-api comprometido no puede lanzar /bin/sh, ni curl, ni python3, porque AppArmor deniega el execve de todo lo que no esté listado. Eso rompe la mayoría de las cadenas de ataque automatizadas, que dan por hecho que tras el fallo hay una shell.
El flujo de trabajo realista para llegar a un perfil así, sin romper producción:
sudo aa-status # ¿qué perfiles hay y en qué modo?
sudo aa-complain /usr/bin/meteo-api # modo QUEJA: registra, no bloquea
# ... dejar correr el servicio unos días con carga real ...
sudo journalctl -k | grep -i apparmor # ver qué habría bloqueado
sudo aa-logprof # refinar el perfil con lo observado
sudo aa-enforce /usr/bin/meteo-api # activar el bloqueo realEl orden importa y es la aplicación práctica del principio de aceptabilidad psicológica: se empieza en modo queja, que registra sin bloquear, se observa varios días con tráfico real —incluido el cierre de mes, la rotación de logs y el despliegue de una versión— y solo entonces se pasa a obligatorio. Un perfil escrito de golpe y activado en producción rompe el servicio, y un servicio roto por seguridad se desactiva en veinte minutos y no vuelve.
Confinamiento y sandboxing: seccomp y la superficie de syscalls
Queda una capa más, y es la más profunda. En 01-06 vimos que todo lo que un proceso puede pedirle al sistema pasa por una llamada al sistema: unas 350 en Linux x86-64. Esa es, literalmente, la superficie completa de interacción entre un programa y el núcleo.
Ahora hazte la pregunta: ¿cuántas de esas 350 necesita el ingestor? Recibe lecturas por un socket, las valida y las escribe en un fichero. Su lista real ronda las cuarenta: read, write, openat, close, accept4, recvfrom, fsync, clock_gettime, mmap, exit_group y poco más. Las 310 restantes —mount, ptrace, init_module, keyctl, bpf, kexec_load, execve…— no las usa jamás, pero están disponibles, y cada una es superficie de ataque: código del núcleo que un proceso comprometido puede intentar alcanzar, incluidos los fallos del propio núcleo.
seccomp (secure computing mode) resuelve eso permitiendo que un proceso renuncie voluntaria e irreversiblemente a un conjunto de llamadas al sistema. A partir de ese momento, cualquier intento de usarlas termina con EPERM, con SIGSYS o con la muerte del proceso, según se configure. La renuncia es irreversible —un proceso no puede quitarse su propio filtro— y se hereda por fork y execve, así que ninguna shell lanzada desde ahí escapa.
/* Idea del arranque del ingestor, en pseudocódigo comentado.
En producción se usa libseccomp, que evita escribir BPF a mano. */
#include <seccomp.h>
int main(void) {
inicializar_socket_de_estaciones(); /* 1. TODO lo que necesita */
abrir_fichero_del_dia(); /* privilegio, ANTES */
soltar_privilegios(); /* de bajar a UID 990 */
/* 2. Política: denegar por defecto, permitir lo enumerado */
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fsync), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(accept4), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(recvfrom), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
/* ... el resto de la lista blanca ... */
/* NO se permiten: execve, ptrace, mount, init_module, socket saliente */
seccomp_load(ctx); /* 3. Irreversible desde aquí */
bucle_principal(); /* 4. Ya confinado */
}La estructura de este arranque es el patrón canónico de un servicio endurecido, y merece la pena memorizarla: primero se hace todo lo que requiere privilegio (abrir el socket, abrir los ficheros), después se sueltan los privilegios, y por último se instala el filtro. El orden no es negociable: instalar seccomp antes de abrir el socket haría fracasar el arranque. A partir de seccomp_load, aunque un atacante controle por completo el flujo de ejecución del proceso, no puede llamar a execve, y por tanto no consigue una shell; no puede llamar a ptrace, y por tanto no puede inyectarse en otro proceso; no puede montar nada. Ha heredado un proceso al que le falta la mitad del sistema operativo.
Ver el estado de seccomp de un proceso en marcha:
grep Seccomp /proc/$(pgrep -f ingestor)/status
# Seccomp: 2 → 0 = desactivado, 1 = modo estricto, 2 = filtro BPFY la buena noticia práctica: no hace falta escribir C para tener seccomp. Systemd expone listas predefinidas que cubren el 95 % de los casos, y las usaremos en 05-03:
[Service]
SystemCallFilter=@system-service # lista blanca razonable para un demonio
SystemCallFilter=~@privileged @mount @module @debug @reboot @swap
SystemCallArchitectures=nativeLa primera línea parte de un conjunto predefinido pensado para servicios; las siguientes, con ~, restan grupos enteros de llamadas peligrosas; y SystemCallArchitectures=native cierra un hueco clásico, el de invocar llamadas por la ABI de 32 bits para esquivar un filtro de 64.
Base de cómputo confiable y superficie de ataque
Dos conceptos que ordenan todo lo anterior y te dan un criterio para decidir.
La base de cómputo confiable (TCB, trusted computing base) es el conjunto de componentes de los que depende la seguridad del sistema, y cuyo fallo la compromete. No es lo que "confías" en sentido coloquial: es lo que estás obligado a confiar porque no tienes forma de comprobarlo desde fuera. En meteo-01 la TCB incluye el firmware y el cargador de arranque, el núcleo de Linux con sus módulos y su política de MAC, los procesos que corren como root o con capabilities peligrosas, los binarios setuid, y también —esto sorprende— la infraestructura de actualización de paquetes y las claves con que se firman.
La regla de oro es directa: cuanto más pequeña es la TCB, más creíble es la seguridad del sistema, porque menos código hay que auditar y menos cosas pueden fallar. Es el principio 2 —economía del mecanismo— convertido en criterio de diseño, y es el argumento técnico a favor de los microkernels de 01-05: mover el controlador de red fuera del núcleo lo saca de la TCB, y un fallo suyo deja de ser una toma de control total.
La superficie de ataque es el conjunto de puntos por donde entra la entrada de un atacante. Para Meteora, enumerarla es un ejercicio de media hora que rinde muchísimo:
| Superficie | Qué la compone en meteo-01 |
Cómo se reduce |
|---|---|---|
| Red | Puerto de estaciones, API HTTP, SSH | Cortafuegos, escuchar solo en la interfaz necesaria (05-03) |
| Llamadas al sistema | ~350 disponibles por proceso | seccomp: bajar a ~40 |
| Sistema de ficheros | Todo lo que el UID 990 puede abrir | AppArmor, ProtectSystem=strict |
| Privilegio | Binarios setuid, ficheros con capabilities | nosuid, NoNewPrivileges, auditoría periódica |
| Software instalado | Cada paquete y cada dependencia | Minimizar paquetes; gestionar dependencias (05-03) |
| Personas | Cuentas con acceso, reglas de sudo |
Ciclo de vida de cuentas, mínimo privilegio (05-02) |
Cada mecanismo de esta lección ataca una fila distinta de esa tabla, y ninguno cubre dos. Eso es, exactamente, lo que significa defensa en profundidad.
El problema del delegado confuso
Terminamos con un problema clásico de 1988 que explica una familia entera de vulnerabilidades y que, una vez lo entiendes, ves por todas partes.
El planteamiento original: un compilador que se ejecuta como servicio con privilegios propios. Además de compilar, lleva una contabilidad de uso en un fichero /var/lib/compilador/factura, sobre el que tiene permiso de escritura y los usuarios no. El compilador acepta un argumento: el nombre del fichero de salida.
Un usuario le pide compilar indicando como fichero de salida... /var/lib/compilador/factura. El compilador, obediente, escribe ahí. Y puede, porque él sí tiene permiso. El usuario acaba de destruir la contabilidad sin tener ningún permiso sobre ella.
El delegado confuso es un programa privilegiado al que se engaña para que use su propia autoridad en nombre de quien no la tiene. El programa no está comprometido: hace exactamente lo que le piden. El fallo está en que confunde dos autoridades: la suya y la de quien le pide.
Aplicado a Meteora, con un caso que podría escribir cualquiera:
# ❌ meteo-api con un delegado confuso
@app.route("/exportar")
def exportar():
destino = request.args.get("destino") # ¡viene de fuera!
datos = leer_lecturas_del_dia()
with open(f"/var/lib/meteora/export/{destino}", "w") as f:
f.write(datos) # escribe con permisos de meteora
return "OK"Una petición con destino=../../../../etc/meteora/meteora.conf hace que meteo-api escriba con su propia autoridad en un sitio que el peticionario no controla. El proceso no ha sido comprometido: ha usado su permiso legítimo en nombre de un desconocido. Es la misma estructura que el CSRF en la web (el navegador es el delegado confuso, y sus cookies la autoridad), que el SSRF (el servidor es el delegado, y su posición dentro de la red la autoridad) y que muchos abusos de binarios setuid.
La versión correcta separa la autoridad de la petición:
# ✔ Delegado que no se confunde
import os, re
BASE = "/var/lib/meteora/export"
@app.route("/exportar")
def exportar():
destino = request.args.get("destino", "")
# 1. Lista blanca de forma, no lista negra de caracteres prohibidos
if not re.fullmatch(r"[a-zA-Z0-9_-]{1,64}\.csv", destino):
return "Nombre no válido", 400
# 2. Resolver y comprobar que sigue dentro de BASE
ruta = os.path.realpath(os.path.join(BASE, destino))
if os.path.commonpath([ruta, os.path.realpath(BASE)]) != os.path.realpath(BASE):
return "Ruta fuera de rango", 400
# 3. Escribir con O_NOFOLLOW y O_EXCL: ni enlaces ni sobrescrituras
fd = os.open(ruta, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o640)
with os.fdopen(fd, "w") as f:
f.write(leer_lecturas_del_dia())
return "OK"Tres defensas, y cada una tapa un hueco distinto. La validación por lista blanca acepta solo la forma esperada, en vez de intentar enumerar lo prohibido —que siempre olvida algo, como ..%2f o una codificación exótica—. La resolución con realpath y la comprobación de prefijo neutraliza los .. y los enlaces simbólicos que apunten fuera, y se hace después de resolver, no antes. Y O_NOFOLLOW | O_EXCL cierra la ventana TOCTOU de 04-04: si el destino ya existe o es un enlace, la operación falla en vez de seguir el enlace a donde el atacante quiera. Por encima de las tres está el diseño: el perfil de AppArmor del apartado 8 solo permite escribir en rutas concretas, así que aunque las tres validaciones fallaran, el núcleo denegaría la escritura en /etc/meteora/. Cuatro capas independientes para un solo fallo: eso es defensa en profundidad aplicada de verdad.
Advertencia necesaria. Todo lo de esta lección se explica para defender sistemas propios. Cualquier prueba de seguridad —incluso una tan inocente como comprobar si un servicio valida bien un parámetro— debe hacerse exclusivamente sobre sistemas de tu propiedad o con autorización expresa y por escrito del responsable. Probar sobre sistemas ajenos sin permiso es un delito en la mayoría de jurisdicciones, con independencia de la intención. Y cuando una decisión de seguridad tenga implicaciones legales o regulatorias —datos personales, retención, notificación de brechas—, consúltala con un profesional de cumplimiento normativo o con asesoría jurídica.
Errores Comunes y Consejos
Confundir "tiene los permisos correctos" con "está protegido". Los permisos limitan a qué ficheros llega un proceso. No dicen nada sobre a qué red se conecta, qué programas ejecuta o qué llamadas al sistema usa. Un meteo-api comprometido con permisos perfectos sigue pudiendo abrir una shell si nadie se lo impide.
Usar chmod u+s para resolver un problema de privilegio. Concede todos los privilegios del propietario cuando casi siempre hace falta uno. La alternativa correcta es una capability concreta —mejor aún, vía AmbientCapabilities en la unidad de systemd—, un grupo, o un socket con los permisos adecuados.
Creer que las capabilities de Linux son "capacidades" en el sentido teórico. No lo son: no nombran un objeto. CAP_DAC_OVERRIDE no es "puede leer este fichero", es "puede leer todos". Y precisamente por eso hay capabilities que equivalen a root: CAP_SYS_ADMIN, CAP_SYS_MODULE, CAP_SYS_PTRACE y CAP_DAC_OVERRIDE.
Auditar solo los binarios setuid y olvidar getcap. Un binario con capabilities no muestra ninguna marca en ls -l. Si tu inventario de privilegio solo mira la s, tienes un punto ciego. Ejecuta también getcap -r /usr /bin /sbin y guarda la lista de referencia.
Escribir un perfil de MAC de golpe y activarlo en producción. Rompe el servicio, y un control que rompe el servicio se desactiva y no vuelve. Empieza siempre en modo queja o permisivo, observa varios días con carga real —incluyendo despliegues y rotación de logs— y solo entonces pon el modo obligatorio.
Poner el sistema en permissive o complain "temporalmente" para depurar. Ese "temporalmente" dura años. Si necesitas depurar, desactiva un solo perfil, anótalo con fecha y ponle un recordatorio.
Confiar en la seguridad por oscuridad. Cambiar el puerto de SSH reduce el ruido de los escaneos automáticos, y está bien. No es un control de seguridad, y no sustituye a las claves públicas ni al cortafuegos. Diseño abierto: la seguridad está en la clave, no en el secreto del diseño.
Consejo: ordena cualquier decisión de seguridad con la pregunta de los dominios. Ante cualquier componente, pregúntate: ¿cuál es su dominio de protección exacto?, ¿cuándo cambia de dominio?, y ¿qué pasa si un atacante hereda ese dominio entero? Las tres respuestas te llevan directamente a la lista de mecanismos que necesitas.
Ejercicios
Ejercicio 1: la matriz de acceso de Meteora y su implementación
Construye la matriz de acceso completa de meteo-01 para los sujetos ingestor, agregador, meteo-api, nuria (analista) y carlos (administrador de sistemas), sobre los objetos /var/lib/meteora/lecturas/*.dat, /etc/meteora/meteora.conf, /etc/meteora/secrets.conf, /var/log/meteora/meteo-api.log, /dev/shm/meteora-cache y /run/meteora/lecturas.fifo. Después: (a) indica para cada celda no vacía con qué mecanismo concreto se implementa hoy (bits, ACL, grupo, capability); (b) señala qué celda sería imposible de expresar solo con los nueve bits y por qué; (c) explica qué cambia en la matriz si meteo-api resulta comprometido, y qué fila no cambia.
Ejercicio 2: reducir el dominio de meteo-api
meteo-api debe escuchar en el puerto 443, leer los .dat y la configuración, y escribir en su registro. Nada más. Diseña la protección completa en tres capas independientes y, para cada una, escribe la configuración concreta y explica qué ataque concreto detiene esa capa y no las otras: (1) DAC, con propietarios y modos; (2) capabilities, decidiendo entre setcap sobre el fichero y AmbientCapabilities en la unidad, y justificando la elección; (3) MAC, con el esqueleto de un perfil de AppArmor. Termina indicando qué llamadas al sistema no debería poder usar y con qué directiva de systemd lo conseguirías.
Ejercicio 3: identificar principios violados
Para cada una de estas cinco situaciones reales, di qué principio o principios de Saltzer y Schroeder se violan, qué ataque concreto lo aprovecha y cuál es la corrección:
- Un script de despliegue ejecuta
chmod -R 777 /var/lib/meteora"para que no dé problemas de permisos". - La política de la empresa obliga a cambiar la contraseña cada 30 días y prohíbe los gestores de contraseñas.
meteo-apicorre como root "porque así seguro que funciona".- La API de administración no está documentada y está en
/admin-x7f3, sin autenticación, "porque nadie la conocerá". - Los tres procesos de Meteora comparten
/tmpy se pasan ficheros temporales con nombres predecibles como/tmp/meteora-cache-hoy.
Soluciones
Solución 1
La matriz (L = leer, E = escribir, A = añadir, X = ejecutar, — = sin acceso):
*.dat |
meteora.conf |
secrets.conf |
meteo-api.log |
meteora-cache |
lecturas.fifo |
|
|---|---|---|---|---|---|---|
| ingestor | L, E | L | L | — | L, E | L (lee del FIFO) |
| agregador | L | L | — | — | L, E | — |
| meteo-api | L | L | L | A | L | — |
| nuria | L | — | — | — | — | — |
| carlos | L (vía sudo) |
L, E (vía sudo) |
L, E (vía sudo) |
L (grupo adm) |
— | — |
(a) Mecanismos. Las celdas de los tres procesos se implementan con DAC clásico: los tres corren con UID 990 y GID 990, y los ficheros son meteora:meteora 0640, salvo la configuración que es root:meteora 0640 —lectura por el grupo, escritura solo root—. La celda de nuria es una entrada de ACL POSIX (setfacl -m u:nuria:r) más su ACL por defecto para los ficheros futuros. La celda de carlos sobre los registros es pertenencia al grupo adm, y las demás son reglas de sudo (05-02), no acceso directo. Si meteo-api escuchara en el 443, aparecería una capability, CAP_NET_BIND_SERVICE, que no es una celda de esta matriz sino un privilegio global sobre el sistema: es justo la anomalía que comentábamos en el apartado 7.
(b) La celda imposible con nueve bits es la de nuria sobre los .dat. Los nueve bits solo expresan permisos para un usuario, un grupo y el resto; el usuario ya es meteora y el grupo también, así que la única forma de darle acceso a una cuarta identidad sería meterla en el grupo meteora —lo que le daría además meteora.conf y secrets.conf— o abrir other —lo que se lo daría a todo el sistema—. Es exactamente el desbordamiento del modelo de tres clases que motiva las ACL, y por eso hace falta una entrada nominal.
(c) Si meteo-api se compromete, su fila no cambia formalmente: el atacante hereda exactamente esos derechos. Y eso es lo grave, porque incluye secrets.conf con las claves de API, todos los .dat y meteora-cache. Pero el atacante obtiene además todo lo que la matriz no representa: ejecutar /bin/sh, abrir sockets salientes, escribir en /tmp, leer /etc/passwd, y persistir. La fila que no cambia es la de carlos: sus privilegios pasan por sudo y exigen una autenticación adicional que el atacante no tiene. Esa es la lección del ejercicio: la matriz de acceso describe DAC, y DAC no es toda la historia; para acotar lo que la matriz no ve hacen falta AppArmor y seccomp.
Solución 2
Capa 1 — DAC:
sudo chown root:root /usr/bin/meteo-api && sudo chmod 0755 /usr/bin/meteo-api
sudo chown root:meteora /etc/meteora/meteora.conf && sudo chmod 0640 /etc/meteora/meteora.conf
sudo chown -R meteora:meteora /var/lib/meteora
sudo find /var/lib/meteora -type d -exec chmod 2750 {} +
sudo find /var/lib/meteora -type f -exec chmod 0640 {} +
sudo chown meteora:adm /var/log/meteora/meteo-api.log && sudo chmod 0640 /var/log/meteora/meteo-api.log
sudo chattr +a /var/log/meteora/meteo-api.logQué detiene esta capa y no las otras: el acceso de otros usuarios y otros servicios del sistema a los datos de Meteora. Si mañana se instala otra aplicación con su propia cuenta de servicio, no podrá leer un solo .dat. Es la capa que separa inquilinos dentro de la misma máquina, y ni las capabilities ni AppArmor la sustituyen.
Capa 2 — capabilities. La elección correcta es AmbientCapabilities en la unidad, no setcap sobre el fichero:
[Service]
User=meteora
Group=meteora
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yesRazones de la elección: con setcap, el privilegio queda grabado en el inodo del binario, de modo que cualquiera que lo ejecute lo obtiene y una copia del fichero podría arrastrarlo; con AmbientCapabilities, el privilegio lo concede systemd solo a este servicio, el binario del disco queda limpio, y CapabilityBoundingSet además impide para siempre que el proceso o sus hijos adquieran cualquier otra capability.
Qué detiene esta capa y no las otras: la escalada a root. Sin ella, la forma "fácil" de escuchar en el 443 sería correr como root o con setuid, y entonces un fallo en el análisis de peticiones daría el sistema entero. Con ella, el proceso comprometido no puede cargar módulos, ni montar, ni saltarse los permisos de fichero, ni depurar otros procesos. NoNewPrivileges=yes remata: aunque el proceso ejecutara un binario setuid, no ganaría privilegio.
Capa 3 — MAC (esquema de AppArmor):
/usr/bin/meteo-api {
#include <abstractions/base>
network inet stream,
/etc/meteora/meteora.conf r,
/var/lib/meteora/lecturas/*.dat r,
/var/log/meteora/meteo-api.log aw,
/run/meteora/api.sock rw,
deny /etc/shadow rwx,
deny /home/** rwx,
deny /tmp/** wx,
# sin reglas de ejecución: no puede lanzar NINGÚN programa
}Qué detiene esta capa y no las otras: el abuso de los permisos legítimos. DAC permite a meteora escribir en /tmp, leer /etc/passwd y ejecutar /bin/sh; las capabilities no dicen nada de eso porque no son privilegios de root. AppArmor lo deniega punto por punto, y sobre todo corta la obtención de una shell, que es el primer paso de casi cualquier cadena de ataque automatizada.
Llamadas al sistema que no debería poder usar: execve/execveat (nada que ejecutar), ptrace (nada que depurar), mount/umount2, init_module/finit_module/delete_module, kexec_load, keyctl, bpf, setuid/setgid tras el arranque, y unshare/setns. En systemd:
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @mount @module @debug @reboot @swap @obsolete
SystemCallArchitectures=nativeLas tres capas son independientes: si el perfil de AppArmor se descarga por error, DAC y las capabilities siguen ahí; si los permisos se estropean en un despliegue, AppArmor sigue denegando. Eso es lo que hace que la suma valga más que las partes.
Solución 3
1. chmod -R 777. Viola mínimo privilegio (concede a todo el sistema el máximo derecho posible), valores por defecto seguros (invierte la política: en vez de denegar por defecto, permite todo) y separación de privilegios (borra la distinción entre quien ejecuta el servicio y cualquier otro). El ataque: cualquier cuenta de la máquina —incluida la de otro servicio comprometido— puede modificar o borrar los datos, y los ficheros quedan además ejecutables, lo que habilita el vector "escribo un binario y espero a que alguien lo lance". Corrección: chmod -R u=rwX,g=rX,o= o separar por tipo con find -type d/-type f, y diagnosticar el permiso real con namei -l en vez de abrir de par en par.
2. Caducidad a 30 días sin gestor de contraseñas. Viola la aceptabilidad psicológica y, de rebote, el mínimo privilegio —porque produce contraseñas reutilizadas entre sistemas—. El resultado observable es Meteora2026! → Meteora2026!! → Meteora2026!!!, contraseñas apuntadas en post-its y en documentos compartidos, y reutilización entre servicios. Corrección alineada con las guías modernas: contraseñas largas en vez de complejas, sin caducidad periódica obligatoria, cambio solo ante indicio de compromiso, comprobación contra listas de contraseñas filtradas, y fomentar el gestor de contraseñas más segundo factor. Se detalla en Usuarios, Autenticación y Escalada de Privilegios.
3. meteo-api como root. Viola mínimo privilegio de la forma más directa posible, y también economía del mecanismo —toda la seguridad pasa a depender de que no haya un solo fallo en el código de la aplicación— y mínimo mecanismo común —comparte la identidad más poderosa con el resto del sistema—. El ataque: cualquier fallo de análisis de una petición HTTP se convierte en control total de la máquina, sin ningún paso intermedio. Corrección: User=meteora, la capability concreta si hace falta un puerto bajo, NoNewPrivileges=yes y confinamiento MAC.
4. La API en /admin-x7f3 sin autenticación. Viola el diseño abierto (la seguridad depende del secreto de la ruta, no de una credencial) y la mediación completa (hay un camino que no pasa por ninguna comprobación de acceso). El ataque no requiere adivinar nada: la ruta se filtra por los registros del proxy, por el historial del navegador, por una captura de pantalla en un ticket, por el Referer de una petición o por un escaneo de rutas. Corrección: autenticación real en ese punto —la ruta puede seguir siendo poco evidente, eso no molesta—, restricción por red de origen y registro de todos los accesos.
5. /tmp compartido con nombres predecibles. Viola el mínimo mecanismo común (los tres procesos comparten un recurso que además comparten con todo el sistema) y prepara un TOCTOU de manual: cualquier usuario puede crear /tmp/meteora-cache-hoy antes que Meteora, o sustituirlo por un enlace simbólico a otro fichero, provocando que el servicio escriba donde el atacante quiera —con la autoridad de meteora, lo que además lo convierte en un delegado confuso—. Corrección en tres frentes: PrivateTmp=yes en las unidades de systemd, de modo que cada servicio reciba su propio /tmp aislado; ficheros temporales con mkstemp() en lugar de nombres fijos; y para la comunicación real entre los procesos, los mecanismos de IPC del módulo 3 —el FIFO /run/meteora/lecturas.fifo y /dev/shm/meteora-cache, con permisos 0750 y 0660 en un directorio propio— en vez de ficheros sueltos en un directorio público.
Conclusión
La protección es el mecanismo interno con que el SO controla accesos; la seguridad es la propiedad global frente a un adversario. La primera se puede demostrar, la segunda solo se puede razonar con un modelo de amenazas, y de ahí la regla que gobierna todo el módulo: ningún mecanismo es una solución, todos son capas.
Todo control de acceso se describe con cuatro piezas: sujetos (procesos, no usuarios), objetos (ficheros, pero también sockets, procesos y memoria compartida), derechos y dominios de protección —el conjunto de pares (objeto, derechos) vigente ahora mismo—. Lo interesante son los cambios de dominio: el execve de un binario setuid es uno, y es donde se concentra la escalada de privilegios. Puestos en una tabla, sujetos y objetos forman la matriz de acceso, que ningún sistema almacena entera y todos trocean de una de dos maneras: por columnas son las ACL —la información vive en el objeto, se audita bien "¿quién accede a esto?" y se revoca fácil—; por filas son las capacidades —la llave vive en el sujeto, la comprobación es O(1), la delegación es natural y la revocación es difícil—. Los descriptores de fichero de UNIX son capacidades, y eso explica de una vez por qué el permiso se comprueba en open() y no en read().
Los ocho principios de Saltzer y Schroeder siguen siendo la mejor lista de comprobación que existe: mínimo privilegio (y durante el mínimo tiempo), economía del mecanismo, valores por defecto seguros, mediación completa —cuyo enemigo tiene nombre, TOCTOU—, diseño abierto frente a la seguridad por oscuridad, separación de privilegios, mínimo mecanismo común y aceptabilidad psicológica, el más ignorado y el que más brechas causa, porque un control que estorba se desactiva. Sobre ellos se construyen los cuatro modelos: DAC (decide el dueño; su punto débil es que un proceso comprometido hereda esa discreción), MAC (decide el administrador y ni root se la salta), RBAC (permisos a roles) y ABAC (reglas con atributos y contexto). Un servidor real los combina todos.
Y los tres mecanismos de Linux, cada uno atacando una superficie distinta. Las capabilities trocean el "todo o nada" de root en unas cuarenta piezas: CAP_NET_BIND_SERVICE deja a meteo-api escuchar en el 443 sin ser root, mientras que CAP_SYS_ADMIN, CAP_SYS_MODULE, CAP_SYS_PTRACE y CAP_DAC_OVERRIDE equivalen prácticamente a root y hay que tratarlas así; se conceden mejor con AmbientCapabilities que con setcap, y se auditan con getcap porque ls -l no las muestra. SELinux y AppArmor añaden una segunda comprobación obligatoria después de DAC —etiquetas frente a rutas— que convierte los permisos correctos en insuficientes para el atacante: un perfil sin reglas de ejecución impide que un meteo-api comprometido obtenga una shell. Y seccomp reduce la superficie más profunda de todas, de ~350 llamadas al sistema a las ~40 que el ingestor usa de verdad, con el patrón canónico de arranque: privilegio primero, soltar después, filtrar al final. Todo ello se ordena con dos ideas: hacer pequeña la TCB y enumerar la superficie de ataque. Y el delegado confuso recuerda que un programa privilegiado puede ser abusado sin estar comprometido, simplemente por confundir su autoridad con la de quien le pide algo.
Con esto tenemos el marco. Pero fíjate en que todo él descansa sobre una palabra que no hemos definido: identidad. Un dominio de protección se asigna a un sujeto, y el sujeto se identifica por un UID. Hemos hablado de meteora, de nuria y de carlos como si el sistema supiera quiénes son, cuando lo único que sabe el núcleo son los números 990, 1002 y 1001. ¿De dónde salen esos números? ¿Qué ocurre exactamente entre teclear una contraseña y tener una shell? ¿Cómo se guarda esa contraseña para que ni root pueda leerla? ¿Y cómo se sube de un dominio a otro de forma controlada y auditable, que es lo que hace sudo cincuenta veces al día?
Eso es Usuarios, Autenticación y Escalada de Privilegios, donde diseccionaremos /etc/passwd, /etc/shadow y /etc/group campo a campo, veremos cómo PAM encadena los módulos que deciden si entras, cómo funciona la autenticación por clave pública de SSH, y qué categorías de error de configuración convierten una cuenta normal en root sin que nadie se dé cuenta.
Fundamentos de Sistemas Operativos
Módulo 1: Introducción a los Sistemas Operativos
- Conceptos Básicos de Sistemas Operativos
- Historia y Evolución de los Sistemas Operativos
- Tipos de Sistemas Operativos
- Funciones Principales de un Sistema Operativo
- Arquitectura del Núcleo: Monolítico, Microkernel e Híbrido
- Modo Usuario, Modo Núcleo y Llamadas al Sistema
Módulo 2: Gestión de Recursos
- Gestión de Procesos
- Planificación de la CPU
- Gestión de Memoria
- Memoria Virtual y Paginación
- Gestión de Almacenamiento
- Gestión de Dispositivos
- Controladores, Interrupciones y Operaciones de E/S
Módulo 3: Concurrencia
- Conceptos de Concurrencia
- Hilos y Procesos
- Comunicación entre Procesos (IPC)
- Sincronización y Exclusión Mutua
- Problemas Clásicos de Concurrencia
- Interbloqueos: Prevención, Detección y Recuperación
Módulo 4: Estructuras de Archivos
- Sistemas de Archivos
- Estructuras de Directorios
- Particiones, Montaje y Sistema de Archivos Virtual
- Gestión de Archivos
- Asignación de Espacio, Journaling e Integridad
- Seguridad y Permisos de Archivos
Módulo 5: Protección y Seguridad del Sistema
- Principios de Protección y Control de Acceso
- Usuarios, Autenticación y Escalada de Privilegios
- Amenazas Comunes y Endurecimiento del Sistema
- Auditoría, Registros y Respuesta a Incidentes
Módulo 6: Virtualización y Contenedores
- Virtualización: Hipervisores y Máquinas Virtuales
- Contenedores: Namespaces y cgroups
- El Sistema Operativo en la Nube
- Sistemas Operativos Móviles y de Tiempo Real
