Vous déployez. Vous rechargez. Vous actualisez. Au lieu de votre site, vous obtenez un 403 ou un 404 net et peu utile. Hier ça marchait. Aujourd’hui Nginx agit comme s’il n’avait jamais entendu parler de vos fichiers.
Sur Debian 13, le chemin le plus rapide n’est pas de « fixer la config en la regardant jusqu’à en voir des étoiles ». C’est de prouver si vous êtes face à un mur de permissions, un mauvais routage/configuration, ou un mécanisme de sécurité (AppArmor, symlinks, environnements chroot-ish) qui fait son travail. Cet article présente le workflow pragmatique que j’utilise en production : devinettes minimales, signal maximal.
Playbook de diagnostic rapide
Voici l’ordre de triage qui gagne sous pression. Il est biaisé pour répondre d’abord à la seule question qui compte : « Est-ce que Nginx a le droit de lire le fichier qu’il tente de servir, et tente-t-il réellement de servir le fichier que je pense ? »
Premier point : lire le journal d’erreurs pour la requête exacte
Ne commencez pas par éditer la configuration. Commencez par regarder ce que dit Nginx au moment de l’échec. Pour un 403/404, le journal d’erreurs contient souvent la vérité en une ligne : le chemin résolu, la raison de l’échec, et parfois quel bloc serveur a pris la requête.
Second point : confirmer quel bloc serveur gère la requête
La moitié des incidents de « 404 soudain » sont en réalité des « vous êtes sur le mauvais hôte virtuel ». Les changements Debian, renouvellements de certificats, ajouts de blocs serveur ou un nouveau site par défaut peuvent voler le trafic en silence.
Troisième point : valider la résolution du chemin (root/alias/try_files)
403/404 revient souvent à : l’URI demandé ne correspond pas au fichier que vous croyez. Nginx applique un modèle de mapping très littéral. Si vous vous trompez d’une barre oblique, vous vous trompez.
Quatrième point : tester les permissions du système de fichiers sur tout le chemin
Nginx n’a pas seulement besoin d’autorisation de lecture sur le fichier ; il a besoin du bit d’exécution (« traverse ») sur chaque répertoire du chemin. Un répertoire trop strict au milieu renvoie « permission denied » même si le fichier lui-même est lisible par tous.
Cinquième point : vérifier la politique LSM (AppArmor sur Debian est courant)
Sur Debian, les profils AppArmor peuvent refuser des lectures d’une façon qui ressemble aux permissions Unix ordinaires ou à « fichier introuvable ». Vos journaux vous diront si vous écoutez.
Sixième point : vérifier que vous n’avez pas rechargé une config qui ne correspond pas au processus actif
Systemd peut afficher « active (running) » alors que Nginx sert une ancienne config parce qu’un reload a échoué, ou parce que vous avez rechargé la mauvaise instance/conteneur/chroot. Validez rapidement la configuration chargée.
Règle opérationnelle : si vous ne pouvez pas reproduire avec curl -v tout en suivant access+error logs, vous êtes en train de déboguer une rumeur.
Modèle mental instantané : ce que 403 vs 404 signifie vraiment dans Nginx
Nginx est déterministe. Si vous pensez qu’il est aléatoire, c’est que vous n’avez pas encore trouvé l’entrée qui le fait se comporter ainsi.
403 Forbidden : Nginx a trouvé « l’endroit », mais refuse de servir l’objet
403 est généralement l’un de ces cas :
- Le fichier existe mais est illisible par l’utilisateur worker de Nginx.
- Traversée de répertoire bloquée (bit d’exécution absent sur un répertoire parent).
- Index de répertoire interdit : vous avez demandé un répertoire et Nginx ne trouve pas de fichier index et autoindex est désactivé.
- Règles deny explicites dans la config (par ex.,
deny all;, listes d’IP autorisées). - Politique de symlink :
disable_symlinksou des options de montage particulières peuvent provoquer un refus. - Refus AppArmor.
404 Not Found : Nginx n’a pas pu mapper l’URI sur un fichier (ou a choisi de ne pas le révéler)
404 signifie souvent « mauvais root/alias » ou « mauvais bloc serveur », mais cela peut aussi être une dissimulation délibérée : certaines configs renvoient 404 pour du contenu interdit afin d’éviter de révéler l’existence de fichiers sensibles.
Pourquoi vous ne pouvez pas vous fier uniquement au code de statut
Nginx peut être configuré pour renvoyer 404 pour du contenu interdit, ou pour réécrire vers un emplacement interne qui renvoie autre chose. Donc la règle : le code est un indice ; les logs sont la preuve.
Blague n°1 : quand une personne d’astreinte dit « c’est juste un 404 », j’entends « c’est juste un incendie, mais il est dans les journaux. »
Faits intéressants et contexte historique (utile, pas du trivia)
- Nginx a été conçu pour une concurrence prévisible (modèle événementiel) où une requête mal routée peut échouer de façon très cohérente à grande échelle — idéal pour déboguer si vous regardez une requête représentative.
- 403 vs 404 a une histoire de sécurité : de nombreuses organisations renvoient intentionnellement 404 pour les chemins protégés afin de réduire l’énumération d’endpoints.
- Le bit « execute » Unix sur les répertoires signifie « peut traverser », pas « peut exécuter ». Un répertoire peut être lisible mais non traversable, ce qui embrouille même des développeurs expérimentés.
- Les choix par défaut du packaging Debian favorisent la sécurité : la disposition standard des sites sous
/etc/nginx/sites-availableetsites-enabledvise à réduire les expositions accidentelles, mais elle crée aussi des incidents de « mauvais vhost » lors des changements. - AppArmor s’est imposé comme contrôle mainstream dans de nombreuses distributions pour fournir un contrôle d’accès obligatoire sans la lourdeur opérationnelle de SELinux, et il peut bloquer silencieusement des chemins hors des racines web attendues.
- Alias vs root est un piège historique :
aliasde Nginx se comporte différemment derootdans les blocs location, et une barre oblique manquante peut transformer une URI valide en un 404 garanti. - Les locations « internal » de Nginx sont largement utilisées pour l’auth et la gestion d’erreurs ; elles peuvent faire afficher un 404 à votre navigateur alors que Nginx rencontre en réalité une erreur de permission ailleurs.
- Les règles de sélection du serveur par défaut importent : si aucun
server_namene correspond, Nginx choisit un défaut. Ajouter un nouveau bloc serveur peut changer qui devient « défaut » et provoquer des 404 « soudains ».
Tâches pratiques : commandes, ce que signifie la sortie et la décision suivante
Voici des tâches réelles que j’exécute en production. Copiable/collable. Chaque section inclut : commande, éléments à vérifier, et la décision qu’elle déclenche.
Tâche 1 : Reproduire avec curl et capturer les en-têtes
cr0x@server:~$ curl -sv -o /dev/null http://example.internal/static/app.css
* Trying 127.0.0.1:80...
* Connected to example.internal (127.0.0.1) port 80 (#0)
> GET /static/app.css HTTP/1.1
> Host: example.internal
> User-Agent: curl/8.6.0
> Accept: */*
< HTTP/1.1 404 Not Found
< Server: nginx/1.26.2
< Date: Mon, 29 Dec 2025 10:12:32 GMT
< Content-Type: text/html
< Content-Length: 153
< Connection: keep-alive
Sens : vous confirmez que c’est Nginx qui répond, pas un CDN ou un upstream, et vous avez l’URI et l’en-tête Host exacts.
Décision : conservez la valeur exacte de Host ; vous l’utiliserez pour identifier le bloc serveur.
Tâche 2 : Surveiller le journal d’erreurs pendant la reproduction
cr0x@server:~$ sudo tail -Fn0 /var/log/nginx/error.log
2025/12/29 10:12:32 [error] 1842#1842: *921 open() "/srv/www/example/static/app.css" failed (2: No such file or directory), client: 127.0.0.1, server: example.internal, request: "GET /static/app.css HTTP/1.1", host: "example.internal"
Sens : Nginx a tenté d’ouvrir un chemin de fichier concret. Le code d’erreur (2) est « No such file or directory ». Ce n’est pas un problème de permissions ; c’est du mapping de chemin ou un fichier manquant.
Décision : vérifiez que le fichier existe à ce chemin exact et confirmez la logique root/alias pour /static/.
Tâche 3 : Surveiller le journal d’accès pour confirmer le code réellement écrit
cr0x@server:~$ sudo tail -n 3 /var/log/nginx/access.log
127.0.0.1 - - [29/Dec/2025:10:12:32 +0000] "GET /static/app.css HTTP/1.1" 404 153 "-" "curl/8.6.0"
Sens : confirme que ce n’est pas un artefact de cache du navigateur ou des codes mélangés. Une requête, un code.
Décision : poursuivre sur Nginx+système de fichiers, pas sur l’app upstream.
Tâche 4 : Vérifier la syntaxe de la config Nginx avant d’approfondir
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Sens : vous ne poursuivez pas sur un reload cassé qui ne s’est jamais appliqué.
Décision : passez à « quel bloc serveur et quel root » plutôt qu’à « Nginx parse-t-il la config ».
Tâche 5 : Dumper la configuration chargée (étape « que tourne réellement Nginx »)
cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '1,120p'
# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Sens : vous voyez les includes et les valeurs par défaut clés, en particulier l’utilisateur des workers (www-data par défaut sur Debian).
Décision : confirmez que le fichier de site que vous avez modifié est bien dans sites-enabled et n’est pas masqué par un autre include.
Tâche 6 : Identifier quel bloc serveur correspond à votre en-tête Host
cr0x@server:~$ sudo nginx -T 2>/dev/null | awk '
$1=="server" && $2=="{" {inserver=1; sn=""; ls=""; fn=FILENAME}
inserver && $1=="server_name" {sn=$0}
inserver && $1=="listen" {ls=$0}
inserver && $1=="}" {print ls " | " sn; inserver=0}
'
listen 80; | server_name example.internal;
listen 80 default_server; | server_name _;
Sens : vous pouvez voir si votre hôte est explicitement apparié, ou s’il tombe dans le serveur par défaut.
Décision : si vous atteignez server_name _; ou un serveur par défaut de façon inattendue, corrigez l’ordre des vhosts / default_server, et arrêtez de toucher aux permissions.
Tâche 7 : Valider que le chemin de fichier résolu existe
cr0x@server:~$ sudo ls -la /srv/www/example/static/app.css
ls: cannot access '/srv/www/example/static/app.css': No such file or directory
Sens : le fichier n’existe pas réellement au chemin que Nginx a tenté d’utiliser.
Décision : trouvez la racine web correcte, corrigez root/alias/try_files, ou corrigez votre déploiement qui n’a pas livré l’asset.
Tâche 8 : Si le fichier existe, tester l’accès en lecture en tant qu’utilisateur Nginx
cr0x@server:~$ sudo -u www-data head -c 64 /srv/www/example/static/app.css
head: cannot open '/srv/www/example/static/app.css' for reading: Permission denied
Sens : classique problème de permissions. Vous avez maintenant la preuve avec le même utilisateur que Nginx utilise.
Décision : corrigez la propriété/permissions/ACL sur la chaîne de répertoires. Ne faites pas un « chmod 777 » pour éviter un rapport de sécurité.
Tâche 9 : Vérifier les bits de traversée de répertoire le long du chemin
cr0x@server:~$ namei -l /srv/www/example/static/app.css
f: /srv/www/example/static/app.css
drwxr-xr-x root root /
drwxr-xr-x root root srv
drwx------ root root www
drwxr-xr-x root root example
drwxr-xr-x root root static
-rw-r--r-- root root app.css
Sens : /srv/www est drwx------. Même si le fichier est lisible, www-data ne peut pas traverser ce répertoire, donc Nginx échouera.
Décision : ajustez les permissions des répertoires (bit d’exécution pour l’utilisateur/groupe Nginx) ou déplacez le contenu vers une racine web prévue pour être servie.
Tâche 10 : Repérer rapidement les refus AppArmor
cr0x@server:~$ sudo journalctl -k -g apparmor --since "10 minutes ago"
Dec 29 10:11:58 server kernel: audit: type=1400 audit(1767003118.123:91): apparmor="DENIED" operation="open" profile="nginx" name="/srv/www/example/static/app.css" pid=1842 comm="nginx" requested_mask="r" denied_mask="r" fsuid=33 ouid=0
Sens : le contrôle d’accès obligatoire a bloqué la lecture. Les permissions Unix peuvent être correctes, mais la politique dit « non ».
Décision : soit ajustez le profil pour autoriser ce chemin, soit servez les fichiers depuis des emplacements autorisés. Modifier les chmod ne règlera pas ça.
Tâche 11 : Confirmer l’utilisateur et les processus master/worker en cours
cr0x@server:~$ ps -o user,pid,cmd -C nginx
USER PID CMD
root 1721 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data 1842 nginx: worker process
www-data 1843 nginx: worker process
Sens : les workers tournent sous www-data. C’est l’identité qui doit lire votre contenu.
Décision : arrêtez de deviner quel « user ». Corrigez l’accès pour www-data (ou pour l’utilisateur configuré si vous l’avez changé).
Tâche 12 : Détecter si la requête est réécrite vers autre chose
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R --line-number -E 'try_files|rewrite|return 404|error_page 404|internal' /etc/nginx/sites-enabled
/etc/nginx/sites-enabled/example.conf:27: try_files $uri $uri/ /index.html;
/etc/nginx/sites-enabled/example.conf:41: error_page 403 404 = /errors/notfound.html;
/etc/nginx/sites-enabled/example.conf:42: location = /errors/notfound.html { internal; }
Sens : même un 403 peut être mappé vers une 404 interne, et try_files peut rediriger des fichiers statiques manquants vers un point d’entrée SPA. Votre navigateur peut voir un 404 alors que Nginx fait exactement ce que vous lui avez demandé.
Décision : décidez si ce comportement de réécriture est intentionnel ; sinon, ajustez try_files ou le mapping error_page.
Tâche 13 : Vérifier si vous touchez le « mauvais » site par défaut
cr0x@server:~$ ls -l /etc/nginx/sites-enabled
total 0
lrwxrwxrwx 1 root root 34 Dec 29 09:48 default -> /etc/nginx/sites-available/default
lrwxrwxrwx 1 root root 34 Dec 29 09:49 example.conf -> /etc/nginx/sites-available/example.conf
Sens : le site par défaut de Debian est activé. S’il a default_server sur votre directive listen, il peut attraper des hosts non appariés et renvoyer le mauvais contenu (ou 404).
Décision : soit désactivez le site par défaut en production, soit faites de votre vhost prévu le défaut explicitement.
Tâche 14 : Vérifier que les options de montage du système de fichiers ne vous sabotent pas
cr0x@server:~$ findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS /srv
/dev/mapper/vg0-srv /srv ext4 rw,relatime
Sens : les options de montage ne sont généralement pas la cause d’un 403/404, mais elles peuvent compter (par ex., état lecture seule étrange, bind mounts, ou comportement d’overlay dans des conteneurs).
Décision : si vous voyez un ro inattendu ou des montages bind/overlay, confirmez que les chemins de déploiement et les montages de conteneurs correspondent à vos attentes.
Tâche 15 : Confirmer le comportement d’index lorsqu’un répertoire est demandé
cr0x@server:~$ curl -svo /dev/null http://example.internal/static/
* Trying 127.0.0.1:80...
> GET /static/ HTTP/1.1
> Host: example.internal
< HTTP/1.1 403 Forbidden
< Server: nginx/1.26.2
Sens : demander un répertoire peut produire un 403 quand aucun fichier index n’existe et que autoindex est désactivé.
Décision : ajoutez un index (par ex., index.html), activez autoindex (rarement correct en production), ou changez le routage pour éviter les URI de répertoire.
Tâche 16 : Confirmer que le déploiement n’a pas changé la propriété des fichiers
cr0x@server:~$ sudo stat -c '%U %G %a %n' /srv/www/example /srv/www/example/static /srv/www/example/static/app.css
root root 755 /srv/www/example
root root 750 /srv/www/example/static
deploy deploy 640 /srv/www/example/static/app.css
Sens : le fichier appartient à deploy avec le mode 640. Si www-data n’est pas dans le groupe deploy, il ne peut pas le lire.
Décision : corrigez la stratégie de propriété/groupe (commun : contenu en root:www-data avec 644), ou utilisez setgid sur les répertoires et fichiers lisibles par le groupe, ou appliquez des ACLs pour www-data.
Tâche 17 : Vérifier les refus liés aux symlinks
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R --line-number 'disable_symlinks' /etc/nginx
/etc/nginx/nginx.conf:63: disable_symlinks if_not_owner from=$document_root;
Sens : si votre contenu utilise des symlinks (fréquent dans les déploiements), Nginx peut refuser de les servir selon la propriété.
Décision : soit alignez la propriété, supprimez les symlinks pour le contenu servi, soit ajustez la politique volontairement (en connaissance de cause).
Tâche 18 : Confirmer que le reload a bien eu lieu et n’a pas échoué silencieusement
cr0x@server:~$ sudo systemctl reload nginx; sudo systemctl status nginx --no-pager -l
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-29 09:40:10 UTC; 33min ago
Docs: man:nginx(8)
Main PID: 1721 (nginx)
Tasks: 5 (limit: 18754)
Memory: 8.4M
CPU: 1.142s
CGroup: /system.slice/nginx.service
├─1721 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
├─1842 "nginx: worker process"
└─1843 "nginx: worker process"
Sens : le statut montre que Nginx tourne, mais ne prouve pas que le reload a été appliqué. Associez cela à nginx -T et aux timestamps des logs lorsque vous modifiez.
Décision : si vous suspectez un échec de reload, vérifiez les entrées journald pour des erreurs de reload et refaites nginx -t.
Échecs de permissions qui ressemblent à des bugs de config
Le piège de traversée de répertoire (celui qui mord les expérimentés)
Vous pouvez appliquer chmod 644 sur un fichier toute la journée. Si un répertoire parent manque du bit d’exécution pour l’utilisateur Nginx (ou son groupe), Nginx ne peut pas y accéder. Le résultat est un 403, et le journal d’erreurs affichera souvent (13: Permission denied).
Sur Debian, Nginx tourne typiquement sous www-data. Donc le test canonique est : est-ce que www-data peut lire le fichier ? Pas « est-ce que root peut le lire ». Root peut lire votre journal intime ; Nginx non.
Dérive de propriété lors des déploiements
Les pipelines CI/CD qui rsync des fichiers, décompressent des tarballs ou changent des symlinks peuvent modifier la propriété et les modes. Un utilisateur de déploiement laisse des fichiers en deploy:deploy avec 640, et soudain les assets statiques sont morts.
Réglez-le à la source : définissez un groupe cohérent pour le contenu servi, imposez un umask, ou appliquez des ACLs. « Fixer les permissions après chaque déploiement » n’est pas une stratégie ; c’est un incident récurrent.
AppArmor : des permissions dont vous ignoriez l’existence
Debian utilise couramment AppArmor. Si votre profil Nginx autorise /var/www/** et que vous servez depuis /srv/www/**, Nginx peut logger « permission denied » même si les bits Unix sont parfaits.
Les logs d’audit du noyau nommeront le profil et le chemin. C’est votre preuve irréfutable. Si vous ne vérifiez pas, vous passerez des heures à « corriger » des chmod sans rien obtenir.
Symlinks, propriété et « disable_symlinks »
Les symlinks sont populaires pour des déploiements atomiques : current -> releases/2025-12-29. Nginx peut être configuré pour restreindre le service des symlinks afin de prévenir une classe de problèmes de traversée de chemin et de surprises liées à la propriété. C’est un bon contrôle — jusqu’à ce que quelqu’un oublie qu’il existe.
Si vous voyez des erreurs liées aux symlinks, décidez si vous voulez que Nginx serve les symlinks du tout. Si oui, rendez-le cohérent et explicite ; sinon, faites en sorte que votre déploiement cesse d’utiliser des symlinks sous la racine du document.
Échecs de configuration qui ressemblent à des permissions
Mauvais bloc serveur (roulette du serveur par défaut)
Si l’en-tête Host ne correspond à aucun server_name, Nginx sélectionne un défaut. Ce défaut peut avoir un root différent, tout refuser, ou pointer vers un répertoire vide. Résultat : 404 ou 403 « soudain ».
La correction est ennuyeuse : faites correspondre votre server_name à la réalité, et contrôlez default_server intentionnellement. Ne laissez pas « le fichier qui s’affiche le premier » définir le comportement en production.
Alias vs root : mêmes mots, physique différente
Celle-ci mérite de la franchise : si vous utilisez alias sans le comprendre, vous finirez par livrer un 404.
root ajoute l’URI (ou une partie) à un chemin de base. alias remplace le préfixe de location par le chemin d’alias. Cela signifie que la présence ou l’absence d’une barre oblique finale peut changer le chemin résultant. Nginx n’est pas « intelligent » ici ; il est cohérent.
try_files : le rerouter silencieux
try_files est fantastique : il permet de servir des assets statiques si présents et de retomber sur une route applicative sinon. C’est aussi une machine à créer des 404 confus quand vous redirigez des fichiers manquants vers un fallback qui lui-même manque ou est interdit.
En débogage, localisez la séquence exacte des chemins de try_files et vérifiez que le fallback existe et est lisible.
Gestion des index : un 403 qui n’est pas une « permission OS »
Une demande de répertoire comme /static/ peut renvoyer 403 parce que Nginx interdit la liste des répertoires à moins d’activer autoindex, et parce qu’aucun fichier index n’existe. Ce n’est pas le OS qui refuse l’accès ; c’est Nginx qui refuse de fournir la liste.
Masquage par error_page personnalisé
Les équipes sécurité aiment mapper 403 en 404. Parfois elles ont raison. Mais cela complique la vie en astreinte. Si vous héritez d’une config, cherchez error_page et les locations internes avant de décider de ce que signifie le code de statut.
Blague n°2 : « Nous avons mappé 403 en 404 pour la sécurité » est comme repeindre le voyant moteur — techniquement efficace jusqu’à ce que le moteur lâche.
Erreurs courantes : symptômes → cause racine → correction
1) Symptom : 404 pour chaque chemin sur un domaine connu
Cause racine : le mauvais bloc serveur capture les requêtes (mismatch Host ; default_server changé ; nouveau vhost ajouté).
Correction : vérifiez le Host avec curl, puis confirmez le server_name correspondant. Rendre le vhost voulu explicite ; désactiver le site par défaut Debian s’il n’est pas nécessaire.
2) Symptom : 403 sur des répertoires, 200 sur des fichiers connus
Cause racine : répertoire demandé sans fichier index ; autoindex désactivé.
Correction : ajoutez index index.html; et assurez-vous que le fichier existe, ou évitez les liens vers des URI de répertoire, ou activez intentionnellement autoindex on; (rarement correct).
3) Symptom : 403 sur des fichiers statiques spécifiques après un déploiement
Cause racine : dérive de propriété/mode : fichiers créés en 640 par l’utilisateur de déploiement ; le groupe ne contient pas www-data.
Correction : imposez une propriété cohérente, par ex. root:www-data avec 644, ou utilisez des répertoires setgid avec fichiers lisibles par le groupe, ou appliquez des ACLs pour www-data.
4) Symptom : 404, mais le journal d’erreurs montre « permission denied »
Cause racine : la config masque l’interdit en « non trouvé » via un mapping error_page ou des réécritures internes.
Correction : retirez le masquage pendant le débogage ; inspectez error_page et les règles de rewrite ; corrigez le problème de permission/LSM sous-jacent.
5) Symptom : 404 sur des assets sous /static, mais le fichier existe ailleurs
Cause racine : alias utilisé avec une mauvaise barre oblique finale, ou root déclaré au mauvais niveau (server vs location).
Correction : calculez le chemin résolu. Préférez des patterns cohérents ; validez avec la ligne open() du journal d’erreurs.
6) Symptom : Tout fonctionne en root quand vous testez, mais Nginx renvoie toujours 403
Cause racine : vous avez testé en root, pas en www-data, et vous avez manqué les bits de traversée ou les ACLs.
Correction : testez toujours les lectures avec sudo -u www-data et namei -l.
7) Symptom : Les permissions semblent correctes, mais toujours 403/404
Cause racine : AppArmor refuse l’accès au chemin (souvent contenu déplacé vers /srv, /data, ou un bind mount).
Correction : confirmez le refus dans les logs du noyau ; mettez à jour le profil AppArmor ou déplacez le contenu vers des chemins autorisés.
8) Symptom : 404 aléatoire après ajout d’un nouveau site
Cause racine : nouveau bloc serveur devenu par défaut pour un socket d’écoute ; ou server_name et directives listen qui se chevauchent causent des correspondances ambiguës.
Correction : définissez exactement un default par socket d’écoute ; validez avec nginx -T et des curl ciblés avec les en-têtes Host appropriés.
Listes de vérification / plan étape par étape
Étape par étape : diagnostiquer un 403 soudain
- Reproduire avec curl en utilisant l’en-tête Host correct. Capturez le statut et l’en-tête server.
- Suivre error.log pendant la reproduction ; cherchez
(13: Permission denied),directory index of ... is forbidden, ouaccess forbidden by rule. - Confirmer le bloc serveur (matching server_name, comportement default_server).
- Calculer le chemin cible à partir de la ligne du log. Ne devinez pas.
- Tester la lecture en tant que www-data et exécuter
namei -lpour repérer un répertoire verrouillé. - Vérifier les refus AppArmor dans les logs du noyau.
- Corriger la plus petite chose (un mode de répertoire, une ACL, une ligne root), recharger, retester.
Étape par étape : diagnostiquer un 404 soudain
- Confirmer que c’est Nginx et pas l’upstream en vérifiant les en-têtes et le journal d’accès.
- Trouver le chemin open() dans error.log. Si c’est
(2: No such file or directory), vous êtes en territoire mapping/déploiement. - Vérifier que le fichier existe à ce chemin exact.
- Si le fichier existe ailleurs, revoyez
root/aliaset le matchinglocation; vérifieztry_files. - Confirmer que vous êtes dans le vhost prévu et non dans un catchall par défaut.
- Cherchez les règles de masquage qui transforment l’interdit en introuvable.
- Recharger avec validation (
nginx -tpuis reload) et retester.
Checklist opérationnelle : durcir pour éviter les récurrences
- Désactivez le site par défaut Debian sur les hôtes de production sauf si vous le souhaitez vraiment.
- Journalisez le chemin résolu (error.log le fait déjà ; gardez-le activé à un niveau raisonnable).
- Faites de la propriété et des permissions une partie de l’artifact de déploiement, pas une étape post-exécution.
- Garder le contenu servi sous un petit nombre de racines connues et alignez la politique AppArmor sur celles-ci.
- Utilisez
nginx -ten CI et avant les reloads ; échouez rapidement. - Lorsque vous utilisez des déploiements par symlink, décidez explicitement de la politique
disable_symlinkset documentez-la.
Trois mini-récits du monde de l’entreprise (anonymisés, plausibles)
Mini-récit n°1 (mauvaise hypothèse) : « 403 veut dire que notre WAF le bloque »
Le symptôme semblait net : une rafale de 403 sur un chemin d’assets statiques juste après une petite release. L’ingénieur d’astreinte a supposé que c’était la couche edge/WAF, car la page 403 ne correspondait pas à la page d’erreur Nginx habituelle. Tout le monde s’est rué sur le tableau de bord de sécurité.
Pendant ce temps, le journal d’erreurs sur l’origine racontait une autre histoire : open() ".../app.css" failed (13: Permission denied). Le job de déploiement avait cessé de packager les fichiers en root:www-data pour les packager en deploy:deploy avec un umask restrictif. Le contenu avait atterri en 640, illisible par www-data.
La théorie « WAF » a duré parce que les gens faisaient davantage confiance au corps HTML de la page d’erreur qu’aux logs du serveur. Mais le corps provenait d’un mapping error_page personnalisé qui renvoyait une page brandée pour 403 et 404. Le code de statut était réel ; la page était de la décoration.
La correction fut ennuyeuse et durable : faire produire au build la propriété et le mode corrects dans l’artifact, l’imposer dans l’étape de déploiement, et ajouter un smoke test qui lit un fichier connu en tant que www-data avant de marquer le déploiement comme sain.
La leçon n’était pas « ne blâmez pas le WAF ». C’était : arrêtez de traiter les pages d’état comme preuve. Les logs sont la preuve.
Mini-récit n°2 (optimisation qui a mal tourné) : « Durcissons les symlinks pour la sécurité »
Une équipe plateforme a durci Nginx avec disable_symlinks if_not_owner from=$document_root;. L’intention était raisonnable : prévenir une classe d’attaques par symlink et réduire le périmètre d’impact si un développeur pointe accidentellement vers quelque chose de sensible.
Puis une équipe produit a déployé un pattern de déploiement atomique utilisant des symlinks sous la racine du document : /srv/www/app/current pointait vers un nouveau dossier release créé par l’utilisateur de déploiement. La propriété différait entre la cible du symlink et la racine du document, parce que les dossiers de release étaient créés par des jobs CI avec un mappage UID différent.
Résultat : des 403 intermittents selon quels assets étaient résolus via des symlinks, et quels fichiers appartenaient à quel utilisateur après l’étape de build. Les erreurs étaient correctes. La configuration était correcte. Le design système n’était pas aligné avec la politique.
Le rollback a corrigé vite, mais ce fut un électrochoc : les « bascules de durcissement » ne sont pas gratuites. Si vous activez un contrôle de sécurité qui change la sémantique du système de fichiers, vous devez le valider contre votre modèle de déploiement. Sécurité et fiabilité ne sont pas ennemies, mais elles exigent de la coordination.
Mini-récit n°3 (pratique routinière qui a sauvé la mise) : « On suit toujours les logs pendant la repro »
Une équipe avait une règle simple : quand vous pouvez reproduire une panne web, vous la reproduisez depuis le serveur avec curl tout en suivant les logs. Pas d’exceptions, pas de débat. Ce n’était pas une culture héroïque ; c’était une culture qui fait gagner du temps.
Un matin, un hôte Debian 13 a commencé à renvoyer des 404 pour un seul domaine tandis que d’autres domaines sur la même instance Nginx étaient OK. L’hypothèse réflexe était « quelqu’un a supprimé des fichiers » ou « rsync a échoué ». L’astreinte a fait le rituel : curl + tail access/error logs.
Le journal d’erreurs montrait que Nginx cherchait sous la mauvaise racine, et le champ server de la ligne d’erreur ne correspondait pas au domaine prévu. Cela a mené directement au vrai souci : un nouveau vhost avec listen 80 default_server; avait été déployé dans le cadre d’un changement d’une autre équipe, volant silencieusement les hosts non appariés.
La correction a pris des minutes : retirer default_server du vhost non voulu, reload, vérifier le routage d’hôte avec curl. Pas de restauration de fichiers. Pas de churn de permissions. Pas de salle de crise dramatique.
La pratique « ennuyeuse » n’était pas de suivre les logs. C’était de convenir que la preuve l’emporte sur la théorie, et de rendre cet accord opérationnel.
Comment distinguer instantanément : la hiérarchie des preuves
Si vous voulez une seule recommandation, c’est cette hiérarchie — utilisez-la pour rester honnête :
- La ligne du journal d’erreurs pour la requête (contient le chemin résolu et le code d’erreur du noyau).
- Entrée du journal d’accès (confirme host, URI, code, heure).
- Reproduction depuis l’hôte avec curl (élimine DNS et variabilité edge).
- Test du système de fichiers en tant que www-data (prouve les permissions réelles).
- Logs d’audit LSM (prouvent les refus par politique obligatoire).
- Ensuite seulement : revue de configuration et refactoring.
Une citation que je garde en tête quand les incidents deviennent bruyants :
idée paraphrasée — W. Edwards Deming : Sans données, vous n’êtes qu’une autre personne avec une opinion.
C’est à peu près l’astreinte en une phrase.
FAQ
1) « Si c’est un 404, ce n’est pas une permission, non ? »
Faux. C’est souvent du mapping, mais les configs peuvent convertir intentionnellement un 403 en 404 (error_page ou réécritures internes). Vérifiez toujours error.log pour (13) vs (2).
2) « Où se trouve le journal d’erreurs Nginx sur Debian 13 ? »
Typiquement /var/log/nginx/error.log, sauf si redéfini par error_log dans /etc/nginx/nginx.conf ou un fichier de site. Confirmez avec nginx -T.
3) « Pourquoi je reçois 403 pour un répertoire mais 200 pour des fichiers à l’intérieur ? »
Parce que la requête de répertoire déclenche la gestion d’index. S’il n’y a pas de fichier index et que autoindex est désactivé, Nginx renvoie 403 (« directory index … is forbidden »). Ce n’est pas une permission OS.
4) « Quelle est la façon la plus rapide de confirmer un mismatch de vhost ? »
Utilisez curl -sv avec l’en-tête Host voulu, puis inspectez le champ server: dans la ligne du journal d’erreurs (il imprime souvent le server_name apparié). Dump la config avec nginx -T et cherchez les blocs server.
5) « J’ai changé les permissions et ça échoue encore. Et maintenant ? »
Vérifiez les refus AppArmor dans les logs du noyau. Si vous voyez apparmor="DENIED" pour nginx, chmod n’aidera pas. Soit mettez à jour le profil, soit servez depuis des chemins permis.
6) « Dois-je faire tourner les workers Nginx sous un autre utilisateur que www-data ? »
Seulement si vous avez un objectif d’isolation clair et de la discipline opérationnelle. Changer l’utilisateur des workers sans corriger la stratégie de propriété et d’ACL est un excellent moyen de fabriquer des 403.
7) « Pourquoi namei est important si j’ai déjà vérifié les permissions du fichier ? »
Parce que les répertoires ont besoin du bit d’exécution pour la traversée. namei -l montre les permissions sur chaque segment de chemin, vous permettant de repérer le répertoire verrouillé qui casse tout.
8) « Comment éviter les erreurs alias/root pour les fichiers statiques ? »
Choisissez une convention et tenez-vous y. Si vous utilisez alias, soyez méticuleux avec les barres obliques finales et confirmez le chemin résolu via le journal d’erreurs. Si vous pouvez utiliser root proprement, c’est souvent plus simple.
9) « Un reload qui échoue peut-il laisser Nginx servir une ancienne config ? »
Oui. Un reload peut échouer à cause d’une erreur de syntaxe ou de permissions. Lancez toujours nginx -t avant un reload, et vérifiez avec nginx -T quand les enjeux sont élevés.
10) « Pourquoi un déploiement causerait un 404 au lieu d’un 403 ? »
Si les assets n’ont pas été livrés au chemin attendu (étape build changée, exclusions rsync, artifact erroné), Nginx ne les trouvera littéralement pas : (2: No such file or directory). C’est un 404, et c’est votre pipeline de déploiement qui parle.
Conclusion : prochaines étapes que vous pouvez réellement faire
Si votre Nginx Debian 13 renvoie soudainement 403/404, ne négociez pas avec vos hypothèses. Suivez le playbook rapide :
- Reproduisez avec
curl -sven utilisant le vrai en-tête Host. - Suivez
/var/log/nginx/error.loget capturez la ligneopen()exacte (chemin + errno). - Confirmez la sélection du vhost (
nginx -Tet server_name/default_server). - Testez l’accès au système de fichiers en tant que
www-dataet vérifiez la traversée de répertoire avecnamei -l. - Si cela ne colle toujours pas, vérifiez les refus AppArmor dans les logs du noyau.
Puis corrigez une chose, rechargez, et retestez. La meilleure réponse à un incident laisse derrière elle un garde-fou : une politique de permissions de déploiement, un contrôle de sanity des vhosts, ou une règle AppArmor alignée sur votre layout de fichiers.