SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-09-04

Nginx erreur 403 : résoudre les blocages SELinux

Votre serveur Nginx renvoie une erreur 403 alors que les permissions sont correctes ? Apprenez à analyser les denials SELinux et à corriger les labels avec semanage et restorecon.

Pourquoi nginx renvoie une erreur 403 sur un fichier dont les permissions sont correctes

Si nginx renvoie une erreur 403 sur un fichier dont les bits de permission sont corrects, il s'agit presque toujours de SELinux (Security-Enhanced Linux) qui refuse la lecture. SELinux vérifie un second ensemble de règles après la validation des permissions classiques. Le serveur web n'est autorisé à lire que les fichiers portant une étiquette de contenu web. Votre fichier porte une étiquette différente, l'ouverture échoue donc et nginx n'a rien à envoyer.

Examinez l'étiquette, pas seulement le mode :

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

Le point affiché après drwxr-xr-x signifie que le fichier porte une étiquette SELinux. default_t est l'étiquette attribuée à un chemin inconnu de la politique, et aucune règle du serveur web n'autorise la lecture de ce type. Le journal d'erreurs affiche une erreur Unix ordinaire, ce qui explique pourquoi cela ressemble à un problème de permissions :

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"

Le noyau renvoie 13: Permission denied pour les deux types de refus, le classique et celui lié à SELinux. La première étape consiste donc à identifier quelle couche a refusé l'accès. Ne commencez pas par setenforce 0.

La partie du modèle dont vous avez besoin

SELinux est un système de contrôle d'accès obligatoire, généralement désigné par l'acronyme MAC. Chaque processus s'exécute dans un domaine, comme httpd_t pour le serveur web. Chaque fichier et chaque port réseau possède un type, comme httpd_sys_content_t. La politique est une liste de combinaisons autorisées entre domaine, type et action ; tout ce qui ne figure pas dans cette liste est refusé. Il intervient après la vérification Unix classique, donc les bits de permission dans drwxr-xr-x doivent toujours autoriser l'accès en premier. Les deux couches doivent valider l'opération.

Un contexte complet comporte quatre champs séparés par des deux-points, comme system_u:system_r:httpd_t:s0 : l'utilisateur SELinux, le rôle, le type et le niveau. Sur un serveur, vous passerez la quasi-totalité de votre temps sur le troisième champ, le type. Deux commandes permettent d'afficher les valeurs actives :

ps -eZ | grep nginx
id -Z

Les workers nginx affichent un contexte se terminant par httpd_t. Votre shell de connexion affiche unconfined_u:unconfined_r:unconfined_t:s0, car la politique par défaut targeted confine les services et laisse les utilisateurs interactifs tranquilles. Il est important de le savoir, car SELinux ne remplace pas l'exécution des services avec des utilisateurs aux privilèges restreints. Il limite ce qu'un service peut atteindre si quelqu'un parvient à compromettre le serveur.

Les trois modes et les distributions utilisant SELinux

sestatus
getenforce

Le mode Enforcing bloque et journalise les actions. Le mode Permissive autorise tout et journalise ce qu'il aurait bloqué. Le mode Disabled ne charge aucune politique. getenforce affiche le mode actuel. sestatus affiche également le mode défini dans /etc/selinux/config, qui est celui appliqué après un redémarrage.

Rocky Linux, AlmaLinux, Fedora et RHEL sont livrés avec SELinux en mode Enforcing et la politique targeted. Ce défaut commun n'est pas une coïncidence, car ces quatre distributions sont issues de la même lignée Red Hat qui incluait CentOS avant l'apparition de Rocky Linux et AlmaLinux. Le choix de l'une ou l'autre ne change rien au contenu de cette page, car elles utilisent la même politique et les mêmes outils. Ainsi, le choix entre Rocky Linux et AlmaLinux repose sur leurs promesses de compatibilité et le support des processeurs anciens plutôt que sur les paramètres de sécurité par défaut. Ubuntu et Debian utilisent AppArmor, qui remplit la même fonction avec un mécanisme différent (la dernière section traite ce sujet). Par conséquent, une même application peut s'installer sans problème sur l'un de vos serveurs et renvoyer une erreur 403 sur un autre.

