Guides/LinuxLinux/Linux File Permissions, Properly Understood

Linux File Permissions, Properly Understood

Permission errors break more deployments than almost anything else. This is the mental model behind chmod, chown, and umask - read the ls -l output, understand who gets access and why, and stop fixing things with chmod 777.


Here's a pattern you'll see your whole career: a deploy goes out, the service won't start, and the logs say Permission denied. Someone runs chmod -R 777 on the directory, the service starts, everyone moves on - and a quiet security hole is now in production.

The reason this keeps happening is that most engineers learn permissions as a set of magic numbers to copy-paste, never the model underneath. The model is small and it's worth an afternoon. Once you have it, permission errors stop being guesswork - you look at the file, you look at the process, and the fix is obvious.

Reading what ls -l tells you

Every permission decision starts here. Run ls -l and the first column is ten characters that describe the file completely:

Anatomy of a Linux permission string - one type bit followed by owner, group, and others triplets The ten characters ls -l prints: one type bit, then three identical read/write/execute triplets - one each for the owner, the group, and everyone else.

That's the whole structure. One character for the type, then the same rwx pattern repeated three times for three different audiences. A dash in any slot means "that permission is off." Once you can read this string at a glance, half the battle is over - you can look at any file and immediately know who can do what.

What r, w, and x actually mean

The catch most people miss: read, write, and execute mean different things for files than they do for directories. This is the source of an enormous amount of confusion, so it's worth being precise.

For a file:

  • read (r) - you can view the contents (cat, less, open it in an editor).
  • write (w) - you can change the contents.
  • execute (x) - you can run it as a program or script.

For a directory:

  • read (r) - you can list the names inside it (ls).
  • write (w) - you can create, rename, and delete files in it. Note: deleting a file depends on the directory's permissions, not the file's. That surprises people.
  • execute (x) - you can enter it (cd) and access files inside by name. Without x on a directory, you can't reach anything within it even if the files themselves are wide open.

The classic head-scratcher: a directory with r but no x. You can see the filenames but can't actually open any of them. Or x without r: you can access a file if you already know its exact name, but ls gives you "Permission denied." Keep the file-vs-directory distinction in mind and these stop being mysteries.

The octal shorthand

You'll constantly see permissions written as three digits - 755, 644, 600. This isn't a separate system; it's just the same rwx triplets written as numbers. Each permission has a value, and you add them up per triplet:

Converting rwx triplets to octal - read is 4, write is 2, execute is 1, summed per triplet read = 4, write = 2, execute = 1. Add them within each triplet, and three triplets give you three digits. rwx r-x r-x becomes 755.

Once it clicks, you can read octal in your head. The handful you'll actually use day to day:

  • 644 (rw-r--r--) - normal files. Owner edits, everyone else reads.
  • 755 (rwxr-xr-x) - executables, scripts, and directories. Owner has full control, everyone else can run/enter and read.
  • 600 (rw-------) - private files. Only the owner, nobody else. SSH keys live here.
  • 700 (rwx------) - private directories. Only the owner can enter. Your ~/.ssh directory wants this.

You don't need to memorize a table. Remember r=4, w=2, x=1, and the rest is arithmetic.

Owner, group, others - and the rule that trips everyone up

Three audiences, checked in a strict order: are you the owner? Failing that, are you in the file's group? Failing that, you're one of the others. Simple enough - until you hit the rule that almost nobody is taught explicitly:

How Linux picks which triplet applies - it checks owner, then group, then others, and stops at the first identity that matches The kernel stops at the first identity that matches. It is not cumulative - if you're the owner, only the owner bits apply to you, even if the group or others have more.

This is the single most counterintuitive thing about Linux permissions, so let it sink in: permissions are not additive. If you are the file's owner, the kernel uses the owner triplet and never looks at the group or others bits - even if they'd grant you more. It's entirely possible to own a file you can't write to, while a stranger can, because the owner triplet says r-- and the others triplet says rw-. Rare in practice, but when it bites you, this is why.

The everyday version of this rule: adding yourself to a group doesn't help if you're already the owner and the owner bits are restrictive. Check which triplet actually applies to you before you start changing things.

Changing permissions: chmod

chmod has two modes. Octal sets all three triplets at once - great when you know exactly what you want:

chmod 755 /usr/local/bin/deploy     # rwxr-xr-x  - an executable
chmod 644 /etc/app/config.yml       # rw-r--r--  - a config file
chmod 600 ~/.ssh/id_ed25519         # rw-------  - a private key
chmod -R 750 /opt/myapp             # recursive, whole tree

Symbolic mode changes just the bits you name and leaves the rest alone - better when you only want to flip one thing:

Symbolic chmod decoded - u+x means add the execute permission for the owner, flipping rw- to rwx Symbolic mode reads as who / operation / permission. u+x = for the owner (u), add (+), execute (x). The other triplets stay exactly as they were.

