$ 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
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
wpermission on the directory is sufficient to delete a file, even if it’s read-only and belongs to someone else. - You must have
xpermissions on every directory in the path. If just one parent directory is set to700, you’ll getPermission denied, even if the file is set to644.
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.
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.shset to777, 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.phpfile 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
- chmod(1) — Linux manual page: octal and symbolic modes, uppercase
X. - path_resolution(7) — Linux manual page: traversal permissions for each directory in the path; owner/group/others verification order.
- namei(1) — Linux manual page: displays permissions along a path.
- ANSSI, Recommendations for Configuring a GNU/Linux System (ANSSI-BP-028, v2.0): R36, umask 0077 for shells, 0027 for services.