Installez les outils avant d'en avoir besoin

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

semanage: command not found sur une image minimale signifie que policycoreutils-python-utils est absent : ce paquet contient semanage et audit2allow. setroubleshoot-server ajoute sealert et écrit un résumé en langage clair de chaque refus dans le journal. Installez les deux sur un serveur neuf, car le moment où vous en aurez besoin est précisément celui où quelque chose sera déjà en panne.

Comment lire un refus SELinux dans le journal d'audit

Chaque refus est enregistré par le démon d'audit sous la forme d'un message AVC (access vector cache) :

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

Quatre champs résument la situation. comm est le programme qui a été bloqué. scontext est le contexte source, le domaine dans lequel le processus s'exécutait. tcontext est le contexte cible, l'étiquette apposée sur l'élément qu'il a tenté d'atteindre. tclass est le type d'objet, ici un fichier. En lisant l'ensemble : le processus dans httpd_t a tenté de lire un fichier étiqueté default_t, et permissive=0 indique que la requête a réellement été bloquée plutôt que simplement journalisée.

Si ausearch n'affiche rien, le démon d'audit n'est peut-être pas en cours d'exécution. Les refus sont alors envoyés dans le tampon circulaire du noyau :

sudo journalctl -k | grep -i avc

Traduisez maintenant l'enregistrement en une phrase explicite :

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

audit2why lit les mêmes enregistrements et identifie la cause reconnue : un booléen désactivé, une étiquette non conforme à la politique, ou l'absence totale de règle. sealert parcourt l'intégralité du journal et affiche une commande suggérée pour chaque refus. Considérez cette suggestion comme une piste. La formulation change selon les releases, et sealert propose parfois un module de politique personnalisé là où une simple correction d'étiquette en une ligne serait la solution appropriée.

Un dernier point à connaître. La politique contient des règles dontaudit qui masquent les refus considérés comme inoffensifs ; un programme peut donc dysfonctionner alors que le journal reste vide. Affichez-les le temps d'un test :

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

Corriger un chemin mal étiqueté avec semanage fcontext et restorecon

Deux commandes sont nécessaires, et l'ordre a son importance. semanage fcontext -a enregistre ce que l'étiquette d'un chemin devrait être. restorecon applique cette valeur par défaut enregistrée aux fichiers sur le disque.

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

Le chemin est une expression régulière. (/.*)? couvre le répertoire lui-même ainsi que tout son contenu, ce qui est requis pour une racine de documents. Vérifiez les changements prévus avant de les appliquer : sudo restorecon -Rvn /data/www affiche les changements d'étiquettes planifiés, car -n signifie aucune action. Après un restorecon réel, l'étiquette devient httpd_sys_content_t et l'erreur 403 disparaît sans redémarrage du service.

N'utilisez chcon qu'à des fins de test. chcon -t httpd_sys_content_t index.html définit l'étiquette directement, mais le prochain restorecon, une mise à jour de paquet ou un réétiquetage complet la réinitialisera, car la politique stipule toujours que le chemin doit avoir une autre valeur. Sur une machine où dnf-automatic applique les mises à jour de sécurité selon un calendrier, cette réinitialisation survient de manière autonome, et non pendant que vous êtes devant la machine ; le site devient donc inaccessible des heures après votre dernière intervention. semanage fcontext est la version qui persiste. Listez vos enregistrements avec sudo semanage fcontext -l | grep '^/data'.

Le contenu que le service doit écrire nécessite un type différent. Utilisez httpd_sys_rw_content_t pour un répertoire de téléchargement ou un cache, et limitez-le à ces chemins : un site en lecture seule sous un type inscriptible accorde plus d'accès que ce dont l'application a besoin.

