$ curl -s -o /dev/null -w "%{http_code}\n" -F "photo=@chat.jpg" https://site.lan/upload.php
500

$ sudo tail -1 /var/log/nginx/error.log
... FastCGI sent in stderr: "PHP message: PHP Warning: move_uploaded_file(/var/www/site/uploads/chat.jpg):
Failed to open stream: Permission denied in /var/www/site/upload.php on line 12" ...

$ sudo chmod -R 777 /var/www/site

$ curl -s -o /dev/null -w "%{http_code}\n" -F "photo=@chat.jpg" https://site.lan/upload.php
200

Ça marche. Et c’est exactement ça, le problème.

L’erreur a disparu en une commande, et la sécurité avec : n’importe quel compte de la machine peut désormais lire, modifier et exécuter tout le site.

Ce premier article pose les bases et donne la vraie réponse au Permission denied. Les bits spéciaux et les ACL viendront dans une partie 2.

L’essentiel en 30 secondes

  • Chaque fichier a un propriétaire, un groupe, et des droits pour trois classes : propriétaire, groupe, autres.
  • r = 4, w = 2, x = 1. On additionne : 7 = rwx, 5 = r-x, 6 = rw-.
  • Sur un répertoire, x veut dire « traverser » et w veut dire « créer et supprimer des fichiers dedans ».
  • Le noyau applique une seule classe : la première qui correspond.
  • Face à un Permission denied, on corrige le propriétaire ou le groupe. On n’ouvre pas tout.

Lire un ls -l

$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68248 mars 23 2025 /usr/bin/passwd

Anatomie d’une ligne ls -l : type, droits propriétaire, groupe, autres

Un caractère de type (- fichier, d répertoire, l lien), puis trois blocs de trois lettres : propriétaire, groupe, autres. Ensuite viennent le propriétaire et le groupe du fichier. Le s de passwd est un bit spécial, on le garde pour la partie 2.

r, w, x : pas le même sens sur un répertoire

C’est la source de la plupart des incompréhensions. Un répertoire n’est qu’une liste de noms, et ses droits s’appliquent à cette liste.

Droit Sur un fichier Sur un répertoire
r lire lister les noms
w modifier créer, renommer, supprimer des entrées
x exécuter traverser (cd, ouvrir un fichier dedans)

Deux conséquences à retenir :

  • Le w du dossier suffit pour supprimer un fichier, même en lecture seule et appartenant à quelqu’un d’autre.
  • Il faut x sur chaque répertoire du chemin. Un seul parent en 700 et c’est Permission denied, même si le fichier est en 644.

Ce second point provoque une bonne partie des 777 de panique. namei -l affiche toute la chaîne d’un coup :

$ namei -l /var/www/site/index.html
drwxr-xr-x root     root     /
drwxr-xr-x root     root     var
drwxr-xr-x root     root     www
drwx------ jemadhar jemadhar site        ← le coupable
-rw-r--r-- jemadhar jemadhar index.html

L’octal en une table

Trois chiffres, dans l’ordre propriétaire / groupe / autres. Les seuls à connaître par cœur :

Mode Droits Pour quoi
755 rwxr-xr-x dossiers publics, scripts
644 rw-r--r-- fichiers de contenu
750 / 640 rwxr-x--- / rw-r----- dossiers et fichiers d’un service
700 / 600 rwx------ / rw------- ~/.ssh, clés privées

Pour corriger une arborescence entière, le X majuscule ajoute x aux répertoires seulement, pas aux fichiers :

chmod -R u=rwX,g=rX,o= /srv/appli    # dossiers en 750, fichiers en 640

Le noyau prend le premier qui correspond

Sous Windows, les droits s’additionnent. Sous Linux, non : le noyau teste les classes dans l’ordre et s’arrête à la première qui correspond.

Algorithme de décision des droits Unix : root, puis propriétaire, puis groupe, puis autres

D’où ce cas étonnant :

$ ls -l rapport.txt
----rw-r-- 1 jemadhar equipe 512 oct. 6 10:00 rapport.txt
$ cat rapport.txt
cat: rapport.txt: Permission non accordée

Je suis le propriétaire, donc seul le premier bloc --- s’applique à moi. Tout le monde peut lire ce fichier, sauf moi.

Pourquoi 777 est vraiment la bête

« Sur mon infra perso, qui va exploiter ça ? » Trois scénarios très concrets :

  • Le script cron. Un sauvegarde.sh en 777, lancé par root. N’importe quel compte local y ajoute une ligne, qui tournera en root à la prochaine exécution.
  • Le dossier d’upload. Un site en 777, un formulaire mal filtré : un .php déposé est aussitôt exécutable. C’est le webshell le plus banal qui soit.
  • Le partage. Un dossier commun en 777 : chacun peut effacer les fichiers des autres, puisque supprimer, c’est écrire dans le dossier.

777 ne résout pas le problème : il le fait disparaître en même temps que la sécurité. Le vrai problème, c’était presque toujours un mauvais propriétaire.

La vraie réponse : qui est le processus, à qui est le fichier ?

Trois commandes pour poser le diagnostic :

ps -o user,cmd -C nginx              # sous quel compte tourne le service ?
id www-data                          # dans quels groupes est ce compte ?
stat -c '%U:%G %a' /var/www/site     # à qui appartient le dossier ?

Puis on corrige l’appartenance, pas les bits. Exemple complet pour un site web : le code appartient au compte de déploiement, le serveur web le lit via son groupe, et il n’écrit que dans uploads/.

chown -R deploy:www-data /var/www/site
chmod -R u=rwX,g=rX,o= /var/www/site       # lecture seule pour le serveur web
chmod 770 /var/www/site/uploads            # écriture uniquement ici
sudo -u www-data touch /var/www/site/uploads/test && echo OK

Si le site est compromis, l’attaquant ne peut pas réécrire le code. Côté serveur web, on interdit en plus l’exécution de PHP dans uploads/.

Un piège fréquent : après un usermod -aG www-data deploy, le nouveau groupe n’est actif qu’à la prochaine connexion ou au redémarrage du service. Sans ça, on croit que rien ne marche et on finit en 777.

umask : les droits à la naissance

Un programme qui crée un fichier demande 666, ou 777 pour un dossier. La bête est là dès la naissance. Le noyau retire ensuite les bits de l’umask :

666 - 022 = 644 (fichier)      777 - 022 = 755 (dossier)

L’umask par défaut, 022, laisse tout le monde lire ce que vous créez. L’ANSSI recommande 0077 pour les shells utilisateurs et 0027 pour les services (directive UMask=0027 dans l’unité systemd).

Les erreurs classiques

Erreur À faire à la place
chmod -R 777 pour débloquer une appli corriger propriétaire et groupe
chmod -R 755 sur tout un site chmod -R u=rwX,g=rX,o=rX
Permission denied avec un fichier en 644 namei -l : un parent bloque la traversée
usermod -aG sans se reconnecter nouvelle session ou redémarrage du service
Clé SSH privée en 644 chmod 600, et 700 sur ~/.ssh

Ce que je retiens

  • Un Permission denied est une question d’identité avant d’être une question de bits.
  • Sur un dossier, w permet de supprimer et x de traverser. Tout le chemin compte.
  • Pas d’addition : le noyau applique la première classe qui correspond.
  • chown pour donner, X majuscule pour corriger en masse, umask pour partir sur de bonnes bases.
  • 777 fait disparaître l’erreur, et la sécurité avec.

Dans la partie 2 : le s de passwd, le t de /tmp, le dossier partagé propre avec setgid, et les ACL quand un propriétaire et un groupe ne suffisent plus.

Sources et références