SELinux sin Miedo: Politicas Personalizadas para Servicios Criticos
De la auditoria con audit2allow a modulos de policy versionados y mantenidos en produccion, sin caer en el permissive eterno.

Cada vez que un servicio se rompe en RHEL o Rocky, el reflejo del equipo de guardia es el mismo: setenforce 0, problema resuelto, ticket cerrado. Seis meses despues el cluster entero corre en permissive, nadie recuerda por que, y el informe de compliance se convierte en ciencia ficcion. El equipo Basilisk OffSec paso dos anos derribando ambientes asi en red teams autorizados, y la conclusion es directa: un SELinux desactivado es uno de los caminos mas fiables de una sola RCE a la comprometida total. Este post muestra como escribir policies personalizadas para servicios criticos sin romper produccion, y por que setenforce 0 no es una solucion sino una factura aplazada.
Enforcing sobre permissive: por que importa
SELinux es Mandatory Access Control: aun cuando un proceso corre como root, la policy limita que tipos puede leer, escribir y ejecutar. Eso es exactamente lo que rompe una cadena de exploit, porque un httpd_t comprometido simplemente no puede leer shadow_t ni escribir en bin_t. Permissive solo registra, no bloquea nada, asi que un cluster en permissive esta funcionalmente desprotegido. El estado objetivo es siempre Enforcing, verificable con getenforce y sestatus. Un host enforcing bien mantenido no es un obstaculo para la operacion, es la ultima defensa de perimetro cuando se explota una vulnerabilidad de aplicacion; complementa el trabajo base de Hardening de Linux Server: CIS Benchmark Aplicado sin Romper Produccion.
Leer la policy existente antes de escribir
Antes de escribir cualquier policy, necesitas leer lo que ya existe. El comando seinfo -t lista alrededor de 5.000 tipos en RHEL 9 estandar, y sesearch --allow -s httpd_t muestra exactamente lo que Apache puede tocar. seinfo -ahttpd_t -x resuelve los atributos de un dominio, y sesearch --allow -s httpd_t -t etc_t -c file responde con precision si un acceso ya esta permitido. Iniciamos toda investigacion con estas consultas, porque en el 80% de los casos ya existe un tipo o boolean adecuado, y no necesitas escribir una policy nueva, solo corregir el label o accionar el switch.
Capturar los AVC de forma limpia
Cuando algo falta de verdad, capturas los denials en vez de adivinar. Corre el servicio con ausearch -m AVC -ts recent en paralelo y pon el dominio afectado en modo permissive TEMPORARIO, jamas el sistema entero: semanage permissive -a httpd_t. Asi solo este servicio corre sin trabas y registra cada violacion, mientras el resto del sistema queda enforcing. Reproduce el caso de uso completo (arranque, reload, todos los codepaths), junta los AVC y quita la excepcion de inmediato con semanage permissive -d httpd_t. Una entrada permissive olvidada es tan peligrosa como un SELinux desactivado globalmente.
audit2allow es de doble filo
audit2allow es un arma de doble filo. Ejecutar ausearch -m AVC | audit2allow -M mimodulo genera un .te que compila y funciona, pero frecuentemente concede permisos absurdos tipo allow httpd_t shadow_t:file read. Nuestro checklist interno exige que cada .te pase por revision manual antes del semodule -i. Busca reglas que toquen shadow_t, etc_t, kernel_t o self:capability sys_admin, esas son banderas rojas. Permitir un denial porque el servicio no arranca es comodo y a menudo el origen mismo del hueco que un atacante necesitara despues. Ante cada regla pregunta: por que el proceso quiere esto, y es el acceso realmente necesario?
Policy desde cero con refpolicy
Para servicios nuevos, preferimos escribir policy desde cero con el lenguaje macro del refpolicy. Un modulo tipico tiene tres archivos: miservicio.te con las reglas, miservicio.fc con file contexts y miservicio.if con interfaces para otros dominios. make -f /usr/share/selinux/devel/Makefile genera el .pp que instalas con semodule -i. Versionamos esos tres archivos en git junto con Ansible, y cada PR pasa por la misma revision que el codigo de aplicacion. Asi la policy queda trazable, reproducible y auditable, en vez de pudrirse como trabajo manual sin documentar en un solo host.
Puertos y file contexts: 80% de los casos
Los servicios que abren sockets en puertos no estandar son el caso mas comun de ruptura silenciosa. Postgres en 5433 por ejemplo necesita semanage port -a -t postgresql_port_t -p tcp 5433, y no una nueva policy. Nginx sirviendo archivos fuera de /var/www quiere semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?" seguido de restorecon -Rv /srv/app. El 80 por ciento de los casos que vemos son problemas de label y puerto, no reglas allow faltantes. Por eso revisa siempre primero con ls -Z y semanage port -l si un label equivocado o un puerto no registrado es la causa, antes de siquiera pensar en un .te.
Booleans en vez de policy custom
Muchos requisitos aparentemente complejos ya estan cubiertos por un boolean. getsebool -a | grep httpd muestra decenas de switches; httpd_can_network_connect permite conexiones salientes, httpd_can_network_connect_db solo a la base de datos. Fijalos de forma persistente con setsebool -P httpd_can_network_connect_db on. Un boolean siempre es preferible a un modulo custom, porque lo mantienen los empaquetadores de la distro, esta documentado y se arrastra en los updates. La policy custom es el ultimo recurso, no el primer agarre; quien busca booleans primero escribe muchas menos archivos .te propios en la practica.
Confined vs. unconfined: la falacia comun
Un error muy difundido es creer que un servicio esta protegido solo porque SELinux esta enforcing. Muchos procesos auto-lanzados corren como unconfined_service_t o init_t y estan practicamente sin freno. Verifica con ps -eZ | grep miservicio en que dominio corre realmente el servicio. Un binario bajo /usr/local/bin suele llevar bin_t en vez de un dominio propio, asi que no ocurre transicion. Todo el esfuerzo de una policy custom no vale nada si el proceso nunca transiciona al dominio confined; para eso existe justamente el archivo .fc mas una type_transition desde el init_t que lo lanza. Verifica siempre la transicion en vez de asumirla.
Probar y hacer rollback
Una policy es codigo y se prueba como codigo. Instala primero en staging con semodule -i miservicio.pp, ejercita el caso de uso completo y revisa ausearch -m AVC -ts recent buscando cero denials nuevos. Lista los modulos cargados con semodule -l, y quita uno defectuoso de inmediato con semodule -r miservicio. Ten presente el mecanismo de prioridades: semodule -X 400 -i carga con prioridad mas alta y sobreescribe la version de la distro de forma controlada. Para iterar rapido, pon brevemente el dominio objetivo en permissive, junta los AVC restantes en una sola pasada y agregalos con justificacion, en vez de caer en diez rondas de deploy-y-reza.
Observabilidad y policy drift
El mantenimiento en produccion exige observabilidad. Configuramos setroubleshoot-server en modo silent enviando AVC al SIEM via journald, con reglas Sigma especificas para denials no esperados. Cuando un deploy rompe, la alerta llega antes que el usuario se queje. Tambien corremos sealert -a /var/log/audit/audit.log semanalmente en staging para detectar policy drift antes de que llegue a produccion. Para el lado forense, cuando un denial apunta a un ataque real, engancha DFIR en Linux: Triaje en Vivo con UAC y Velociraptor, y la policy firmada encaja en la cadena de Supply Chain Security: Firma con Sigstore y SBOM Real en CI/CD.
Trampas comunes
Las trampas mas comunes: setenforce 0 como estado permanente en vez de diagnostico de 15 minutos; usar chcon en lugar de semanage fcontext mas restorecon, lo que pierde el label en el proximo restorecon o relabel; aplicar la salida de audit2allow a ciegas; y olvidar las reglas dontaudit que ocultan denials relevantes (se desactivan temporalmente con semodule -DB). Otro clasico es un label perdido en un restore con tar; usa tar --selinux o relabela explicitamente despues. La base de recon desde la perspectiva del atacante esta en Nmap Avanzado: Scripts NSE para Recon Interno en Lab Corporativo Simulado.
Checklist
Antes de que una policy custom vaya a produccion: (1) buscaste primero un tipo y boolean adecuados; (2) AVC capturados solo en permissive por dominio, el sistema quedo enforcing; (3) cada .te revisado manualmente, ninguna regla shadow_t/sys_admin sin justificacion; (4) file contexts via semanage fcontext mas restorecon, nunca chcon; (5) puertos registrados; (6) tres archivos versionados en git; (7) .pp firmado y desplegado via Ansible; (8) alerta SIEM sobre denials inesperados activa; (9) enforcing confirmado via getenforce; (10) ninguna entrada permissive olvidada.
FAQ
Por que no reemplazar SELinux por AppArmor? En la familia RHEL, SELinux es el camino soportado e integrado; cambiar tira a la basura las policies de la distro. Enforcing cuesta rendimiento? El overhead esta en un porcentaje de un solo digito y es despreciable en la practica frente a la ganancia de seguridad. Y un servicio que no arranca ni a la de tres? Pon permissive por dominio, reproduce el flujo completo, junta AVC, escribe la policy, revisala, vuelve a enforcing, verifica. Nunca dejes el sistema entero en permissive.
Takeaway practico: nunca corras setenforce 0 en produccion por mas de 15 minutos. Usa semanage permissive -a dominio_t para aislar el problema, captura AVC con ausearch, revisa la salida de audit2allow manualmente, commitea el .te en git, firma el .pp y despliega via Ansible. Si no puedes justificar cada allow rule en revision de codigo, la policy no esta lista. SELinux no es obstaculo, es la ultima defensa de perimetro que se interpone entre una sola vulnerabilidad y la perdida total del host.


