SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-09-04

SELinux: nginx devuelve 403 con permisos correctos

nginx devuelve 403 aunque los permisos parecen correctos. Lee la denegación de SELinux, corrige la etiqueta con semanage y restorecon, y mantén enforcing.

Por qué nginx devuelve 403 para un archivo cuyos permisos son correctos

Que nginx devuelva 403 para un archivo cuyos bits de permisos son correctos casi siempre significa que SELinux (Linux con seguridad mejorada) está rechazando la lectura. SELinux comprueba un segundo conjunto de reglas después de validar los permisos normales. El servidor web sólo puede leer archivos que tengan una etiqueta de contenido web. Su archivo tiene otra etiqueta, por lo que la apertura falla y nginx no tiene nada que enviar.

Compruebe la etiqueta, no sólo el modo:

ls -ldZ /data/www /data/www/index.html
drwxr-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.html

El punto que aparece después de drwxr-xr-x indica que el archivo tiene una etiqueta de SELinux. default_t es lo que obtiene una ruta cuando la política nunca la ha identificado. Ninguna regla del servidor web permite leer ese tipo. El registro de errores muestra un error Unix normal, por lo que parece un problema 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 rechazo: el normal y el de SELinux. Por eso, lo primero es determinar qué capa rechazó la operación. No empiece con setenforce 0.

La parte del modelo que necesita

SELinux es un mecanismo de control de acceso obligatorio, normalmente 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 aparece en esa lista se deniega. Se ejecuta después de la comprobación clásica de Unix, por lo que los bits de permisos de drwxr-xr-x también deben permitir primero el acceso. Ambas capas deben permitirlo.

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, casi todo el tiempo se trabaja con el tercer campo: el tipo. Dos comandos muestran los valores actuales:

ps -eZ | grep nginx
id -Z

Los procesos de trabajo de nginx muestran un contexto que termina en httpd_t. El shell de inicio de sesión muestra unconfined_u:unconfined_r:unconfined_t:s0, porque la política targeted predeterminada confina los servicios y deja sin restricciones a los usuarios interactivos. Esto es importante porque SELinux no sustituye a ejecutar los servicios con usuarios con privilegios mínimos. Limita a qué puede acceder un servicio después de que alguien lo comprometa.

Los tres modos y qué imágenes incluyen SELinux

sestatus
getenforce

Enforcing bloquea y registra los eventos. 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 de /etc/selinux/config, que es el que se restablece después de reiniciar.

Rocky Linux, AlmaLinux, Fedora y RHEL incluyen SELinux en modo enforcing con la política targeted. Este valor predeterminado común es heredado y no una coincidencia, porque las cuatro distribuciones proceden de la misma línea de Red Hat que pasó por CentOS antes de la aparición de Rocky Linux y AlmaLinux. Usar una u otra no cambia nada en esta página, porque incluyen la misma política y las mismas herramientas. Por tanto, elegir entre Rocky Linux y AlmaLinux depende de sus compromisos de compatibilidad y de la compatibilidad con CPU antiguas, no de los valores predeterminados de seguridad. Ubuntu y Debian incluyen AppArmor, que cumple la misma función con un mecanismo diferente (la última sección lo explica). Por eso, la misma aplicación puede instalarse correctamente en uno de sus servidores y devolver 403 en otro.

Instale las herramientas antes de necesitarlas

sudo dnf install -y policycoreutils-python-utils setroubleshoot-server

En una imagen mínima, la ausencia de semanage: command not found significa que falta policycoreutils-python-utils: ese paquete contiene semanage y audit2allow. setroubleshoot-server añade sealert y escribe en el journal un resumen en lenguaje claro de cada denegación. Instale ambos en un servidor nuevo, porque cuando los necesita es precisamente cuando algo ya está averiado.

Cómo leer una denegación de SELinux en el registro de auditoría

Cada rechazo lo registra el daemon de auditoría como un mensaje AVC (caché de vectores de acceso):

sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent
type=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=0

Cuatro campos contienen toda la información. comm es el programa cuyo acceso se bloqueó. scontext es el contexto de origen, es decir, el dominio en el que se ejecutaba el proceso. tcontext es el contexto de destino, la etiqueta del objeto al que intentó acceder. tclass es el tipo de objeto, en este caso, un archivo. En conjunto, indican que el proceso en httpd_t intentó leer un archivo con la etiqueta default_t y que permissive=0 confirma que la solicitud se bloqueó realmente, en lugar de registrarse únicamente.

