linux
chmod 755 vs 644: Linux File Permissions Without the Guesswork
What the three digits actually mean, why directories need the execute bit, when to use 600, and the recursive chmod mistake that breaks every folder on your server.
Everyone learns chmod 755 and chmod 644 by copy-paste, and most people stop there — right up until a deploy script fails, SSH refuses a key, or someone in a code review asks why a config file is world-readable. The numbers aren’t arbitrary, and once you can read them the whole system takes about five minutes to understand for good.
The three digits
Each digit is a role: owner, group, everyone else.
7 5 5
│ │ │
owner group others
Each digit is the sum of three permission bits:
| Bit | Value | On a file | On a directory |
|---|---|---|---|
read (r) | 4 | Read the contents | List the names inside |
write (w) | 2 | Modify the contents | Create, rename, delete entries |
execute (x) | 1 | Run it as a program | Enter it / traverse into it |
So 7 = 4+2+1 (read, write, execute), 6 = 4+2 (read, write), 5 = 4+1 (read, execute), 4 (read only), 0 (nothing). There are only eight possible digits, and you can do the arithmetic in your head after a few days.
That means:
- 755 =
rwxr-xr-x— owner does anything; everyone else can read and execute, not modify. - 644 =
rw-r--r--— owner reads and writes; everyone else reads only.
The free Chmod Calculator flips between the numeric and symbolic forms with checkboxes, which is faster than doing the math when you’re mid-incident.
The rule that covers 95% of cases
644 for files. 755 for directories and anything meant to run.
HTML, CSS, images, .php files served by a web server, config files, README — 644. Directories, shell scripts, binaries, manage.py — 755.
The reason directories need the execute bit is the part that’s genuinely non-obvious: on a directory, x means “traverse,” not “run.” Without it, you cannot cd into the directory or access anything inside it — even a file you have full permissions on. A directory at 644 is effectively a wall. You can list its contents (that’s r) but you can’t open any of them.
This produces the classic broken-website symptom: files look fine, permissions look fine, and every request 403s because one parent directory lost its execute bit.
Reading ls -l output
drwxr-xr-x 4 kyle staff 128 Jul 25 09:14 assets
-rw-r--r-- 1 kyle staff 2048 Jul 25 09:12 index.html
-rwx------ 1 kyle staff 512 Jul 25 09:10 deploy.sh
The first character is the type, not a permission: - file, d directory, l symlink. The next nine are three groups of three. So drwxr-xr-x is a directory at 755, -rw-r--r-- is a file at 644, and -rwx------ is 700 — a script only its owner can even see.
Symbolic mode: the safer everyday tool
Numeric mode replaces all permissions at once. Symbolic mode adjusts them relatively, which is usually what you want:
chmod +x deploy.sh # make executable (for everyone the umask allows)
chmod u+x deploy.sh # executable for the owner only
chmod go-w shared.conf # remove write from group and others
chmod a+r public.txt # readable by all
chmod u=rw,go=r index.html # exactly 644, spelled out
The letters: u user/owner, g group, o others, a all. The operators: + add, - remove, = set exactly (clearing the rest).
chmod +x script.sh is the single most-typed permission command in existence, and it’s symbolic for a reason — it adds the execute bit without disturbing anything else.
The 600 and 700 cases
Some things must be private to their owner, and tools will actively refuse to work otherwise:
chmod 700 ~/.ssh # rwx------
chmod 600 ~/.ssh/id_ed25519 # rw------- (private key)
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/authorized_keys
SSH will reject a private key that’s group- or world-readable with WARNING: UNPROTECTED PRIVATE KEY FILE! and then refuse to use it. That’s not paranoia — a readable key on a shared box is a full compromise. Same logic applies to .env files, .netrc, API credentials, and database config: 600, always.
(If you’re generating keys, Ed25519 vs RSA covers which type to pick, and the SSH Key Generator makes a pair in-browser.)
Never, ever chmod 777. It means “anyone on this system may rewrite this file,” and it is never the correct fix for a permission problem — it’s just a way to convert a 403 into a security hole. If 777 “fixes” it, the real answer is almost always ownership (chown) or a missing directory x bit.
The recursive chmod mistake
This is the one that ruins afternoons:
chmod -R 644 /var/www/html # ← DO NOT
It sets every directory to 644 too, stripping the traverse bit from all of them. Your entire site becomes unreachable, including to the process trying to fix it.
Do it in two passes instead:
find /var/www/html -type d -exec chmod 755 {} +
find /var/www/html -type f -exec chmod 644 {} +
Or use the capital-X trick, which sets execute only on directories and files that already have an execute bit somewhere:
chmod -R u=rwX,go=rX /var/www/html
Capital X versus lowercase x is one of the genuinely useful bits of chmod trivia. It exists precisely for this situation.
umask: why new files aren’t what you set
Create a file and check it — you’ll typically get 644 even though you never chose that. The umask is a mask of bits removed from the default (666 for files, 777 for directories):
| umask | New files | New directories |
|---|---|---|
022 (typical) | 644 | 755 |
002 (group-collaborative) | 664 | 775 |
077 (private) | 600 | 700 |
umask # show current value
umask 077 # everything you create from now on is owner-only
Files never get the execute bit by default no matter what the umask says — that’s why you always have to chmod +x a fresh script. If your deploy uploads files with unexpected permissions, the umask of the process doing the writing is where to look.
The fourth digit: setuid, setgid, sticky
Occasionally you’ll see four digits, like chmod 4755 or 1777. The leading digit holds three special bits:
| Bit | Value | Effect |
|---|---|---|
| setuid | 4 | Executable runs as its owner, not the caller (passwd uses this) |
| setgid | 2 | On a dir: new files inherit the dir’s group — the standard fix for shared team folders |
| sticky | 1 | Only the file’s owner can delete it, even in a world-writable dir (/tmp is 1777) |
In ls -l they show up in place of the x: rwsr-xr-x is setuid, rwxr-sr-x is setgid, drwxrwxrwt is sticky. setuid on your own scripts is a privilege-escalation bug waiting to happen — Linux ignores it on shell scripts for exactly that reason. setgid on directories, on the other hand, is genuinely useful whenever several people write to the same tree.
Common permissions at a glance
| Mode | Symbolic | Use for |
|---|---|---|
| 644 | rw-r--r-- | Web files, configs, documents, public keys |
| 755 | rwxr-xr-x | Directories, scripts, binaries |
| 600 | rw------- | SSH private keys, .env, credentials |
| 700 | rwx------ | ~/.ssh, private script directories |
| 664 | rw-rw-r-- | Files a team group edits together |
| 775 | rwxrwxr-x | Directories a team group writes to |
| 400 | r-------- | Read-only secrets (some cloud tools require this) |
| 777 | rwxrwxrwx | Nothing. Ever. |
When permissions aren’t the problem
If 755/644 are correct and access still fails, check these before reaching for bigger numbers:
- Ownership.
chown -R www-data:www-data /var/www— permissions describe what the owner may do; if the web server isn’t the owner or in the group, generous file modes won’t help. - A parent directory. Access requires the execute bit on every directory in the path.
namei -l /var/www/html/index.htmlprints the permissions of each component in the chain and usually finds the culprit instantly. - SELinux or AppArmor. On RHEL/Fedora,
ls -lcan look perfect while SELinux denies the access. Checkls -Zand the audit log. - Mount options. A filesystem mounted
roornoexecoverrides everything above it.
Cheat sheet
chmod 644 file.txt # standard file
chmod 755 script.sh # standard executable / directory
chmod +x script.sh # just add execute
chmod 600 ~/.ssh/id_ed25519 # private key
chmod -R u=rwX,go=rX ./site # recursive, directory-safe
find . -type d -exec chmod 755 {} + # recursive dirs
find . -type f -exec chmod 644 {} + # recursive files
stat -c '%a %n' file.txt # show numeric mode (Linux)
stat -f '%A %N' file.txt # show numeric mode (macOS)
umask 022 # default for new files
For anything more complicated than the table above, the Chmod Calculator will translate between numeric and symbolic notation both ways, so you can confirm what 2775 means before you run it on a production directory. If you’re locking down web content specifically, the htpasswd Generator and .htaccess Generator cover the layer above the filesystem.