You upload a new site to your Linux server, open the page in a browser, and see a flat, unfriendly 403 Forbidden. Or worse, you fix it by typing chmod 777 on everything, the page loads, and you quietly hope nobody ever looks at that folder again.
Almost everyone who runs a server has been there. File permissions feel like a cryptic code, so we copy a command from a forum and move on. But the code is small, consistent, and learnable in about fifteen minutes. Once you can read it, most "permission denied" errors stop being mysteries and start being one-line fixes.
Reading the permission string
Run ls -l in any directory and you will see lines like this:
$ ls -l
-rw-r--r-- 1 deploy www-data 4821 Nov 2 09:14 index.php
drwxr-xr-x 2 deploy www-data 4096 Nov 2 09:12 uploads
-rwx------ 1 deploy deploy 912 Nov 1 18:40 backup.shThe first column is ten characters. The first character is the type: - for a regular file, d for a directory, l for a symbolic link. The next nine characters are three groups of three, and each group answers the same question for a different audience.
- Characters 2–4: what the owner (user) can do
- Characters 5–7: what the group can do
- Characters 8–10: what everyone else (others) can do
Inside each group, r means read, w means write, x means execute, and - means "not allowed." So rw-r--r-- reads as: the owner can read and write, the group can only read, and everyone else can only read. The third and fourth columns of ls -l tell you who the owner and the group actually are (deploy and www-data above).
Permissions are just three yes-or-no questions — read, write, execute — asked of three audiences.
The numbers: why 644 and 755 keep showing up
The numeric form is the same information in a compact shape. Each permission has a value: read = 4, write = 2, execute = 1. Add them up per group and you get one digit per audience.
| Symbolic | Math | Number |
|---|---|---|
rwx | 4+2+1 | 7 |
rw- | 4+2 | 6 |
r-x | 4+1 | 5 |
r-- | 4 | 4 |
--- | 0 | 0 |
So rw-r--r-- is 644 and rwxr-xr-x is 755. These two numbers cover most of what a web server needs: 644 for ordinary files (the owner edits, everybody can read) and 755 for directories and scripts (everybody can enter or run, only the owner can change).
chmod 644 index.php
chmod 755 uploads
chmod 600 .env # secrets: owner onlyThe last line is the habit worth building. Anything holding passwords, API keys or database credentials should be 600, so no other account on the machine can even read it.
Directories play by different rules
Here is the detail that trips up beginners: the same three letters mean something slightly different on a directory.
ron a directory lets you list the names inside it.wlets you create, rename or delete entries inside it, regardless of the permissions on the files themselves.xlets you enter it (cd) and reach files inside by name.
That explains a classic puzzle. Suppose index.php is 644, but the web server still returns 403. Very often the file is fine and a parent directory is missing x for the web server's user. The server cannot walk through /home/deploy to reach the file, so the file's own permissions never even get a vote. You can check every folder along the path in one go:
namei -l /home/deploy/public_html/index.phpnamei prints the permissions of each component of the path, so the broken link in the chain stands out immediately.
chown and chgrp: fixing who owns what
Permissions answer "what is allowed." Ownership answers "for whom." If you upload files as root (say, by running sudo cp or sudo unzip), they belong to root, and a web server running as www-data is just "others" with read-only access. Uploads will then fail because PHP cannot write to a folder it does not own.
# Give the site to your deploy user, with the web server's group
sudo chown -R deploy:www-data /var/www/mysite
# Let the group write only where it must
sudo chmod -R g+w /var/www/mysite/uploadsThe deploy:www-data syntax sets the owner and the group in one command. The symbolic form, g+w, adds the write bit for the group without touching anything else, which is handy when you do not want to recompute the number.
A safer recipe than chmod -R 777
Running chmod -R 777 makes every file writable and executable by every user and every process on the box. If any one of your sites has a vulnerable upload form, an attacker can drop a script into a writable folder and run it. It "fixes" the error by removing the lock from the door.
The better habit is to set files and directories separately. A recursive chmod 755 would mark every image and text file executable, which you do not want. Use find to treat each type correctly:
cd /var/www/mysite
sudo find . -type d -exec chmod 755 {} \;
sudo find . -type f -exec chmod 644 {} \;
# Only the folders the app must write to get extra access
sudo chmod 775 storage uploads
sudo chmod 600 .envA tidy pattern for a typical PHP and MySQL site looks like this:
- Own the code as a deploy user, so the web process cannot rewrite its own source.
- Give the web server's group read access to the code.
- Make only
uploads/,cache/orstorage/group-writable. - Keep config files with credentials at 600 or 640.
The goal is not "make the error go away." It is "give each process exactly the access it needs and nothing more."
AD
Special bits worth knowing
Three extra bits show up less often, but you will meet them eventually.
The setgid bit on a directory (chmod g+s) makes new files inherit the directory's group instead of the creator's primary group. That is perfect for a shared project folder where several people should all end up in the same group. You will see it as an s in the group's execute slot, or as a leading 2 in the numeric form, like 2775.
The sticky bit (chmod +t) on a directory means only a file's owner can delete it, even if others have write access to the folder. That is exactly how /tmp works, and you will see it as a t at the end, as in drwxrwxrwt.
The setuid bit makes a program run with its owner's privileges. It is how passwd can update protected files on behalf of ordinary users. It is also a bit you should never set casually, since a setuid program owned by root is a high-value target.
A quick debugging checklist
When something says "Permission denied," run through the same short routine each time instead of guessing:
- Run
ls -lon the file andls -ldon its directory to see owner, group and mode. - Run
namei -lon the full path to find a parent folder missingx. - Ask which user the process runs as (
ps aux | grep -E 'php-fpm|nginx|apache') and compare it with the owner and group. - Remember that SELinux or AppArmor can deny access even when the classic permissions look perfect. If everything else checks out, look at their logs.
Working through those four steps usually finds the culprit in under two minutes.
Summary
Linux permissions are three audiences (owner, group, others) times three actions (read, write, execute). Add 4, 2 and 1 to get the familiar numbers: 644 for normal files, 755 for directories and scripts, 600 for secrets. Directories behave a little differently, because x means "may enter," which is why a locked parent folder can break an otherwise correct file. Fix ownership with chown first, tune modes with find and chmod second, and treat 777 as a warning sign rather than a solution.
Next time a 403 appears, take a breath and read the string. It is a small code, and now you can read it.