Si ausearch no muestra nada, es posible que el daemon de auditoría no esté en ejecución. En ese caso, las denegaciones aparecen en el búfer circular del kernel:

sudo journalctl -k | grep -i avc

Ahora convierta el registro en una frase:

sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.log

audit2why lee los mismos registros y señala la causa que reconoce: un booleano desactivado, una etiqueta que no coincide con la política o la ausencia total de una regla. sealert recorre todo el registro y muestra un comando sugerido para cada denegación. Trate la sugerencia como una orientación. El texto cambia entre versiones y, en ocasiones, sealert propone un módulo de política personalizado cuando la solución correcta es cambiar una etiqueta en una sola línea.

Hay otro aspecto importante. La política contiene reglas dontaudit que ocultan las denegaciones consideradas inofensivas. Por eso, un programa puede funcionar de forma incorrecta aunque el registro permanezca vacío. Muéstrelas durante una sola prueba:

sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -B

Corregir una ruta con una etiqueta incorrecta mediante semanage fcontext y restorecon

Son dos comandos, y el orden importa. semanage fcontext -a registra cuál debería ser la etiqueta de una ruta. restorecon aplica ese valor predeterminado registrado a los archivos del disco.

sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.html

La ruta es una expresión regular. (/.*)? incluye el directorio y todo lo que contiene, que es lo necesario para una raíz de documentos. Compruebe qué cambiaría antes de aplicar el cambio: sudo restorecon -Rvn /data/www muestra las reevaluaciones de etiquetas previstas, porque -n no realiza ninguna acción. Después de un restorecon real, la etiqueta se muestra como httpd_sys_content_t y el error 403 desaparece sin reiniciar el servicio.

Use chcon sólo como prueba. chcon -t httpd_sys_content_t index.html establece la etiqueta directamente, y el siguiente restorecon, una actualización de paquetes o una reevaluación completa de etiquetas la restablece, porque la política sigue indicando que la ruta debería tener otra etiqueta. En un equipo donde dnf-automatic aplica actualizaciones de seguridad con un temporizador, ese restablecimiento ocurre según su propia programación y no mientras está delante de la máquina, por lo que el sitio deja de funcionar horas después de la última modificación. semanage fcontext es la versión persistente. Muestre lo que ha registrado con sudo semanage fcontext -l | grep '^/data'.

El contenido en el que el servicio debe escribir necesita otro tipo. Use httpd_sys_rw_content_t para un directorio de cargas o una caché, y limítelo a esas rutas: un sitio de sólo lectura bajo un tipo que permite escritura proporciona más acceso del que necesita la aplicación.

¿Por qué era incorrecta la etiqueta? Casi siempre se debe a cómo llegaron los archivos. mv conserva la etiqueta existente de un archivo, por lo que un sitio trasladado desde /root llega con la etiqueta admin_home_t y la conserva. Un cp simple asigna al archivo nuevo la etiqueta predeterminada del directorio de destino, que normalmente es lo que se necesita, mientras que cp -a y rsync -X copian las etiquetas del origen junto con el archivo. Un git clone a un directorio nuevo de primer nivel produce default_t. Cuando una página se carga correctamente desde /usr/share/nginx/html y falla desde su propio directorio, esta es la causa.

Corregir una clase de comportamiento con un valor booleano

Algunos fallos no se deben a un problema de etiquetas. Un reverse proxy en un sistema Rocky o AlmaLinux recién instalado devuelve 502 y el registro de errores muestra:

2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstream

El upstream funciona correctamente. De forma predeterminada, el dominio httpd_t no puede abrir conexiones de red salientes, por lo que la llamada connect() se rechaza antes de llegar a la interfaz de loopback. Un solo interruptor controla todo este comportamiento:

getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on

-P es la opción importante: escribe el valor en el disco. Sin -P, el cambio se pierde en el siguiente reinicio. El servicio funciona hasta que se reinicia el equipo. Confírmelo con semanage boolean -l | grep httpd_can_network_connect, que muestra el valor activo junto al valor almacenado.

Prefiera un valor booleano a una regla escrita manualmente cuando exista esa opción. Los booleanos forman parte de la política de la distribución, por lo que se mantienen, están documentados y son fáciles de localizar para la siguiente persona. getsebool -a muestra todos los booleanos del sistema.