chmod u+x script.sh        # make it runnable by its owner
chmod g-w shared.conf      # take write away from the group
chmod o-rwx secret.log     # remove ALL access for others
chmod a+r public.txt       # everyone can read (a = all)
chmod u=rw,go= private.env # owner reads/writes, group & others get nothing

Reach for octal when you're setting a known-good state, symbolic when you're surgically changing one permission without disturbing the others. A common mistake is using chmod 644 to "fix" one bit and accidentally wiping the execute bit off a script in the process - symbolic mode avoids that.

Recursive chmod footgun: chmod -R 755 . sets 755 on files and directories. That's usually fine for directories but makes every file executable. If you need different modes for each, use find . -type d -exec chmod 755 {} + and find . -type f -exec chmod 644 {} +.

Changing ownership: chown

Permissions decide what each audience can do; ownership decides who is the owner and which group applies. When a service runs as www-data but the files are owned by root, the service falls into the "others" bucket - and if others has no access, you get Permission denied. The fix is usually ownership, not a looser chmod:

chown www-data:www-data /var/www/html/index.html   # owner:group
chown -R appuser:appgroup /opt/myapp/              # recursive
chown :developers shared.txt                        # change only the group

The right instinct when you hit a permission error in production: find out which user the process runs as (ps -o user= -p <pid>, or check the systemd unit's User=), then make sure that user has access - ideally by owning the files or being in the right group, with the minimum permissions needed.

Defaults: umask

New files don't come out of nowhere with their permissions - a umask decides the defaults by masking off bits. The common umask 022 means new files are created 644 and new directories 755 (it strips the write bit from group and others). You'll occasionally need to set a tighter umask 027 for a service so its files aren't world-readable:

umask                # show the current mask, e.g. 0022
umask 027            # new files 640, new dirs 750 - nothing for "others"

Most of the time you won't touch this, but when a daemon keeps creating files with permissions you didn't expect, its umask (often set in the systemd unit or an init script) is the thing to check.

The special bits, briefly

Beyond rwx there are three special bits you'll meet occasionally:

  • setuid (u+s, mode 4xxx) - the program runs as its owner rather than whoever launched it. passwd uses this to edit /etc/shadow as root. Powerful and dangerous: a setuid-root binary with a bug is a privilege-escalation path. Audit them with find / -perm -4000 -type f 2>/dev/null.
  • setgid (g+s, mode 2xxx) - on a directory, new files inherit the directory's group. Handy for shared team folders so everything stays group-readable.
  • sticky bit (+t, mode 1xxx) - on a directory, users can only delete their own files, even if the directory is world-writable. This is why /tmp (mode 1777) doesn't let users delete each other's files.

You don't need these often, but recognizing an s or t in ls -l output - and knowing it's deliberate, not corruption - matters.

The SSH-key gotcha everyone hits once

This deserves its own callout because it wastes an afternoon for almost everyone the first time. SSH refuses to use a private key whose permissions are too loose, and the error it gives - Permission denied (publickey) - points you in completely the wrong direction. The rules:

chmod 700 ~/.ssh             # the directory: owner only
chmod 600 ~/.ssh/id_ed25519  # private keys: owner read/write only
chmod 644 ~/.ssh/id_ed25519.pub   # public keys can be world-readable
chmod 600 ~/.ssh/authorized_keys  # on the server side

If your key is group- or world-readable, SSH silently ignores it and falls back to other auth methods, then fails. When publickey auth mysteriously won't work, check the permissions before you touch anything else.

Common mistakes

chmod 777 as a fix. It "works" by removing all protection - it's not a fix, it's a security hole, and it hides the real problem. The real problem is almost always ownership: the wrong user is trying to access the file. Find which user the process runs as and grant the minimum (640 + correct chown covers most cases).

Assuming root ignores all permissions. Root bypasses read and write checks on regular files, but execute still needs the x bit set, and application-level checks (like SSH's key-permission enforcement) are not the kernel's call - root doesn't get a pass on those.

Forgetting the directory x bit. You set a file to 644 and the app still can't read it - because a parent directory is missing x, so the process can't traverse into it to reach the file. Permissions on the whole path matter, not just the file.

Recursively chmod-ing files and directories to the same mode. chmod -R 755 makes every file executable. Use find with -type f and -type d to set them separately.

Practice this until it's reflex

Reading about permissions doesn't build the instinct - breaking and fixing things does. On a throwaway server or even your laptop:

  1. Create a file, ls -l it, and convert the permission string to octal in your head. Check yourself with stat -c '%a' <file>.
  2. Make a script, try to run it (it won't), then chmod u+x it and run it again. Watch the bit change with ls -l.
  3. Create a directory, remove its x bit (chmod -x), and try to cd into it. Then remove r instead and try to ls it. Feel the difference between the two.
  4. Reproduce the SSH gotcha on purpose: chmod 644 a private key, watch the connection fail, fix it with 600, watch it work.

Do that loop once and ls -l stops being a wall of symbols - it becomes a sentence you can read. That's the entire goal, and it pays off every time a deploy fails with Permission denied.