Pourquoi l'étiquette était-elle erronée ? Presque toujours à cause de la manière dont les fichiers ont été transférés. mv conserve l'étiquette existante d'un fichier ; ainsi, un site déplacé depuis /root arrive avec l'étiquette admin_home_t et la conserve. Un simple cp attribue au nouveau fichier l'étiquette par défaut du répertoire de destination, ce qui est généralement le comportement souhaité, tandis que cp -a et rsync -X copient les étiquettes de la source avec le fichier. Un git clone vers un nouveau répertoire de premier niveau produit default_t. Lorsqu'une page se charge correctement depuis /usr/share/nginx/html mais échoue depuis votre propre répertoire, c'est la raison.

Corriger une classe de comportements avec un booléen

Certains échecs ne sont pas liés à un problème de label. Un reverse proxy sur une instance Rocky ou AlmaLinux fraîchement installée renvoie une erreur 502, et le journal d'erreurs indique :

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

Votre upstream fonctionne correctement. Le domaine httpd_t n'est pas autorisé par défaut à ouvrir des connexions réseau sortantes, donc l'appel connect() est refusé avant même d'atteindre l'interface de loopback. Un seul commutateur contrôle ce comportement :

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

-P est le flag important : il écrit la valeur sur le disque. Sans -P, la modification est perdue au redémarrage suivant, ce qui donne un service fonctionnel jusqu'au reboot de la machine. Confirmez avec semanage boolean -l | grep httpd_can_network_connect, qui affiche la valeur active à côté de la valeur enregistrée.

Préférez un booléen à une règle écrite manuellement lorsqu'il en existe un. Les booléens sont fournis avec la politique de la distribution ; ils sont donc maintenus, documentés et faciles à trouver pour la personne suivante. getsebool -a liste tous les booléens présents sur le système.

Autoriser un service à écouter sur un port non standard

Les ports sont également étiquetés. Déplacez Nginx sur 8081 et il refusera de démarrer :

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

httpd_t peut lier les ports étiquetés http_port_t, et 8081 n'en fait pas partie. Ajoutez-le :

sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081

Vérifiez d'abord la liste. Plusieurs ports élevés sont déjà autorisés, dont 8008 et 8443, et l'ajout d'un port déjà présent échouera avec ValueError: Port tcp/8081 already defined. Si le port appartient déjà à un type différent, modifiez-le avec semanage port -m -t http_port_t -p tcp 8081 plutôt que de l'ajouter.

La même commande permet de faire fonctionner un port SSH modifié. Bind to port 2222 on 0.0.0.0 failed: Permission denied dans journalctl -u sshd signifie que 2222 est absent de ssh_port_t, exécutez donc sudo semanage port -a -t ssh_port_t -p tcp 2222 avant de redémarrer le daemon et de fermer votre session. C'est l'étape que les utilisateurs oublient lorsqu'ils suivent un guide générique sur le durcissement SSH sur un VPS sur une image de la famille Red Hat. SELinux n'est pas non plus un pare-feu, le port doit donc toujours être ouvert : sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload ici, ou ufw sur une image Debian ou Ubuntu. Le flag --permanent comporte le même piège au redémarrage que -P sur un booléen, et les zones qui déterminent à quelles interfaces une règle s'applique méritent d'être étudiées dans les bases de firewalld pour un VPS Rocky ou AlmaLinux.

Lorsqu'il n'y a ni booléen ni label à modifier

C'est rare sur un serveur standard, et c'est là que les erreurs causent des dommages. audit2allow peut générer un module de politique à partir des refus (denials) présents dans les logs :

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

Lisez nginx_local.te avant de l'installer. Deux habitudes permettent de travailler en sécurité. Filtrez l'entrée pour ne cibler que le programme que vous corrigez avec -c, car rediriger une semaine de refus non liés vers audit2allow autorise tout simultanément. N'installez jamais un module généré à partir d'un refus que vous ne pouvez pas expliquer : une règle qui autorise httpd_t à lire tous les fichiers de la machine est facile à créer mais difficile à identifier des mois plus tard. Supprimez un module avec sudo semodule -r nginx_local.