Permitir que un servicio escuche en un puerto no estándar

Los puertos también tienen etiquetas. Mueva nginx al puerto 8081 y no podrá iniciarse:

nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)

httpd_t puede asociar 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 8081

Compruebe primero la lista. Ya se permiten varios puertos altos, incluidos 8008 y 8443, y añadir uno dos veces produce ValueError: Port tcp/8081 already defined. Si el puerto ya pertenece a otro tipo, cámbielo con semanage port -m -t http_port_t -p tcp 8081 en lugar de añadirlo.

El mismo comando permite que funcione un puerto SSH cambiado. Bind to port 2222 on 0.0.0.0 failed: Permission denied en journalctl -u sshd indica que falta 2222 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 la sesión. Este es el paso que se omite al seguir una guía genérica para reforzar SSH en un VPS con una imagen de la familia Red Hat. SELinux tampoco es un firewall, por lo que el puerto debe seguir abierto: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload en este caso, o ufw en una imagen Debian o Ubuntu. Ese indicador --permanent tiene la misma trampa relacionada con el reinicio que -P en un booleano, y conviene leer una vez las zonas que determinan a qué interfaces se aplica una regla en conceptos básicos de firewalld para un VPS Rocky o AlmaLinux.

Cuando no hay ningún booleano ni etiqueta que cambiar

Esto es poco frecuente en un servidor normal y es donde se producen daños. audit2allow puede crear un módulo de políticas a partir de las denegaciones del registro:

sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.pp

Lea nginx_local.te antes de instalarlo. Dos hábitos ayudan a mantener este procedimiento seguro. Filtre la entrada para incluir sólo el programa que está corrigiendo con -c, porque canalizar una semana de denegaciones no relacionadas a audit2allow las concede todas de una vez. Nunca instale un módulo creado 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 y difícil de detectar meses después. Elimine un módulo con sudo semodule -r nginx_local.

El modo permissive sirve para diagnosticar, no para solucionar

sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1

El modo permissive permite el acceso y lo registra. Su valor real es la exhaustividad. En enforcing, el servicio se detiene ante la primera denegación. Usted corrige ese problema, reinicia el servicio y encuentra la segunda denegación. En permissive, la ejecución continúa y el registro recopila todas las denegaciones en una sola pasada. Después, vuelve a activar enforcing y las corrige juntas.

setenforce no modifica /etc/selinux/config, por lo que un reinicio devuelve el sistema a enforcing. Esto proporciona una red de seguridad. También explica por qué una «solución» basada en setenforce 0 reaparece en el peor momento posible. Si un servicio necesita más margen mientras trabaja en él, marque ese dominio en lugar de todo el sistema: sudo semanage permissive -a httpd_t mantiene el resto en enforcing y sudo semanage permissive -d httpd_t revierte el cambio.

Por qué desactivar SELinux cuesta más que corregir la etiqueta

Configurar SELINUX=disabled en /etc/selinux/config sustituye una corrección de una sola línea de la etiqueta por un servidor permanentemente más débil. La diferencia se hace evidente el día que una aplicación web resulta comprometida. Con SELinux en modo enforcing, el código del atacante se ejecuta en httpd_t, por lo que puede leer el contenido web, mientras que la política rechaza la lectura de /etc/shadow o la escritura de una unidad de systemd, independientemente de los permisos que tendría el usuario de Unix. Sin una política cargada, ese mismo código obtiene todos los permisos de la cuenta de servicio.

Desactivar SELinux también genera un coste posterior. Mientras no hay ninguna política cargada, los archivos nuevos se crean sin etiqueta y el sistema de archivos deja de coincidir con la política. Al volver a activar SELinux, es necesario volver a etiquetar todo el sistema de archivos o varios servicios fallarán a la vez:

sudo fixfiles -F onboot
sudo reboot

Esto escribe /.autorelabel y vuelve a etiquetar todos los sistemas de archivos durante el siguiente arranque. En un disco grande, el proceso tarda mucho y la consola parece bloqueada, así que inícielo cuando pueda esperar. Como la máquina se va a apagar de todos modos, conviene comprobar primero qué más se ha programado para reiniciarse. Eso es lo que informa needs-restarting después de que una actualización de dnf haya dejado kernels y bibliotecas antiguos en memoria. En Rocky Linux y AlmaLinux 9, el archivo de configuración ya no desactiva por sí solo la parte del kernel. La forma documentada de desactivar SELinux por completo es usar un argumento del kernel (sudo grubby --update-kernel ALL --args selinux=0). Conocer ese comando resulta útil cuando se hereda el servidor de otra persona. No es la solución para un 403.

