Un 403 est le type d’erreur qui fait débattre les équipes avec assurance. « C’est nginx. » « Non, c’est Apache. »
« C’est SELinux. » (Sur Debian. Bien sûr.) Pendant ce temps la page est hors ligne, l’astreinte est réveillée, et quelqu’un est
à une touche près de chmod -R 777.
Ce cas porte sur l’arrêt des 403 de manière ennuyeuse et correcte : comprendre la sémantique des permissions Linux pour
les web roots sur Debian/Ubuntu, diagnostiquer rapidement, et mettre en place un modèle qui garde la production sécurisée et
déployable. L’objectif est simple : le serveur web peut lire ce dont il a besoin, n’écrire que là où vous l’autorisez explicitement,
et personne n’a besoin de « juste essayer 777 ».
Méthode de diagnostic rapide
Quand vous voyez un 403, vous recherchez l’un des trois goulots d’étranglement : la configuration du serveur web le refuse,
le système de fichiers le refuse, ou une couche intermédiaire (chroot, montage de conteneur, AppArmor) le refuse.
Ne devinez pas. Prouvez-le.
Première étape : confirmer quelle couche refuse
- Vérifiez le journal d’erreurs du serveur web pour “permission denied”, “access forbidden by rule”, ou “directory index forbidden”.
- Vérifiez le mappage de la requête : quel chemin sur disque tente-t-on de servir ?
- Reproduisez localement avec l’utilisateur effectif du serveur web (
www-datasur Debian/Ubuntu par défaut).
Deuxième étape : parcourez le chemin, pas seulement le fichier
La plupart des échecs de permissions ne portent pas sur le fichier. Ils concernent un répertoire parent que vous avez oublié.
Chaque répertoire du chemin a besoin du bit d’exécution pour la traversée. Pas de la lecture. Exécution.
Troisième étape : vérifiez les bloqueurs « politiques »
- Profils AppArmor (Ubuntu) peuvent refuser des lectures même si les bits de mode semblent corrects.
- Les montages de volumes en conteneur peuvent modifier la propriété/les modes de façon surprenante.
- Les liens symboliques peuvent pointer en dehors des racines autorisées (Apache) ou vers des répertoires sans traversée.
Si vous faites ces trois étapes, vous cesserez de traiter les 403 comme un événement surnaturel.
Le modèle de permissions qui fonctionne réellement (et qui n’aboutit pas à 777)
Sur Debian/Ubuntu, les processus workers de votre serveur web s’exécutent typiquement en tant que www-data.
Votre utilisateur de déploiement est typiquement deploy ou un runner CI. L’erreur consiste à essayer de faire en sorte qu’un
seul ensemble de permissions satisfasse à la fois « deploy écrit tout » et « le serveur web lit tout » en transformant tout l’arbre en toilettes publiques.
Voici le modèle sensé que je recommande pour la plupart des équipes :
Modèle A : web root en lecture seule, sous-répertoires écrits explicitement
- Web root (
/var/www/site) est possédé par un propriétaire de déploiement (ou root), lisible parwww-data, non inscriptible par lui. - Répertoires inscriptibles sont découpés :
var/,storage/,uploads/, répertoires de cache, sockets si nécessaire. - Les inscriptibles appartiennent à www-data (ou ont une ACL donnant l’écriture à www-data), et rien d’autre ne l’est.
Cela vous donne des propriétés de sécurité prévisibles : une compromission du serveur web ne donne pas automatiquement la capacité
de modifier le code applicatif. Il peut toujours écrire les uploads parce que vous l’avez décidé. Pas parce que vous avez paniqué.
Modèle B : groupe partagé pour les déploiements (lorsque vous devez autoriser les écritures)
- Créez un groupe (par ex.
web) qui inclut l’utilisateur deploy etwww-data. - Définissez la propriété de groupe du web root sur
web. - Utilisez
chmod 2775(setgid) sur les répertoires pour que les nouveaux fichiers héritent du groupe. - Définissez un
umaskraisonnable dans votre processus de déploiement pour que les fichiers soient lisibles par le groupe (et modifiables par le groupe si requis).
Le modèle B est plus flexible, mais il a des arêtes vives : un mauvais umask et vous avez une loterie de permissions.
Si vous allez dans cette voie, faites-le avec discipline et contrôles.
Exactement une citation, parce que le reste devrait être des logs : « L’espoir n’est pas une stratégie. »
— Gene Kranz
Blague n°1 : chmod 777 c’est comme réparer une fenêtre cassée en supprimant tout le mur. Techniquement, il n’y a plus de verre à briser.
Faits & contexte historique (pour que les aspects étranges aient du sens)
- Le bit d’exécution sur les répertoires signifiait à l’origine « recherche » : vous pouvez traverser le répertoire et accéder aux inodes si vous connaissez déjà les noms.
- 403 vs 404 est souvent un choix : de nombreux serveurs renvoient volontairement 404 sur des problèmes de permission pour éviter de révéler l’existence de contenu.
- www-data comme convention est devenu courant sur les systèmes dérivés de Debian parce que les paquets avaient besoin d’un utilisateur non-login prévisible pour les démons.
- La position historique d’Apache sur les symlinks est conservatrice : suivre les symlinks et autoriser les overrides a été une source récurrente d’incidents de sécurité.
- Nginx a popularisé les patterns « try_files » qui peuvent changer le chemin réellement accédé, perturbant le dépannage des permissions si vous inspectez le mauvais fichier.
- Les ACL POSIX ont été introduites pour exprimer des permissions au-delà propriétaire/groupe/autre, largement motivées par NFS d’entreprise et des systèmes multi-utilisateurs où les bits Unix étaient trop grossiers.
- Le setgid sur les répertoires est ancien mais toujours pertinent : c’est l’une des rares façons portables de forcer l’héritage de groupe sans dépendre des ACL par défaut.
- Les valeurs par défaut de umask viennent des temps du timesharing multi-utilisateur : avoir par défaut écriture groupe était considéré risqué même quand les gens étaient plus gentils.
Tâches pratiques : commandes, sorties, décisions
Ce sont des tâches « exécutez ça maintenant ». Chacune inclut ce que la sortie signifie et la décision que vous devez prendre.
Utilisez-les dans l’ordre quand vous êtes sous pression, puis à nouveau plus tard quand vous réparez le système correctement.
Task 1: confirm the service user and active worker identity
cr0x@server:~$ ps -o user,group,pid,cmd -C nginx | head
USER GROUP PID CMD
root root 1021 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data www-data 1022 nginx: worker process
www-data www-data 1023 nginx: worker process
Signification : Les workers s’exécutent en tant que www-data. C’est l’identité qui a besoin d’accès au système de fichiers.
Décision : Tous les tests « peut-il lire ceci ? » doivent être exécutés en tant que www-data.
Task 2: get the on-disk path from the web server config
cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '/server_name example.com/,+40p' | sed -n '1,40p'
server {
listen 80;
server_name example.com;
root /var/www/example/current/public;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
Signification : Le root effectif est /var/www/example/current/public, pas le checkout du repo sur lequel vous regardiez.
Décision : Dépannez les permissions sur ce chemin et ses parents, y compris la cible du symlink current.
Task 3: check the error log for the actual denial wording
cr0x@server:~$ sudo tail -n 8 /var/log/nginx/error.log
2025/12/30 10:12:14 [error] 1022#1022: *884 open() "/var/www/example/current/public/index.html" failed (13: Permission denied), client: 203.0.113.10, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
Signification : L’erreur 13 est un refus de permission au niveau du noyau. Ce n’est pas une règle nginx « deny all ».
Décision : Concentrez-vous sur les permissions du système de fichiers, la propriété, les ACL et la traversée du chemin. Gardez les débats de config pour plus tard.
Task 4: walk the full path permissions (the boring part that fixes it)
cr0x@server:~$ namei -l /var/www/example/current/public/index.html
f: /var/www/example/current/public/index.html
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-x--- deploy web example
lrwxrwxrwx deploy web current -> releases/20251230T1005
drwxr-s--- deploy web releases
drwxr-s--- deploy web 20251230T1005
drwxr-s--- deploy web public
-rw-r----- deploy web index.html
Signification : /var/www/example est drwxr-x---. « Other » n’a pas le bit d’exécution, et www-data n’est ni propriétaire ni dans le groupe web (pas encore).
Décision : Soit ajoutez www-data au groupe web (Modèle B), soit changez les droits de traversée afin que www-data puisse traverser mais pas écrire (Modèle A).
Task 5: verify group membership (and remember to restart workers if needed)
cr0x@server:~$ id www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Signification : www-data n’est pas dans web. L’accès basé sur le groupe ne fonctionnera pas tant que vous ne l’y aurez pas ajouté.
Décision : Si vous choisissez le modèle groupe-partagé, ajoutez l’appartenance et rechargez le service pour que les groupes soient pris en compte.
Task 6: test access as the web user (no guessing)
cr0x@server:~$ sudo -u www-data test -r /var/www/example/current/public/index.html; echo $?
1
Signification : Le code de sortie 1 signifie « non lisible ». Cela correspond au log.
Décision : Corrigez les permissions sur les répertoires/fichiers pour que le test retourne 0.
Task 7: check directory execute bit specifically (common path trap)
cr0x@server:~$ sudo -u www-data ls -ld /var/www/example /var/www/example/current /var/www/example/current/public
ls: cannot access '/var/www/example/current': Permission denied
drwxr-x--- 5 deploy web 4096 Dec 30 10:05 /var/www/example
Signification : Le refus se produit à la traversée de /var/www/example. Même si les fichiers étaient lisibles par tous, vous ne pouvez pas les atteindre.
Décision : Accordez la traversée (exécution) à www-data via l’appartenance au groupe, une ACL, ou le bit « other » si acceptable.
Task 8: implement Model B safely (shared group + setgid)
cr0x@server:~$ sudo groupadd -f web
cr0x@server:~$ sudo usermod -aG web www-data
cr0x@server:~$ sudo usermod -aG web deploy
cr0x@server:~$ sudo chgrp -R web /var/www/example
cr0x@server:~$ sudo find /var/www/example -type d -exec chmod 2775 {} \;
cr0x@server:~$ sudo find /var/www/example -type f -exec chmod 0644 {} \;
Signification : Les répertoires deviennent héritiers du groupe (2 dans 2775), lisibles/traversables par le groupe. Les fichiers deviennent lisibles par le groupe.
Décision : Rechargez nginx pour que les workers prennent en compte le nouveau groupe ; puis retestez l’accès en tant que www-data.
Task 9: reload the service to pick up new groups (don’t skip this)
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ ps -o pid,user,group,cmd -C nginx | head -n 4
PID USER GROUP CMD
1021 root root nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
2110 www-data www-data nginx: worker process
2111 www-data www-data nginx: worker process
Signification : Le groupe principal reste www-data (correct). Les groupes supplémentaires ne sont pas affichés ici.
Décision : Vérifiez les groupes supplémentaires en retestant l’accès ; si cela échoue encore, le worker peut ne pas avoir le nouveau groupe (privilégiez le redémarrage plutôt que le reload).
Task 10: re-test access as www-data after the change
cr0x@server:~$ sudo -u www-data test -r /var/www/example/current/public/index.html; echo $?
0
Signification : L’accès en lecture fonctionne maintenant.
Décision : Confirmez que la réponse HTTP est 200 et que vous n’avez pas accidentellement trop élargi les zones inscriptibles.
Task 11: inspect umask that may sabotage future deploys
cr0x@server:~$ sudo -u deploy sh -lc 'umask; touch /var/www/example/current/public/.umask-test && stat -c "%a %U:%G %n" /var/www/example/current/public/.umask-test'
0022
644 deploy:web /var/www/example/current/public/.umask-test
Signification : Avec umask 0022, les nouveaux fichiers sont 0644. C’est correct pour des web roots en lecture seule. Si vous aviez besoin d’écritures de groupe, cela le casserait.
Décision : Préférez des roots en lecture seule. Si vous avez vraiment besoin d’écritures de groupe, réglez umask à 0002 dans l’environnement de déploiement et prouvez que cela reste ainsi.
Task 12: identify writable directories (and lock everything else down)
cr0x@server:~$ sudo find /var/www/example/current -maxdepth 3 -type d -name storage -o -name uploads -o -name cache
/var/www/example/current/storage
/var/www/example/current/public/uploads
/var/www/example/current/var/cache
Signification : Ce sont les chemins typiques où l’app a besoin d’écrire.
Décision : Rendez uniquement ceux-ci inscriptibles par www-data. Tout le reste doit être en lecture seule pour le serveur web.
Task 13: set explicit ownership for writable dirs (Model A hybrid)
cr0x@server:~$ sudo chown -R www-data:www-data /var/www/example/current/storage /var/www/example/current/public/uploads /var/www/example/current/var/cache
cr0x@server:~$ sudo find /var/www/example/current/storage /var/www/example/current/public/uploads /var/www/example/current/var/cache -type d -exec chmod 0750 {} \;
cr0x@server:~$ sudo find /var/www/example/current/storage /var/www/example/current/public/uploads /var/www/example/current/var/cache -type f -exec chmod 0640 {} \;
Signification : L’application peut écrire où elle doit, mais ne peut pas modifier le code ailleurs.
Décision : Si votre utilisateur de déploiement doit gérer ces répertoires aussi, utilisez des ACL plutôt que d’assouplir les modes.
Task 14: verify no accidental world-writable permissions exist
cr0x@server:~$ sudo find /var/www/example -xdev -perm -0002 -type f -o -perm -0002 -type d | head
Signification : Aucune sortie est bonne. Une sortie signifie qu’un élément est world-writable, ce qui est généralement un incident en attente.
Décision : Si une sortie existe, corrigez la propriété/les ACL et retirez le write pour « other ». Ne négociez pas avec ça.
Task 15: diagnose AppArmor denials on Ubuntu (when modes look fine)
cr0x@server:~$ sudo journalctl -k -g DENIED -n 5
Dec 30 10:14:02 server kernel: [12345.678901] audit: type=1400 apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/srv/apps/example/current/public/index.html" pid=2110 comm="nginx" requested_mask="r" denied_mask="r" fsuid=33 ouid=1001
Signification : Le noyau a refusé la lecture à cause d’une politique AppArmor. Les bits du système de fichiers ne sont pas le problème.
Décision : Ajustez le profil AppArmor (ou déplacez le web root vers un chemin autorisé). Ne continuez pas à faire des chmod ; cela n’aidera pas.
Task 16: catch symlink surprises (Apache especially)
cr0x@server:~$ readlink -f /var/www/example/current/public
/var/www/example/releases/20251230T1005/public
Signification : Vous servez depuis un répertoire de releases. Les permissions doivent être cohérentes sur chaque nouveau chemin de release.
Décision : Intégrez la normalisation des permissions dans l’étape de déploiement (ou définissez des ACL par défaut sur les répertoires parents) pour que chaque release soit correcte.
Blague n°2 : Les permissions Unix sont simples — jusqu’à ce que vous ayez besoin qu’elles soient correctes.
Nginx vs Apache : défauts différents, même réalité du système de fichiers
Modes d’échec nginx qui ressemblent à des problèmes de permissions
Nginx est généralement franc au sujet des refus de système de fichiers : vous verrez « (13: Permission denied) » dans le journal d’erreurs.
Mais la configuration peut encore créer des symptômes « à la permission » :
- try_files change la cible : vous demandez
/mais nginx essaie/index.php. Les permissions doivent fonctionner aussi sur le fallback. - autoindex off + index manquant : vous pouvez obtenir 403 si l’index est interdit et qu’aucun index n’existe.
- confusion alias vs root : une mauvaise concaténation de chemin peut vous pointer vers un endroit que vous n’avez pas permissionné.
Modes d’échec Apache qui ressemblent à des problèmes de permissions
Apache a sa propre marque de « 403 à cause d’une politique », même quand le système de fichiers est correct :
- Directives Require : un strict
Require all deniedhérité d’un contexte parent refusera tout sans vergogne. - Options FollowSymLinks : les symlinks peuvent être rejetés, produisant un 403 alors que la cible est lisible.
- Overrides .htaccess : l’absence de
AllowOverrideou des règles inattendues peuvent refuser des requêtes de façon à imiter des problèmes d’accès aux fichiers.
L’astuce est d’arrêter de traiter « 403 » comme un seul problème. C’est une sortie. L’entrée est dans les logs.
ACL : l’alternative adulte au « tout écrire au groupe »
Les bits de permission Unix sont une autoroute à trois voies : owner, group, other. Les ACL vous donnent des rues secondaires :
« www-data peut lire cet arbre, deploy peut écrire, CI peut lire, et personne d’autre n’y touche. » C’est exactement ce dont les web roots ont souvent besoin.
Quand les ACL valent le coup
- Vous avez plusieurs identités de déploiement (utilisateur humain, runner CI, automatisation) et vous ne voulez pas d’un groupe partagé massif.
- Vous voulez que
www-dataait la traversée et la lecture, mais jamais l’écriture, même si la propriété de groupe change. - Vous voulez que les nouveaux fichiers héritent automatiquement des bonnes permissions sans dépendre de la discipline de umask.
Deux règles ACL qui font la plupart du travail
Définissez une ACL d’accès (ce qui s’applique maintenant) et une ACL par défaut (ce que les nouveaux fichiers héritent) sur les répertoires.
Exemple : autoriser www-data à lire/traverser le web root, et autoriser deploy à écrire, sans rendre world-readable.
cr0x@server:~$ sudo setfacl -R -m u:www-data:rx /var/www/example
cr0x@server:~$ sudo setfacl -R -m d:u:www-data:rx /var/www/example
cr0x@server:~$ getfacl -p /var/www/example | sed -n '1,20p'
# file: /var/www/example
# owner: deploy
# group: web
user::rwx
user:www-data:r-x
group::r-x
mask::r-x
other::---
default:user::rwx
default:user:www-data:r-x
default:group::r-x
default:mask::r-x
default:other::---
Signification : www-data obtient lecture+traversée. Les nouveaux répertoires/fichiers héritent d’une entrée ACL par défaut, donc les nouvelles releases ne feront pas aléatoirement 403.
Décision : Utilisez les ACL quand vous avez besoin d’un accès multi-identités stable sans assouplir les permissions « other ».
Le piège : le mask ACL
Les ACL viennent avec une entrée mask qui limite les permissions effectives pour les utilisateurs/groupes nommés. Les gens oublient que le mask existe,
puis se demandent pourquoi leur rwx soigneusement défini ne s’applique pas réellement.
cr0x@server:~$ sudo setfacl -m u:www-data:rwx /var/www/example/current/storage
cr0x@server:~$ getfacl -p /var/www/example/current/storage | grep -E 'www-data|mask'
user:www-data:rwx
mask::rwx
Signification : Le mask est permissif. S’il avait été r-x, les permissions effectives auraient été réduites.
Décision : Quand les ACL « ne fonctionnent pas », inspectez le mask avant de réécrire la moitié de votre politique de fichiers.
Déploiements : empêcher CI/CD de re-casser les permissions
Les permissions ne sont pas quelque chose que vous réparez une fois. Ce sont des choses que votre déploiement peut ruiner à répétition,
avec l’enthousiasme d’un bambin dans une pièce pleine de marqueurs.
Où provient la dérive des permissions
- Nouveaux répertoires de release créés avec un umask ou un contexte utilisateur différent.
- Extraction d’artefacts préservant des modes depuis le build qui ne correspondent pas aux besoins de production.
- Builds de conteneurs copiant des fichiers en tant que root et les laissant root-owned sur des volumes partagés.
- Hotfixes faits sur le serveur en root à 2h du matin (vous vous reconnaîtrez).
Normaliser les permissions comme étape de déploiement
C’est ennuyeux et correct. Ajoutez une étape à votre pipeline de déploiement qui impose la propriété/les modes/les ACL
sur le répertoire de release avant de basculer le symlink current.
cr0x@server:~$ sudo -u deploy sh -lc '
release=/var/www/example/releases/20251230T1005
find "$release" -type d -exec chmod 2755 {} \;
find "$release" -type f -exec chmod 0644 {} \;
'
Signification : Les répertoires sont traversables ; les fichiers sont lisibles. Le bit setgid garde l’héritage de groupe stable.
Décision : Utilisez la normalisation pour enlever « marche sur mon agent de build » de l’équation des permissions.
Empêcher que des artefacts root-owned arrivent dans le web root
Si vous déployez avec sudo, soyez explicite sur le contexte utilisateur lors de l’écriture des fichiers.
« Je l’ai lancé avec sudo » est la façon dont vous vous retrouvez avec un web root que seul root peut lire.
cr0x@server:~$ sudo -u deploy tar -xf /tmp/release.tar -C /var/www/example/releases/20251230T1005
cr0x@server:~$ stat -c "%U:%G %a %n" /var/www/example/releases/20251230T1005/public/index.html
deploy:web 644 /var/www/example/releases/20251230T1005/public/index.html
Signification : Les fichiers sont possédés par l’identité de déploiement, pas root. C’est ce que vous voulez.
Décision : Faites de « qui écrit les fichiers » un paramètre de déploiement de première classe, pas un accident.
Trois mini-histoires d’entreprise issues des tranchées des permissions
1) Incident causé par une mauvaise hypothèse : « Fichier lisible = site lisible »
Une entreprise de taille moyenne exploitait une flotte Debian avec nginx devant un générateur de site statique. Un ingénieur
a fait tourner l’utilisateur de déploiement et a renforcé les permissions sur /var/www de façon globale, visant le principe du moindre privilège.
Ils ont vérifié quelques fichiers. Tout était 0644. Ils ont poussé.
Cinq minutes plus tard, le site renvoyait 403. L’astreinte a d’abord blâmé une dérive de config nginx parce que
le fichier root était clairement lisible par tous. Le lead sur le canal d’incident a dit « ça ne peut pas être les permissions,
le fichier est 644. »
Le problème réel était un niveau au-dessus : le répertoire était devenu 750 appartenant à l’utilisateur de déploiement et à un groupe privé.
Nginx ne pouvait pas y pénétrer parce que « exécution sur les répertoires » n’était pas dans leur modèle mental.
Le journal d’erreurs disait bien « permission denied », mais les gens lisaient ce qu’ils s’attendaient à lire.
La correction a été petite — accorder la traversée via l’appartenance au groupe et les répertoires setgid — mais la leçon est restée :
les vérifications de permissions doivent inclure le chemin entier. L’action post-incident n’a pas été « faites attention ».
C’était « ajoutez namei dans le runbook et testez en tant que www-data ».
2) Optimisation qui s’est retournée contre eux : « Faisons en sorte que le serveur web écrive la sortie du build »
Une autre équipe voulait accélérer les déploiements pour une appli PHP. Quelqu’un a proposé de builder les assets sur le serveur
lui-même pour éviter de déplacer de gros artefacts. Le moyen le plus rapide était d’exécuter le build sous la même identité qui sert l’appli.
« Si le build tourne en tant que www-data, il aura toujours l’accès. »
Ça a marché. Le temps de déploiement s’est amélioré. Les graphiques sont devenus plus jolis. Puis une dépendance a été compromise
(pas de leur faute), et leur processus applicatif avait des permissions d’écriture sur l’arbre de code. Cela a transformé une compromission
en lecture seule en une compromission qui s’auto-modifie : les requêtes web pouvaient déposer des fichiers dans des répertoires servis.
L’équipe IR l’a contenu rapidement, mais ce fut une longue semaine de vérification de ce qui avait changé sur le disque.
L’« optimisation » a effacé une frontière de sécurité. Une frontière qui était gratuite.
Ils sont revenus à des builds hors production, ont rendu le web root en lecture seule pour l’identité de service, et
ont explicitement autorisé les écritures seulement aux chemins upload/cache. Le temps de déploiement a légèrement augmenté ; le risque
a fortement diminué. C’est le compromis qu’on fait chaque fois.
3) Pratique ennuyeuse mais correcte qui a sauvé la mise : ACL par défaut sur les parents de releases
Une équipe d’entreprise gérait plusieurs sites sous /srv/apps avec une topologie de releases à la Capistrano. Leur CI créait
un nouveau répertoire de release à chaque déploiement. Avec le temps, ils avaient un problème récurrent « parfois 403 après un déploiement ».
Pas tout le temps. Juste assez pour être irritant et entamer la confiance dans l’automatisation.
La cause racine était la dérive de umask entre les runners. Un runner créait des répertoires en 0750, un autre en 0755.
Parfois Apache pouvait traverser ; parfois non. Les ingénieurs ont essayé de « standardiser » en ajoutant une étape chmod,
mais elle s’exécutait après le flip du symlink. Les utilisateurs voyaient une brève panne pendant le déploiement. Ils ont détesté ça.
La correction n’était pas brillante. Elle était correcte. Ils ont défini une ACL par défaut sur le répertoire parent des releases en donnant
rx à l’utilisateur du serveur web, et ils ont imposé une normalisation des permissions avant le switch. Les nouveaux arbres de release
héritaienet d’un accès sensé, donc le déploiement ne dépendait plus de l’humeur du runner.
Rien de tout cela n’était excitant. Ça a aussi fait disparaître la page « 403 après déploiement ». Parfois l’ennuyeux est une fonctionnalité.
Erreurs courantes : symptôme → cause racine → correctif
1) Symptom: 403 for a directory, but files work when requested directly
Cause racine : Fichier index manquant, listing de répertoire désactivé, ou front controller du framework inaccessible.
Correctif : Assurez-vous qu’un index existe (index.html / index.php), vérifiez la directive index de nginx, et vérifiez les permissions sur la cible de fallback dans try_files.
2) Symptom: 403 with “(13: Permission denied)” in nginx/apache logs
Cause racine : Le système de fichiers refuse l’accès. Souvent un répertoire parent manque l’exécution (traversée) pour www-data.
Correctif : Utilisez namei -l sur le chemin complet ; accordez la traversée via groupe, ACL ou ajustement du mode du répertoire.
3) Symptom: Everything is 644/755, still 403
Cause racine : Refus AppArmor sur Ubuntu, ou politique Apache refusant l’accès.
Correctif : Vérifiez les logs du noyau/audit pour AppArmor DENIED ; vérifiez les directives Require et Options d’Apache. Ne continuez pas à faire des chmod.
4) Symptom: Works until a deploy, then 403
Cause racine : Nouveau répertoire de release créé avec umask restrictif ou mauvaise propriété ; le symlink pointe vers un arbre aux permissions différentes.
Correctif : Normalisez les permissions avant de changer le symlink ; appliquez des ACL par défaut au parent des releases ; imposez le contexte utilisateur de déploiement.
5) Symptom: Uploads fail unless you chmod 777 the whole web root
Cause racine : L’application a besoin d’écrire seulement pour uploads/cache, mais vous avez ciblé tout l’arbre.
Correctif : Rendez uniquement certains répertoires inscriptibles par www-data ou accordez l’écriture via ACL là-bas. Gardez le code en lecture seule.
6) Symptom: “Permission denied” when using symlinked paths
Cause racine : Un des composants du symlink ou son chemin cible a une traversée restreinte ; Apache peut aussi exiger des options explicites pour les symlinks.
Correctif : Utilisez readlink -f et namei -l sur le chemin résolu ; ajustez les permissions de traversée ; pour Apache, autorisez les symlinks correctement.
7) Symptom: Deploy user can write, web server can’t read (or vice versa)
Cause racine : Confiance excessive dans les bits propriétaire ; stratégie de groupe pas mise en œuvre ; setgid manquant ; ACL absentes.
Correctif : Choisissez un modèle (root en lecture seule + répertoires inscriptibles explicites, ou groupe partagé + setgid, ou ACL) et implémentez-le de manière cohérente.
Listes de contrôle / plan pas à pas
Checklist: stop the immediate 403 safely
- Vérifiez le journal d’erreurs pour le type de refus (système de fichiers vs config).
- Confirmez le chemin root configuré sur disque (nginx
root/alias, ApacheDocumentRoot). - Exécutez
namei -lsur le chemin exact du fichier qui a échoué. - Testez en tant que
www-dataavecsudo -u www-data test -r(ou-xpour les répertoires). - Si c’est un problème de traversée d’un répertoire parent, corrigez le répertoire le plus étroit nécessaire (pas tout l’arbre).
- Rechargez/redémarrez le serveur web si des changements d’appartenance aux groupes ont eu lieu.
- Retestez : test en ligne de commande de lecture, puis requête HTTP.
- Scannez les permissions world-writable involontaires après le correctif d’urgence.
Checklist: implement a long-term permission policy
- Choisissez un modèle :
- Préféré : web root en lecture seule ; sous-répertoires inscriptibles uniquement.
- Acceptable : groupe partagé + setgid + umask imposé.
- Meilleur pour les organisations complexes : ACL avec entrées par défaut.
- Définissez explicitement les chemins inscriptibles (uploads/cache/sessions/logs/sockets).
- Attribuez la propriété pour les chemins inscriptibles (souvent
www-data:www-data), gardez le code possédé par deploy/root. - Normalisez les permissions lors du déploiement avant le changement de symlink/basculement de trafic.
- Ajoutez un test CI ou un contrôle post-déploiement qui exécute des tests de lecture en tant que
www-data. - Documentez une commande de runbook :
namei -lsur le chemin en échec. - Incluez des vérifications AppArmor dans les runbooks Ubuntu si applicable.
Step-by-step plan: a clean baseline for a typical site
Voici une base pratique qui garde le code en lecture seule pour le serveur web tout en autorisant uploads et cache.
Ajustez les chemins pour votre application.
- Créez un groupe partagé pour deploy + serveur web (optionnel mais utile) :
cr0x@server:~$ sudo groupadd -f web cr0x@server:~$ sudo usermod -aG web deploy cr0x@server:~$ sudo usermod -aG web www-data - Définissez la propriété de groupe et setgid sur l’arbre du site :
cr0x@server:~$ sudo chgrp -R web /var/www/example cr0x@server:~$ sudo find /var/www/example -type d -exec chmod 2755 {} \; cr0x@server:~$ sudo find /var/www/example -type f -exec chmod 0644 {} \; - Marquez les répertoires inscriptibles et accordez l’écriture seulement là :
cr0x@server:~$ sudo install -d -o www-data -g www-data -m 0750 /var/www/example/current/public/uploads cr0x@server:~$ sudo install -d -o www-data -g www-data -m 0750 /var/www/example/current/var/cache - Redémarrez si l’appartenance aux groupes a été changée (redémarrer est plus sûr que reload) :
cr0x@server:~$ sudo systemctl restart nginx - Validez avec un test de lecture en tant que www-data :
cr0x@server:~$ sudo -u www-data test -r /var/www/example/current/public/index.html; echo $? 0
FAQ
1) Why does a missing execute bit on a directory cause 403 even if the file is 644?
Parce que « exécution » sur un répertoire signifie « traverser/rechercher ». Sans elle, vous ne pouvez pas accéder aux entrées à l’intérieur,
même si vous connaissez le nom du fichier. Le noyau bloque la traversée avant d’évaluer les bits du fichier.
2) Should my web root be owned by www-data?
Généralement non. Si www-data possède le code, une compromission web devient souvent persistante.
Préférez le code possédé par deploy/root et lisible par www-data ; rendez inscriptibles uniquement les répertoires runtime nécessaires.
3) What permissions should a typical static web root have?
Baseline commun : répertoires 0755, fichiers 0644, possédés par un utilisateur de déploiement, lisibles par le serveur web via groupe partagé ou ACL.
Serrez le « other » si vous en avez une raison, mais conservez la traversée pour le serveur web.
4) Is using a shared group (deploy + www-data) insecure?
Cela peut être acceptable si vous gardez l’accès du serveur web en lecture seule et restreignez les écritures à des répertoires spécifiques.
Cela devient risqué quand l’écriture de groupe est activée largement et que le processus web peut écrire dans des chemins de code.
5) Why did adding www-data to a group not fix the issue until restart?
Les processus ne mettent pas à jour magiquement leurs groupes supplémentaires en cours d’exécution. Les workers gardent la liste de groupes du moment où ils ont démarré.
Le reload peut ne pas remplacer les workers d’une manière qui intègre les nouveaux groupes ; le redémarrage est déterministe.
6) When should I use ACLs instead of chmod/chgrp?
Utilisez les ACL quand vous avez plusieurs identités nécessitant des accès différents, ou quand vous voulez des permissions héritées
sans dépendre du umask et de la correction du setgid. Les ACL sont aussi utiles quand vous voulez éviter d’ouvrir les permissions « other ».
7) Why do I get 403 only on some files after deploy?
En général des modes de création incohérents : umask différent, utilisateur différent, ou artefacts extraits préservant des modes restrictifs.
La solution est la normalisation des permissions dans le processus de déploiement et/ou des ACL par défaut sur le parent des releases.
8) What’s the safest quick fix under pressure?
Accordez la traversée/lecture minimale nécessaire à l’identité du serveur web. Commencez par le premier répertoire parent en échec montré par
namei -l. Évitez chmod -R sur tout l’arbre ; c’est comme créer de nouveaux problèmes en réparant l’ancien.
9) Why do I see 403 but logs show nothing?
Vous regardez peut-être le mauvais journal (vhost erroné, mauvais conteneur), ou votre niveau de log est trop bas.
Vérifiez aussi que la requête atteint bien le serveur que vous taillez (les load balancers aiment faire des blagues pratiques).
10) Does Debian/Ubuntu use SELinux here?
Par défaut, non — AppArmor est plus courant sur Ubuntu. Si vous avez installé SELinux, alors il est en jeu, mais ne supposez pas sa présence.
Vérifiez avec les logs et les modules activés au lieu de reproduire mécaniquement des correctifs d’une autre distribution.
Conclusion : prochaines étapes pratiques
Cessez de traiter les 403 comme un défaut moral. Ce sont des désaccords de modèle de permissions, et Linux est extrêmement cohérent une fois que vous posez la bonne question.
La bonne question est « est-ce que www-data peut traverser chaque répertoire du chemin et lire la cible ? » et non « est-ce que j’ai fait un chmod récemment ? »
Prochaines étapes que vous pouvez faire aujourd’hui :
- Ajoutez
namei -let « tester en tant que www-data » à votre runbook d’astreinte. - Choisissez un des modèles de permissions et implémentez-le de manière cohérente : root en lecture seule avec répertoires inscriptibles explicites est le choix adulte par défaut.
- Normalisez les permissions dans les déploiements, avant de basculer le trafic ou les symlinks.
- Si vous utilisez Ubuntu, incluez des vérifications des refus AppArmor pour ne pas gaspiller une heure à faire des chmod dans le vide.
Gardez votre web root lisible, vos répertoires inscriptibles intentionnels, et votre doigt loin de la touche 7 sauf si vous écrivez une histoire cautionnaire.