$ 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

It works. And that’s exactly the problem.

The error disappeared with a single command—and so did security: any account on the machine can now read, modify, and execute the entire site.

This first article lays the groundwork and provides the real solution to the Permission denied error. Special bits and ACLs will be covered in Part 2.

> The Essentials in 30 Seconds > > - Every file has an owner, a group, and permissions for three categories: owner, group, and others. > - r = 4, w = 2, x = 1. We add them up: 7 = rwx, 5 = r-x, 6 = rw-. > - For a directory, x means “access” and w means “create and delete files inside it.” > - The kernel applies only one class: the first one that matches. > - If you encounter a Permission denied error, correct the owner or the group. Do not grant full access.

Reading an ls -l

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

Anatomy of an ls -l line: type, permissions for owner, group, and others

A type character (- for a file, d for a directory, l for a link), followed by three blocks of three letters: owner, group, others. Next come the file’s owner and group. The s in passwd is a special bit; we’ll save that for Part 2.

r, w, x: Different Meanings for Directories

This is the source of most misunderstandings. A directory is simply a list of names, and its permissions apply to that list.

Permission On a file On a directory
r read list the names
w write create, rename, delete entries
x execute navigate (cd, open a file inside it)

Two key points to remember:

  • The w permission on the directory is sufficient to delete a file, even if it’s read-only and belongs to someone else.
  • You must have x permissions on every directory in the path. If just one parent directory is set to 700, you’ll get Permission denied, even if the file is set to 644.

This second point is responsible for a good portion of the panic-induced 777 settings. namei -l displays the entire permissions string at once:

$ 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        ← the culprit
-rw-r--r-- jemadhar jemadhar index.html

Octal Permissions at a Glance

Three digits, in the order of owner / group / others. The only ones you need to know by heart:

Mode Permissions Purpose
755 rwxr-xr-x public directories, scripts
644 rw-r--r-- content files
750 / 640 rwxr-x--- / rw-r----- service folders and files
700 / 600 rwx------ / rw------- ~/.ssh, private keys

To adjust an entire directory tree, the uppercase X adds x permissions to directories only, not to files:

chmod -R u=rwX,g=rX,o= /srv/app    # directories set to 750, files set to 640

The kernel takes the first matching set

On Windows, permissions are cumulative. On Linux, this is not the case: the kernel checks the classes in order and stops at the first one that matches.

Unix permission decision algorithm: root, then owner, then group, then others

Hence this surprising case:

$ ls -l report.txt
----rw-r-- 1 jemadhar team 512 Oct 6 10:00 report.txt
$ cat report.txt
cat: report.txt: Permission denied

I am the owner, so only the first block --- applies to me. Everyone can read this file—except me.

Why 777 Is Really the Beast

“On my personal infrastructure, who’s going to exploit this?” Three very real-world scenarios:

  • The cron script. A backup.sh set to 777, run by root. Any local account can add a line to it, which will run as root the next time it’s executed.
  • The upload folder. A website set to 777, a poorly filtered form: a .php file uploaded there becomes executable immediately. It’s the most common type of webshell there is.
  • File sharing. A shared folder set to 777: anyone can delete others’ files, since deleting is equivalent to writing to the folder.

777 doesn’t solve the problem—it just makes it disappear along with security. The real problem was almost always an incorrect owner.

The real answer: Which process owns the file?

Three commands to diagnose the issue:

ps -o user,cmd -C nginx              # Which account is the service running under?
id www-data                          # Which groups does this account belong to?
stat -c '%U:%G %a' /var/www/site     # Who owns the directory?

Then we correct the ownership, not the permissions. Complete example for a website: the code belongs to the deployment account; the web server reads it via its group; and it writes only to uploads/.

chown -R deploy:www-data /var/www/site
chmod -R u=rwX,g=rX,o= /var/www/site       # read-only for the web server
chmod 770 /var/www/site/uploads            # write access only here
sudo -u www-data touch /var/www/site/uploads/test && echo OK

If the site is compromised, the attacker cannot rewrite the code. On the web server side, we also disable PHP execution in uploads/.

A common pitfall: after running usermod -aG www-data deploy, the new group doesn’t take effect until the next login or when the service is restarted. Without that, you might think nothing is working and end up with 777.

umask: Permissions at Creation

A program that creates a file sets permissions to 666, or 777 for a directory. These permissions are set from the very beginning. The kernel then removes the bits specified by the umask:

666 - 022 = 644 (file)      777 - 022 = 755 (directory)

The default umask, 022, allows everyone to read what you create. ANSSI recommends 0077 for user shells and 0027 for services (the UMask=0027 directive in the systemd unit).

Common Errors

Error What to do instead
chmod -R 777 to unblock an app correct owner and group
chmod -R 755 on an entire site chmod -R u=rwX,g=rX,o=rX
Permission denied with a file set to 644 namei -l: a parent file is blocking access
usermod -aG without logging back in new session or service restart
SSH private key set to 644 chmod 600, and 700 on ~/.ssh

> Key Takeaways > > - A Permission denied error is primarily a matter of identity rather than a matter of bits. > - On a directory, w allows deletion and x allows traversal. The entire path matters. > - No addition: the kernel applies the first matching class. > - chown to assign, uppercase X to correct in bulk, and umask to start off on the right foot. > - 777 makes the error disappear—and security along with it.

In Part 2: the s in passwd, the t in /tmp, a clean shared directory with setgid, and ACLs when a single owner and group are no longer sufficient.

Sources and References