Le mode Permissive est un outil de diagnostic, pas une solution

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

Le mode Permissive autorise l'accès tout en le consignant dans les journaux. Son intérêt majeur réside dans l'exhaustivité. En mode Enforcing, le service s'arrête dès le premier refus ; vous corrigez ce point, redémarrez, puis rencontrez le suivant. En mode Permissive, l'exécution se poursuit et le journal collecte tous les refus en une seule passe. Vous pouvez alors repasser en mode Enforcing et corriger l'ensemble des problèmes simultanément.

setenforce ne modifie pas /etc/selinux/config, donc un redémarrage remet le serveur en mode Enforcing. C'est une sécurité, et c'est aussi la raison pour laquelle un « correctif » consistant à utiliser setenforce 0 réapparaît au pire moment. Si un service nécessite plus de souplesse pendant que vous travaillez dessus, marquez uniquement ce domaine plutôt que la machine entière : sudo semanage permissive -a httpd_t maintient tout le reste en mode Enforcing, et sudo semanage permissive -d httpd_t annule cette modification.

Pourquoi désactiver SELinux coûte plus cher que de corriger les labels

Définir SELINUX=disabled dans /etc/selinux/config revient à remplacer une correction de label d'une ligne par un affaiblissement permanent du serveur. La différence se manifeste le jour où une application web est compromise. En mode enforcing, le code de l'attaquant s'exécute dans httpd_t ; il peut donc lire le contenu web, mais la lecture de /etc/shadow ou l'écriture d'une unité systemd est refusée par la politique, indépendamment des permissions accordées à l'utilisateur Unix. Sans politique chargée, ce même code obtient tous les droits du compte de service.

La désactivation entraîne également une dette technique. Tant qu'aucune politique n'est chargée, les nouveaux fichiers sont créés sans label, ce qui désynchronise le système de fichiers de la politique. Réactiver SELinux nécessite alors un relabel complet, sous peine de voir une multitude de services échouer simultanément :

sudo fixfiles -F onboot
sudo reboot

Cette commande écrit /.autorelabel et réétiquette tout le système de fichiers au prochain démarrage. Sur un disque volumineux, l'opération est longue et la console semble figée ; lancez-la donc lorsque vous pouvez vous permettre une indisponibilité. Puisque la machine doit redémarrer, il est utile de vérifier au préalable ce qui est en attente, ce que rapporte needs-restarting après qu'une mise à jour dnf a laissé d'anciens noyaux et bibliothèques en mémoire. Sur Rocky Linux et AlmaLinux 9, le fichier de configuration ne désactive plus la partie noyau par lui-même, et la méthode documentée pour désactiver complètement SELinux consiste à utiliser un argument noyau (sudo grubby --update-kernel ALL --args selinux=0). Connaître cette commande est utile lorsque vous héritez du serveur d'un tiers. Ce n'est pas la solution pour une erreur 403.

Ajouter un label supplémentaire aux conteneurs

Sur un hôte de la famille Red Hat, les processus des conteneurs s'exécutent dans container_t et ne peuvent lire que les fichiers étiquetés container_file_t. Un bind mount depuis l'hôte échoue avec Permission denied à l'intérieur du conteneur, alors que ls -l sur l'hôte semble tout à fait normal. Le suffixe :Z indique au runtime de réétiqueter le montage :

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

:Z étiquette le répertoire pour ce conteneur uniquement. :z l'étiquette pour un partage entre plusieurs conteneurs. Si vous pointez :Z vers un répertoire utilisé par d'autres services, il réétiquette ce répertoire de manière récursive, ce qui interrompt ces services ; attribuez donc aux conteneurs leurs propres chemins. Si le moteur n'est pas encore installé sur la machine, notez que la commande docker sur ces distributions est souvent podman qui répond à ce nom, une subtilité que les étapes d'installation sur Rocky et AlmaLinux règlent avant que vous ne soyez confronté à ce problème. Tout le reste de la configuration est identique à n'importe quelle autre image, comme expliqué dans exécuter Docker sur un VPS.