Los contenedores añaden una etiqueta más

En un host de la familia Red Hat, los procesos de los contenedores se ejecutan en container_t y sólo pueden leer archivos etiquetados con container_file_t. Un montaje bind desde el host falla con Permission denied dentro del contenedor, mientras ls -l en el host parece completamente normal. El sufijo :Z indica al runtime que debe volver a etiquetar el montaje:

docker run -d -v /data/appdata:/var/lib/app:Z myimage

:Z etiqueta el directorio sólo para este contenedor. :z lo etiqueta para compartirlo entre contenedores. Si se apunta :Z a un directorio que utilizan otros servicios, vuelve a etiquetar ese directorio de forma recursiva y esos servicios dejan de funcionar. Por tanto, asigne rutas propias a los contenedores. Si el engine todavía no está instalado en el equipo, tenga en cuenta que el comando docker en estas distribuciones suele ser podman con ese nombre, un detalle que los pasos de instalación de Rocky y AlmaLinux resuelven antes de que aparezca este problema. El resto de la configuración coincide con la de cualquier otra imagen, como se explica en ejecutar Docker en un VPS.

Ubuntu y Debian usan AppArmor

Mismo objetivo, diseño diferente. AppArmor confina un programa según la ruta de su ejecutable, mediante un perfil en /etc/apparmor.d/, en lugar de etiquetar archivos en disco. No hay nada que volver a etiquetar ni existe restorecon. Empiece aquí:

sudo aa-status
sudo journalctl -k | grep -i apparmor

Una denegación aparece como apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". El flujo de trabajo es similar: lea la denegación, localice el perfil y cambie la regla. sudo apt install apparmor-utils proporciona aa-complain (modo permisivo para un perfil) y aa-enforce permite restaurarlo. Ubuntu confina un conjunto seleccionado de servicios empaquetados y deja el resto sin confinar. Por tanto, lea aa-status para comprobar qué está activo realmente en lugar de darlo por supuesto.

Una práctica se aplica a ambos sistemas. Cuando un servicio informa de Permission denied sobre algo que parece correcto, lea el registro de seguridad antes de modificar los permisos. Los bits de permisos rara vez son el problema dos veces.

FAQ

¿Por qué nginx devuelve 403 cuando los permisos del archivo son correctos?

Porque SELinux denegó la lectura, no los bits de permisos. El servidor web se ejecuta en el dominio httpd_t y sólo puede leer archivos etiquetados para contenido web. Por eso rechaza un archivo etiquetado como default_t o admin_home_t y nginx no tiene nada que servir. Confírmelo con sudo ausearch -m AVC -ts recent. Este comando muestra scontext que termina en httpd_t y tcontext con el tipo incorrecto. Después, 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 funcionar un servicio?

setenforce 0 es un paso de diagnóstico, no una solución. Úselo una vez para reproducir el problema y permitir que el registro recopile todas las denegaciones en una sola ejecución. Léalas con sudo ausearch -m AVC -ts recent. Después, ejecute sudo setenforce 1 y corrija las causas. Un servidor que permanece en modo permisivo registra todas las denegaciones y no bloquea ninguna. Así conserva el ruido y pierde la protección. Si un servicio necesita margen mientras trabaja, ejecute sudo semanage permissive -a httpd_t para que el resto del equipo siga 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 ese servicio tiene permitido enlazarse. Para un servidor web en 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Para SSH en 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. Si el puerto ya aparece en la lista, ValueError: Port tcp/8081 already defined falla. Sin este paso, el daemon termina durante el arranque con bind() ... Permission denied, aunque ningún otro proceso esté usando el puerto.

¿Ubuntu tiene SELinux?

No. Ubuntu y Debian incluyen AppArmor, que aplica un perfil asociado a la ruta de un ejecutable, en lugar de usar 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 confinamiento de forma predeterminada. Rocky Linux y AlmaLinux son las distribuciones donde encontrará SELinux en modo enforcing desde el primer arranque, junto con Fedora y RHEL.