Nginx error 403 y SELinux: como diagnosticar y corregir
Si Nginx devuelve 403 aunque los permisos son correctos, SELinux bloquea el acceso. Aprende a leer registros, usar semanage y restorecon para corregir etiquetas sin desactivarlo.
Por qué nginx devuelve un 403 en un archivo con permisos correctos
Que nginx devuelva un 403 en un archivo cuyos bits de permiso son correctos casi siempre significa que SELinux (Security-Enhanced Linux) está denegando la lectura. SELinux comprueba un segundo conjunto de reglas después de que se superan los permisos normales, y el servidor web solo tiene permitido leer archivos que tengan una etiqueta de contenido web. Su archivo tiene una etiqueta diferente, por lo que la apertura falla y nginx no tiene nada que enviar.
Observe la etiqueta, no solo el modo:
ls -ldZ /data/www /data/www/index.htmldrwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.htmlEl punto impreso después de drwxr-xr-x significa que el archivo tiene una etiqueta de SELinux. default_t es lo que obtiene una ruta cuando la política no la reconoce, y nada en las reglas del servidor web permite leer ese tipo. El registro de errores muestra un error de Unix común, que es la razón por la cual esto se interpreta como un error de permisos:
2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"El kernel devuelve 13: Permission denied para ambos tipos de denegación, la común y la de SELinux. Por lo tanto, la primera tarea es averiguar qué capa denegó el acceso. No empiece con setenforce 0.
La parte del modelo que necesita
SELinux es un control de acceso obligatorio, habitualmente denominado MAC. Cada proceso se ejecuta en un dominio, como httpd_t para el servidor web. Cada archivo y cada puerto de red tiene un tipo, como httpd_sys_content_t. La política es una lista de combinaciones permitidas de dominio, tipo y acción; todo lo que no esté en esa lista se deniega. Se ejecuta después de la comprobación clásica de Unix, por lo que los bits de permiso en drwxr-xr-x deben permitir el acceso primero. Ambas capas deben autorizar la operación.
Un contexto completo tiene cuatro campos separados por dos puntos, como system_u:system_r:httpd_t:s0: el usuario de SELinux, el rol, el tipo y el nivel. En un servidor, dedicará casi todo su tiempo al tercer campo, el tipo. Dos comandos muestran los valores en tiempo real:
ps -eZ | grep nginx
id -ZLos procesos worker de nginx muestran un contexto que termina en httpd_t. Su shell de inicio de sesión muestra unconfined_u:unconfined_r:unconfined_t:s0, porque la política predeterminada targeted confina los servicios y no afecta a los usuarios interactivos. Es importante saber esto, ya que SELinux no sustituye a ejecutar servicios con usuarios de privilegios mínimos. Limita lo que un servicio puede alcanzar después de que alguien logre vulnerarlo.
Los tres modos y qué imágenes incluyen SELinux
sestatus
getenforceEnforcing bloquea y registra las acciones. Permissive permite todo y registra lo que habría bloqueado. Disabled no carga ninguna política. getenforce muestra el modo actual. sestatus también muestra el modo desde /etc/selinux/config, que es el que se aplica tras reiniciar el sistema.
Rocky Linux, AlmaLinux, Fedora y RHEL incluyen SELinux en modo enforcing con la política targeted. Ubuntu y Debian utilizan AppArmor en su lugar, que realiza la misma función mediante un mecanismo distinto (la última sección lo detalla). Por lo tanto, una misma aplicación puede instalarse correctamente en uno de sus servidores y devolver un error 403 en otro.
Instale las herramientas antes de necesitarlas
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found en una imagen mínima significa que falta policycoreutils-python-utils: ese paquete contiene semanage y audit2allow. setroubleshoot-server añade sealert y escribe un resumen en lenguaje sencillo de cada denegación en el journal. Instale ambos en un servidor nuevo, porque el momento en que los necesita es el momento en que algo ya se ha roto.
Cómo leer una denegación de SELinux en el registro de auditoría
Cada rechazo es registrado por el demonio de auditoría como un mensaje AVC (access vector cache):
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=AVC msg=audit(1754896442.881:412): avc: denied { read } for pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0Cuatro campos contienen toda la información. comm es el programa que fue bloqueado. scontext es el contexto de origen, el dominio en el que se ejecutaba el proceso. tcontext es el contexto de destino, la etiqueta del objeto que intentó acceder. tclass es el tipo de objeto, en este caso un archivo. Interpretado en conjunto: el proceso en httpd_t intentó leer un archivo etiquetado como default_t, y permissive=0 indica que la solicitud fue bloqueada realmente en lugar de solo registrada.
Si ausearch no muestra nada, es posible que el demonio de auditoría no esté en ejecución. En ese caso, las denegaciones terminan en el búfer circular del kernel:
sudo journalctl -k | grep -i avcAhora convierta el registro en una frase descriptiva:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why lee los mismos registros e identifica la causa reconocida: un booleano desactivado, una etiqueta que no coincide con la política o la ausencia total de una regla. sealert analiza todo el registro e imprime una sugerencia de comando por cada denegación. Considere la sugerencia como una pista. La redacción cambia entre versiones y sealert a veces propone un módulo de política personalizado cuando la solución correcta es simplemente corregir una etiqueta.
Un detalle adicional: la política contiene reglas dontaudit que ocultan las denegaciones consideradas inofensivas, por lo que un programa puede comportarse de forma incorrecta mientras el registro permanece vacío. Desactívelas durante la duración de una prueba:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BCorregir una ruta mal etiquetada con semanage fcontext y restorecon
Dos comandos, y el orden es importante. semanage fcontext -a registra cuál debería ser la etiqueta de una ruta. restorecon aplica ese valor predeterminado registrado a los archivos en el disco.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlLa ruta es una expresión regular. (/.*)? cubre el directorio en sí y todo su contenido, que es lo que necesita una raíz de documentos. Compruebe qué cambiaría antes de realizar la modificación: sudo restorecon -Rvn /data/www muestra las reetiquetas planificadas, ya que -n significa que no se realiza ninguna acción. Tras ejecutar un restorecon real, la etiqueta se lee como httpd_sys_content_t y el error 403 desaparece sin necesidad de reiniciar el servicio.
Utilice chcon solo como prueba. chcon -t httpd_sys_content_t index.html establece la etiqueta directamente, y el siguiente restorecon, una actualización de paquete o una reetiqueta completa la restablecerán, ya que la política sigue indicando que la ruta debería ser otra distinta. semanage fcontext es la versión que persiste. Liste lo que tiene registrado con sudo semanage fcontext -l | grep '^/data'.
El contenido que el servicio debe escribir necesita un tipo diferente. Utilice httpd_sys_rw_content_t para un directorio de carga o una caché, y limítese a esas rutas: un sitio de solo lectura bajo un tipo escribible otorga más acceso del que la aplicación necesita.
¿Por qué la etiqueta era incorrecta? Casi siempre se debe a cómo llegaron los archivos. mv mantiene la etiqueta existente de un archivo, por lo que un sitio movido desde /root llega etiquetado como admin_home_t y permanece así. Un cp simple asigna al nuevo archivo la etiqueta predeterminada del directorio de destino, que suele ser lo que se desea, mientras que cp -a y rsync -X copian las etiquetas de origen junto con el archivo. Un git clone en un nuevo directorio de nivel superior produce default_t. Cuando una página carga correctamente desde /usr/share/nginx/html y falla desde su propio directorio, esta es la razón.
Corregir una clase de comportamiento con un booleano
Algunos fallos no se deben a un problema de etiquetas. Un proxy inverso en una instalación limpia de Rocky o AlmaLinux devuelve un error 502, y el registro de errores indica:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamSu backend funciona correctamente. El dominio httpd_t no tiene permitido abrir conexiones de red salientes de forma predeterminada, por lo que la llamada connect() es rechazada antes de llegar a la interfaz de loopback. Un único interruptor controla todo este comportamiento:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P es el flag relevante: escribe el valor en el disco. Sin -P, el cambio se pierde tras el siguiente reinicio, lo que resulta en un servicio que funciona hasta que la máquina se reinicia. Confirme el estado con semanage boolean -l | grep httpd_can_network_connect, que muestra el valor en ejecución junto al valor almacenado.
Prefiera el uso de un booleano frente a una regla escrita manualmente siempre que exista uno. Los booleanos se distribuyen con la política de la distribución, por lo que están mantenidos, documentados y son fáciles de encontrar para la siguiente persona. getsebool -a enumera todos los disponibles en el sistema.
Configurar un servicio para escuchar en un puerto no estándar
Los puertos también están etiquetados. Si mueve Nginx al puerto 8081, el servicio se negará a iniciar:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t solo puede enlazar puertos etiquetados como http_port_t, y 8081 no es uno de ellos. Añádalo:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Compruebe la lista primero. Varios puertos altos ya están permitidos, incluidos 8008 y 8443; añadir uno que ya existe provocará un error con ValueError: Port tcp/8081 already defined. Si el puerto ya pertenece a un tipo diferente, cámbielo con semanage port -m -t http_port_t -p tcp 8081 en lugar de añadirlo.
El mismo comando es necesario para que funcione un puerto SSH modificado. Bind to port 2222 on 0.0.0.0 failed: Permission denied en journalctl -u sshd significa que 2222 no está en ssh_port_t, así que ejecute sudo semanage port -a -t ssh_port_t -p tcp 2222 antes de reiniciar el daemon y cerrar su sesión. Este es el paso que la gente omite al seguir una guía genérica sobre endurecimiento de SSH en un VPS en una imagen de la familia Red Hat. SELinux no es un firewall, por lo que el puerto debe estar abierto también: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload en este caso, o ufw en una imagen de Debian o Ubuntu.
Cuando no hay un booleano ni una etiqueta que cambiar
Esto es poco frecuente en un servidor estándar y es donde los usuarios suelen causar daños. audit2allow puede generar un módulo de política a partir de las denegaciones registradas en el log:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppLea nginx_local.te antes de realizar la instalación. Dos hábitos mantienen este proceso seguro. Filtre la entrada para incluir solo el programa que está corrigiendo mediante -c, ya que redirigir una semana de denegaciones no relacionadas hacia audit2allow otorgará todos los permisos simultáneamente. Nunca instale un módulo generado a partir de una denegación que no pueda explicar: una regla que permita a httpd_t leer todos los archivos del sistema es fácil de generar, pero difícil de detectar meses después. Elimine un módulo con sudo semodule -r nginx_local.
Permissive es un modo de diagnóstico, no una solución
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1El modo permissive permite el acceso y lo registra. Su valor real es la exhaustividad. En modo enforcing, el servicio se detiene en la primera denegación; usted corrige ese error, reinicia y se encuentra con el siguiente. En modo permissive, la ejecución continúa y el registro recopila todas las denegaciones en una sola pasada; después, usted vuelve al modo enforcing y las corrige todas a la vez.
setenforce no modifica /etc/selinux/config, por lo que un reinicio devuelve el sistema a enforcing. Esto es una red de seguridad y también la razón por la que una "solución" que consistió en setenforce 0 reaparece en el peor momento posible. Si un servicio necesita permisos especiales mientras trabaja en él, marque ese dominio en lugar de toda la máquina: sudo semanage permissive -a httpd_t mantiene el resto del sistema en enforcing, y sudo semanage permissive -d httpd_t revierte este cambio.
Por qué desactivar SELinux cuesta más que corregir la etiqueta
Configurar SELINUX=disabled en /etc/selinux/config cambia una corrección de etiqueta de una línea por un servidor permanentemente más vulnerable. La diferencia se hace evidente el día que una aplicación web es vulnerada. En modo enforcing, el código del atacante se ejecuta en httpd_t, por lo que puede leer contenido web, mientras que la lectura de /etc/shadow o la escritura de una unidad de systemd es rechazada por la política, independientemente de lo que el usuario Unix hubiera permitido. Sin una política cargada, ese mismo código obtiene todo lo que la cuenta de servicio tiene permitido.
Desactivarlo también conlleva un coste futuro. Mientras no hay política cargada, los archivos nuevos se crean sin etiqueta, por lo que el sistema de archivos se desincroniza de la política. Volver a activar SELinux requiere entonces un reetiquetado completo, o una serie de servicios fallarán simultáneamente:
sudo fixfiles -F onboot
sudo rebootEsto escribe /.autorelabel y reetiqueta todo el sistema de archivos durante el siguiente arranque. En un disco grande, esto toma mucho tiempo y la consola parece bloqueada, así que ejecútelo cuando pueda esperar. En Rocky Linux y AlmaLinux 9, el archivo de configuración ya no desactiva la parte del kernel por sí solo, y la forma documentada de desactivar SELinux completamente es mediante un argumento del kernel (sudo grubby --update-kernel ALL --args selinux=0). Conocer ese comando ayuda cuando se hereda el servidor de otra persona. No es la solución para un error 403.
Los contenedores añaden una etiqueta adicional
En un host de la familia Red Hat, los procesos de los contenedores se ejecutan en container_t y solo pueden leer archivos etiquetados como container_file_t. Un montaje de tipo bind desde el host falla con Permission denied dentro del contenedor, mientras que ls -l en el host parece totalmente normal. El sufijo :Z indica al motor de ejecución que reetiquete el montaje:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z etiqueta el directorio exclusivamente para este contenedor. :z lo etiqueta para compartirlo entre contenedores. Si apunta :Z a un directorio que utilizan otros servicios, este reetiquetará dicho directorio de forma recursiva, lo cual interrumpirá el funcionamiento de esos servicios; por tanto, asigne a los contenedores sus propias rutas. Todo lo demás sobre la configuración coincide con cualquier otra imagen, lo cual se detalla en ejecutar Docker en un VPS.
Ubuntu y Debian utilizan AppArmor
La función es la misma, pero el diseño difiere. AppArmor confina un programa mediante la ruta de su ejecutable, utilizando un perfil bajo /etc/apparmor.d/, en lugar de etiquetar archivos en el disco. No hay nada que reetiquetar ni restorecon. Empiece por aquí:
sudo aa-status
sudo journalctl -k | grep -i apparmorUna denegación aparece como apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". El flujo de trabajo sigue la misma estructura: lea la denegación, localice el perfil y modifique la regla. sudo apt install apparmor-utils le ofrece aa-complain (modo permisivo para un perfil) y aa-enforce para revertirlo. Ubuntu confina un conjunto seleccionado de servicios empaquetados y deja el resto sin confinar, por lo que debe consultar aa-status para ver qué está realmente activo en lugar de hacer suposiciones.
Un hábito es común en ambos sistemas. Cuando un servicio informe de Permission denied sobre algo que parece correcto, lea el registro de seguridad antes de modificar los permisos. Es raro que el problema sean los bits dos veces.
FAQ
¿Por qué Nginx devuelve un error 403 si los permisos de archivo son correctos?
Porque SELinux denegó la lectura, no los bits de permisos. El servidor web se ejecuta en el dominio httpd_t y solo puede leer archivos etiquetados para contenido web; por lo tanto, un archivo etiquetado como default_t o admin_home_t es rechazado y Nginx no tiene nada que servir. Confírmelo con sudo ausearch -m AVC -ts recent, que muestra scontext terminando en httpd_t y tcontext conteniendo el tipo incorrecto. Luego, registre la etiqueta correcta y aplíquela: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" seguido de sudo restorecon -Rv /data/www.
¿Es seguro ejecutar setenforce 0 para hacer que un servicio funcione?
setenforce 0 es un paso de diagnóstico, no una solución. Úselo para reproducir el problema una vez, de modo que el registro recopile cada denegación en una sola pasada, léalas con sudo ausearch -m AVC -ts recent, luego ejecute sudo setenforce 1 y repare las causas. Un servidor dejado en modo permissive registra cada denegación pero no bloquea ninguna, por lo que mantiene el ruido y pierde la protección. Si un servicio necesita espacio mientras usted trabaja, ejecute sudo semanage permissive -a httpd_t para que el resto de la máquina permanezca en modo enforcing.
¿Cómo ejecuto un servicio en un puerto no estándar con SELinux en modo enforcing?
Añada el puerto al tipo al que el servicio tiene permitido vincularse. Para un servidor web en el puerto 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Para SSH en el puerto 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Compruebe primero la lista actual con sudo semanage port -l | grep -w http_port_t, ya que un puerto que ya está listado fallará con ValueError: Port tcp/8081 already defined. Sin este paso, el daemon se cierra al iniciar con bind() ... Permission denied, aunque ningún otro proceso esté ocupando el puerto.
¿Ubuntu tiene SELinux?
No. Ubuntu y Debian utilizan AppArmor, que aplica un perfil vinculado a la ruta de un ejecutable en lugar de a etiquetas en los archivos. Compruébelo con sudo aa-status y busque líneas apparmor="DENIED" en sudo journalctl -k. Ubuntu confina un conjunto seleccionado de servicios empaquetados, por lo que muchos programas se ejecutan sin confinar de forma predeterminada. Rocky Linux y AlmaLinux son donde encontrará SELinux en modo enforcing desde la instalación, junto con Fedora y RHEL.