Ubuntu et Debian utilisent AppArmor

Même fonction, conception différente. AppArmor restreint un programme selon le chemin de son exécutable, en utilisant un profil situé dans /etc/apparmor.d/, plutôt qu'en étiquetant les fichiers sur le disque. Il n'y a rien à réétiqueter et pas de restorecon. Commencez ici :

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

Un refus apparaît sous la forme apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Le flux de travail suit la même logique : lire le refus, trouver le profil, modifier la règle. sudo apt install apparmor-utils vous permet d'utiliser aa-complain (mode permissif pour un profil) et aa-enforce pour revenir à l'état initial. Ubuntu restreint un ensemble sélectionné de services packagés et laisse le reste sans restriction ; consultez donc aa-status pour voir ce qui est réellement actif au lieu de faire des suppositions.

Une habitude est commune aux deux systèmes. Lorsqu'un service signale Permission denied sur un élément qui semble correct, lisez le journal de sécurité avant de modifier les permissions. Les bits sont rarement la cause du problème deux fois.

FAQ

Pourquoi nginx renvoie-t-il une erreur 403 alors que les permissions des fichiers sont correctes ?

C'est parce que SELinux a refusé la lecture, et non les bits de permission. Le serveur web s'exécute dans le domaine httpd_t et ne peut lire que les fichiers étiquetés pour le contenu web. Un fichier étiqueté default_t ou admin_home_t est donc refusé et nginx n'a rien à servir. Confirmez cela avec sudo ausearch -m AVC -ts recent, qui affiche scontext se terminant par httpd_t et tcontext contenant le mauvais type. Enregistrez ensuite l'étiquette correcte et appliquez-la : sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" suivi de sudo restorecon -Rv /data/www.

Est-il sûr d'exécuter setenforce 0 pour faire fonctionner un service ?

setenforce 0 est une étape de diagnostic, pas une solution. Utilisez-la pour reproduire le problème une fois afin que le journal collecte chaque refus en un seul passage, lisez-les avec sudo ausearch -m AVC -ts recent, puis exécutez sudo setenforce 1 et corrigez les causes. Un serveur laissé en mode permissif enregistre chaque refus sans en bloquer aucun ; vous conservez donc le bruit tout en perdant la protection. Si un service a besoin de marge de manœuvre pendant que vous travaillez, exécutez sudo semanage permissive -a httpd_t afin que le reste de la machine demeure en mode enforcing.

Comment exécuter un service sur un port non standard avec SELinux en mode enforcing ?

Ajoutez le port au type que le service est autorisé à utiliser. Pour un serveur web sur 8081 : sudo semanage port -a -t http_port_t -p tcp 8081. Pour SSH sur 2222 : sudo semanage port -a -t ssh_port_t -p tcp 2222. Vérifiez d'abord la liste actuelle avec sudo semanage port -l | grep -w http_port_t, car un port déjà répertorié provoquera une erreur ValueError: Port tcp/8081 already defined. Sans cette étape, le démon s'arrête au démarrage avec bind() ... Permission denied, même si aucun autre processus n'occupe le port.

Ubuntu utilise-t-il SELinux ?

Non. Ubuntu et Debian utilisent AppArmor, qui applique un profil lié au chemin d'un exécutable plutôt qu'à des étiquettes sur les fichiers. Vérifiez-le avec sudo aa-status et recherchez les lignes apparmor="DENIED" dans sudo journalctl -k. Ubuntu restreint un ensemble sélectionné de services packagés, de sorte que de nombreux programmes s'exécutent sans restriction par défaut. Rocky Linux et AlmaLinux sont les distributions où vous rencontrerez SELinux en mode enforcing par défaut, tout comme Fedora et RHEL.