SELinux y AppArmor:
control de acceso obligatorio sin misterio
Linux avanzado | unfantasmaenelsistema.com
Casi todo el que administra Linux se ha topado alguna vez con un «Permission denied» imposible: el usuario es el correcto, los permisos del fichero son 644, el propietario es el que toca, y aun así el servicio se niega a arrancar. Cuando `ls -l` e `id` dicen que todo está bien pero el sistema sigue diciendo que no, la explicación casi siempre es la misma: hay una segunda capa de control de acceso por encima de la clásica, y esa capa es SELinux o AppArmor.
Estas dos tecnologías implementan lo que se llama control de acceso obligatorio (MAC). En este artículo vamos a quitarles el aura de «cosa que se desactiva en el primer paso de cualquier tutorial»: qué problema resuelven, en qué se diferencian del modelo de permisos de toda la vida, cómo se leen sus decisiones y cómo se depura un bloqueo sin recurrir al `setenforce 0` que tantos disgustos ha dado.
Qué aprenderás en este artículo
La diferencia real entre control de acceso discrecional (DAC) y obligatorio (MAC) · Cómo funciona SELinux: contextos, tipos, type enforcement, booleans y modos · Cómo funciona AppArmor: perfiles basados en rutas, modos complain y enforce · Qué distribuciones usan cada uno y por qué · Cómo diagnosticar un «permission denied» que no es de permisos Unix
1. DAC frente a MAC: por qué hace falta una segunda capa
El modelo de permisos clásico de Unix —usuario, grupo, otros; lectura, escritura, ejecución— es control de acceso discrecional (DAC, *Discretionary Access Control*). Se llama discrecional porque el dueño de un recurso decide a su discreción quién accede a él: si tú creas un fichero, puedes hacer `chmod 777` sobre él y abrirlo al mundo. El sistema confía en el criterio de cada propietario.
Ese modelo tiene dos agujeros estructurales. El primero es root: el superusuario ignora por completo el DAC, así que cualquier proceso que se ejecute como root —y son muchos: servidores web, gestores de paquetes, demonios de sistema— tiene acceso total. El segundo es que el DAC razona sobre *identidades*, no sobre *comportamientos*: no hay forma de expresar «el servidor web puede leer sus páginas pero jamás debe tocar `/etc/shadow` ni abrir un puerto saliente».
El control de acceso obligatorio (MAC, *Mandatory Access Control*) cambia esa lógica. Una política central, definida por el administrador del sistema y no por los dueños de los ficheros, declara exactamente qué puede hacer cada programa. Esa política es de obligado cumplimiento: aunque un proceso corra como root, aunque un fichero tenga permisos 777, si la política no autoriza esa operación concreta, el kernel la deniega.
La idea clave en una frase
El DAC responde a «quién eres tú»; el MAC responde a «qué es correcto que este programa haga», y se aplica además del DAC, nunca en su lugar — una operación tiene que pasar los dos controles
El caso de uso canónico es la contención de daños. Si un atacante consigue ejecutar código a través de una vulnerabilidad en tu servidor web, con solo DAC hereda todo lo que puede hacer el usuario del servicio. Con MAC bien configurado queda encerrado en lo que la política permite a ese servicio: leer el *docroot*, escribir en su directorio de logs y poco más. El confinamiento no evita la vulnerabilidad, pero limita drásticamente lo que se puede hacer con ella.
2. SELinux: etiquetas por todas partes
SELinux (*Security-Enhanced Linux*) nació en la NSA y hoy lo mantiene una comunidad amplia. Es el MAC por defecto en la familia Red Hat: RHEL, Rocky, AlmaLinux, Fedora y CentOS Stream, y también en Android. Su modelo es potente y granular, a cambio de una curva de aprendizaje empinada.
2.1 Todo tiene un contexto
SELinux asigna una etiqueta —el contexto de seguridad— a cada proceso, fichero, puerto, socket y prácticamente cualquier objeto del sistema. Puedes verlo con la opción `-Z`, que casi todas las herramientas base entienden.
# Contexto de los ficheros de un directorio ls -Z /var/www/html # -> unconfined_u:object_r:httpd_sys_content_t:s0 index.html # Contexto de los procesos en ejecucion ps -eZ | grep httpd # -> system_u:system_r:httpd_t:s0 1234 ? 00:00:01 httpd # Contexto de tu propia sesion id -Z # -> unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
Un contexto tiene la forma `usuario:rol:tipo:nivel`. En la práctica diaria el campo que importa es el tercero, el tipo (todos acaban en `_t`). El usuario y el rol de SELinux no son el usuario y el grupo de Unix: son categorías internas de la política. El nivel solo entra en juego con MLS/MCS, que la mayoría de sistemas no usan de forma explícita.
2.2 Type enforcement: la regla de oro
El corazón de SELinux es el type enforcement. La política es esencialmente una lista enorme de reglas del tipo «un proceso con el tipo `httpd_t` puede leer ficheros con el tipo `httpd_sys_content_t`». Si el tipo del proceso y el tipo del objeto no tienen una regla que los conecte para esa acción concreta, la operación se deniega y se registra.
De ahí viene el «permission denied» clásico: mueves tu web a `/srv/misitio`, los ficheros conservan un tipo genérico como `var_t` o `default_t`, y `httpd_t` no tiene permiso para leer ese tipo. Los permisos Unix son perfectos; el que dice que no es SELinux. La solución no es desactivar nada, es corregir la etiqueta.
# Anadir una regla persistente: este directorio es contenido web semanage fcontext -a -t httpd_sys_content_t '/srv/misitio(/.*)?' # Aplicar la etiqueta a lo que ya existe en disco restorecon -Rv /srv/misitio # Ver que tipos de puerto tiene permitido usar el servicio web semanage port -l | grep http
2.3 Booleans: interruptores sin reescribir la política
Muchos comportamientos opcionales se controlan con booleans: variables on/off que activan o desactivan bloques de reglas ya escritos. Por ejemplo, por defecto el servidor web no puede hacer conexiones de red salientes (para frenar exfiltración y SSRF); si tu aplicación legítimamente necesita eso, activas el boolean correspondiente en lugar de tocar la política.
# Listar booleans y su estado (aqui, los del servidor web) getsebool -a | grep httpd # Permitir que el servicio web haga conexiones de red salientes # -P lo hace persistente tras reinicios setsebool -P httpd_can_network_connect on
2.4 Modos: enforcing, permissive y disabled
| Modo | Qué hace | Cuándo usarlo |
|---|---|---|
| enforcing | Aplica la política y bloquea lo no permitido | Producción, siempre que sea viable |
| permissive | No bloquea nada, pero registra todo lo que habría bloqueado | Diagnóstico y ajuste de política |
| disabled | SELinux apagado, deja de etiquetar | Casi nunca; reactivarlo exige reetiquetar todo el disco |
La herramienta de diagnóstico es el modo permissive, no el apagado. En permissive el sistema funciona con normalidad y a la vez te deja un rastro completo de cada denegación que se produciría en enforcing, para que puedas construir la política correcta antes de volver a activarlo.
# Estado actual getenforce sestatus # Cambiar a permissive solo hasta el proximo reinicio (para depurar) setenforce 0 # Cambio permanente: editar SELINUX= en el fichero de config sudoedit /etc/selinux/config
El reetiquetado tras un disabled
Si SELinux ha estado en ‘disabled’, los ficheros creados en ese periodo no tienen contexto válido · Al reactivarlo hay que forzar un reetiquetado completo del sistema de ficheros, creando el fichero ‘/.autorelabel’ y reiniciando · En discos grandes ese primer arranque puede tardar bastante — planifícalo, no lo hagas en caliente sobre un servidor crítico
3. AppArmor: perfiles atados a rutas
AppArmor es el MAC por defecto en Debian, Ubuntu, openSUSE y SUSE Linux Enterprise. Persigue el mismo objetivo que SELinux —confinar programas— pero con un modelo deliberadamente más sencillo de leer y de escribir.
3.1 Perfiles en lugar de etiquetas
AppArmor no etiqueta el sistema de ficheros. En su lugar, cada programa confinado tiene un perfil: un fichero de texto en `/etc/apparmor.d/` que enumera, mediante rutas, a qué puede acceder el proceso y con qué permisos. Es la diferencia conceptual grande frente a SELinux: AppArmor razona sobre *dónde está* un fichero, no sobre *qué etiqueta lleva*.
# Fragmento simplificado de un perfil de AppArmor
/usr/sbin/miservicio {
#include <abstractions/base>
/etc/miservicio/** r, # leer su configuracion
/var/lib/miservicio/** rw, # leer y escribir sus datos
/var/log/miservicio.log w, # escribir su log
network inet stream, # sockets TCP
# todo lo que no aparece aqui, queda denegado
}Esa legibilidad tiene un coste en granularidad: como el modelo se apoya en rutas, un enlace duro o un `bind mount` pueden dar acceso a un fichero por un camino que el perfil no contemplaba. SELinux, al viajar la etiqueta con el inodo, no tiene ese problema. Es el compromiso clásico entre simplicidad y hermetismo.
3.2 Modos: complain y enforce
Aquí AppArmor brilla en ergonomía. Cada perfil, de forma individual, puede estar en modo *enforce* (aplica) o *complain* (solo registra lo que bloquearía). No es un interruptor global como el enforcing/permissive de SELinux: puedes tener el 90 % de los perfiles aplicando y dejar en complain solo el que estás ajustando.
# Ver el estado de todos los perfiles aa-status # Poner un perfil concreto en modo aprendizaje aa-complain /usr/sbin/miservicio # Volver a aplicarlo cuando este afinado aa-enforce /usr/sbin/miservicio
3.3 Generar y afinar perfiles
AppArmor incluye dos asistentes que hacen el trabajo pesado. `aa-genprof` arranca un perfil nuevo y te pide que ejercites el programa en otra terminal (que use sus ficheros, abra sus puertos) para observar qué necesita. `aa-logprof` lee los registros de denegaciones acumulados y te propone, entrada por entrada, añadir la regla, generalizarla con un patrón o rechazarla.
# Crear un perfil nuevo de forma interactiva sudo aa-genprof /usr/sbin/miservicio # Revisar denegaciones registradas y actualizar perfiles existentes sudo aa-logprof # Recargar un perfil tras editarlo a mano sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.miservicio
4. SELinux o AppArmor: comparativa
| Aspecto | SELinux | AppArmor |
|---|---|---|
| Modelo | Etiquetas (contextos) sobre cada objeto | Perfiles por programa basados en rutas |
| Distribuciones | RHEL, Fedora, Rocky, Alma, CentOS, Android | Debian, Ubuntu, openSUSE, SLES |
| Granularidad de modo | Global (enforcing / permissive / disabled) | Por perfil (enforce / complain) |
| Curva de aprendizaje | Empinada; muchos conceptos nuevos | Suave; los perfiles se leen casi como prosa |
| Robustez ante trucos de rutas | Alta: la etiqueta viaja con el inodo | Menor: enlaces y bind mounts pueden esquivar reglas |
| Herramientas de ajuste | audit2allow, semanage, sealert, restorecon | aa-genprof, aa-logprof, aa-complain, aa-enforce |
| Cobertura por defecto | Todo el sistema (targeted policy) | Solo los programas con perfil instalado |
No se eligen: vienen con la distribución
En la práctica no instalas uno u otro según preferencia: usas el que trae tu distribución, porque toda su política base y sus paquetes ya vienen preparados para él · Cambiar de MAC en una distro que no es la suya nativa es posible pero raro y con poco soporte · Lo útil es saber manejar el que te toca, y entender el otro para cuando administres ambos mundos
5. Depurar un «permission denied» que no es de permisos
El síntoma típico: un servicio recién instalado o recién reconfigurado falla al arrancar, al abrir un fichero o al escuchar en un puerto, y los permisos Unix están impecables. Este es el procedimiento ordenado.
5.1 Confirmar que el MAC es el culpable
- Comprueba si está activo: `getenforce` (SELinux) o `aa-status` (AppArmor)
- Ponlo temporalmente en modo no bloqueante — `setenforce 0` o `aa-complain` sobre el perfil — y reproduce la operación
- Si con eso funciona, ya sabes que la causa es la política MAC; vuelve a activarlo y pasa a construir la regla correcta. No lo dejes desactivado como «solución»
5.2 SELinux: leer las denegaciones y proponer política
# Denegaciones recientes, en lenguaje humano sudo sealert -a /var/log/audit/audit.log # Buscar las entradas AVC (denegaciones) en el log de auditoria sudo ausearch -m AVC -ts recent # Generar una regla que permita EXACTAMENTE lo denegado sudo ausearch -m AVC -ts recent | audit2allow -M mi_modulo_local # revisa mi_modulo_local.te ANTES de cargarlo: sudo semodule -i mi_modulo_local.pp
audit2allow no es copiar y pegar a ciegas
audit2allow te da la regla que hace desaparecer el error, no necesariamente la regla correcta: si el bloqueo lo estaba causando un intento de acceso ilegítimo, permitirlo abre justo el agujero que SELinux acababa de tapar · Lee siempre el fichero .te que genera y entiende qué tipo y qué acción estás autorizando · Cuando exista un boolean para ese caso (por ejemplo httpd_can_network_connect), usarlo es mejor que cargar un módulo local a medida
5.3 AppArmor: revisar el perfil que bloquea
# Denegaciones de AppArmor en el diario del sistema sudo journalctl -k | grep -i apparmor | grep -i denied # -> apparmor="DENIED" operation="open" profile="/usr/sbin/miservicio" # name="/srv/datos/x" requested_mask="r" ... # Procesar esas denegaciones y actualizar el perfil de forma guiada sudo aa-logprof
El log de AppArmor es directo: te dice el perfil implicado, la operación (open, exec, connect…) y la ruta o recurso exactos. Con eso, `aa-logprof` te propone la línea que falta en el perfil; tú decides si la añades tal cual, la generalizas con un patrón o la rechazas.
5.4 La regla que nunca falla
Ante un bloqueo MAC, el orden correcto de preferencia es: (1) corregir la etiqueta o la ruta si el recurso simplemente está en un sitio no estándar; (2) activar el boolean o la abstracción prevista para ese caso de uso; (3) como último recurso, añadir una regla local mínima y revisada. Desactivar el MAC entero no está en la lista: es cambiar un problema acotado y con solución conocida por una superficie de ataque mayor en todo el sistema.
Conclusión
SELinux y AppArmor no son obstáculos que los tutoriales te enseñan a apagar en el primer paso: son la capa que convierte una vulnerabilidad en un servicio en un incidente contenido en lugar de un compromiso total. El DAC dice quién eres; el MAC dice qué es razonable que ese programa haga, y lo hace cumplir aunque el proceso sea root.
SELinux etiqueta cada objeto del sistema y razona sobre tipos; AppArmor escribe perfiles legibles atados a rutas. Uno es más hermético, el otro más fácil de vivir. No se elige entre ellos: se usa el que trae la distribución y se aprende a diagnosticar sus bloqueos con permissive/complain, los logs de auditoría y las herramientas de ajuste.
La próxima vez que veas un «permission denied» que no cuadra con `ls -l`, ya sabes dónde mirar — y ya sabes que `setenforce 0` es para depurar cinco minutos, no para dejarlo así.
Referencias
El proyecto SELinux (github.com/SELinuxProject) y la guía de SELinux de Red Hat · Wiki oficial de AppArmor (gitlab.com/apparmor/apparmor/-/wikis/home) · Páginas de manual: selinux(8), semanage(8), audit2allow(1), apparmor.d(5), aa-logprof(8) · ‘The SELinux Coloring Book’ — introducción visual a contextos y type enforcement
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

