DEV Community

Amayo Clinton
Amayo Clinton

Posted on

Linux Permissions, Actually Explained (Not Just Memorized)

If you've been using Linux for more than a week, you've typed chmod 755 or chmod 644 into a terminal without really knowing what those numbers meant. You copy-pasted it from Stack Overflow, it worked, and you moved on. Fair enough — most days, that's all you need.

But permissions show up everywhere: SSH key errors, deploy scripts that mysteriously fail, Docker volumes that refuse to be read, cron jobs that silently do nothing. At some point "I'll just chmod 777 it" stops being a joke and starts being a real security hole. So let's actually break this down — what the letters mean, what the numbers mean, and how they connect — so you're reasoning about permissions instead of guessing.

Who Permissions Apply To

Every file and directory on a Unix-like system has exactly three permission "classes" checked against it:

  • User — the account that owns the file
  • Group — a set of accounts assigned to the file
  • Other — literally everyone else on the system

That's it. There's no "admin" class, no per-user access control list in the base model — just owner, group, and the rest of the world. When the kernel decides whether you're allowed to read, write, or execute something, it checks which of these three buckets you fall into, in that order, and applies the matching permission set.

This is worth sitting with for a second, because it explains a lot of "why can't I access this file" confusion. If you're not the owner and you're not in the file's group, Linux doesn't care that you're a sudo-capable admin on the box — it applies the other permissions, full stop (unless you escalate privileges, which is a separate mechanism entirely).

Reading Symbolic Notation

Run ls -l in any directory and you'll see a string like -rwxr-xr-- sitting to the left of every filename. This is symbolic notation, and once you know the pattern, it reads like a sentence.

The very first character isn't a permission at all — it's the file type:

  • - regular file
  • d directory
  • l symbolic link
  • c character device
  • b block device

Everything after that comes in groups of three, one group per class, always in the order user, group, other. Each group has the same three-slot pattern: read, write, execute.

  • r — permission granted
  • w — permission granted
  • x — permission granted
  • - — permission not granted, in that slot

So -rwxr-xr-- breaks down as: a regular file, owned by a user who can read/write/execute it, a group that can read/execute it (but not write), and everyone else who can only read it.

One subtlety that trips people up: x on a directory doesn't mean "execute" in the programmatic sense — it means "you're allowed to cd into it or access files inside it by name." Read on a directory just lets you list its contents (ls). You genuinely need both r and x to browse a directory normally, which is why you'll often see rwxr-xr-x on folders instead of rw-r--r--.

Octal Notation and the Math Behind It

Symbolic notation is readable but verbose. Octal notation compresses the same information into up to four digits, and it's what you'll actually type most of the time with chmod.

The trick is that each permission has a fixed numeric weight:

  • r = 4
  • w = 2
  • x = 1

To get the digit for a class, you just add up whichever permissions are granted. rwx is 4+2+1 = 7. rw- is 4+2 = 6. r-x is 4+1 = 5. r-- is just 4. There's no ambiguity — every combination of read/write/execute maps to exactly one number from 0 to 7, because these are binary flags being summed, not arbitrary values.

Do this once for each of the three classes and you get a three-digit mode. rwxr-xr-- becomes 7 (user) 5 (group) 4 (other), or 754. That's genuinely the entire algorithm — there's no lookup table to memorize, just three additions.

A fourth, leading digit exists too (covered below), which is why you'll sometimes see four-digit modes like 0755 or 4755 — the leading 0 just means "no special bits set," and it's often omitted since chmod 755 and chmod 0755 do the same thing.

chmod in Practice

Once you can do the read/write/execute math in your head, chmod stops being a magic incantation. Here are the combinations you'll actually reach for on a day-to-day basis:

A few notes on judgment calls, not just syntax:

  • 755 for anything executable — scripts, binaries you built, CLI tools. Owner gets full control, everyone else can run it and see it, nobody but you can modify it.
  • 644 for regular files — source code, configs meant to be world-readable, static assets. Nobody but the owner should be writing to most files by default.
  • 700 for anything private — your SSH ~/.ssh directory, personal scripts with credentials baked in. SSH will actually refuse to use a private key if its permissions are too open, which is one of the few places Linux enforces permission hygiene for you.
  • 777 is a code smell, not a fix. If you're reaching for chmod 777 to make an error go away, you haven't found the actual permission problem — you've just removed the protection that would've told you what it was. It's fine on a disposable container for five minutes of debugging; it's not fine on anything that touches real data.

You can also skip octal entirely and use symbolic (relative) syntax, which is handy when you only want to change one thing without recalculating the whole mode:

chmod u+x deploy.sh      # add execute for the owner only
chmod g-w config.yml     # remove write from the group
chmod o=r notes.txt      # set other to read-only, exactly
chmod a+r README.md      # add read for everyone
Enter fullscreen mode Exit fullscreen mode

u, g, o, a map to user, group, other, all — and +, -, = add, remove, or set exactly. This form doesn't require you to know the existing permissions, which makes it safer for quick, targeted changes on files you didn't set up yourself.

Add -R to any chmod command to apply it recursively to a directory and everything inside it — but be careful, because blanket-applying 755 recursively will also make every file executable, which is rarely what you want. For directory trees, it's usually safer to set directory and file permissions separately with find:

find . -type d -exec chmod 755 {} \;   # directories: 755
find . -type f -exec chmod 644 {} \;   # files: 644
Enter fullscreen mode Exit fullscreen mode

The Fourth Digit: setuid, setgid, and the Sticky Bit

Beyond the standard nine permission bits, there's a fourth, less commonly used set that changes how execution or ownership behaves, rather than just who has access. These are the special permission bits, and they're where a leading digit like 4, 2, or 1 comes from in a four-digit octal mode.

setuid (4000) — when set on an executable, the program runs with the file owner's privileges, not the privileges of whoever launched it. The classic example is /usr/bin/passwd: it needs to write to /etc/shadow, which regular users can't touch directly, so it's owned by root and setuid'd — letting any user change their own password without being root themselves. In symbolic notation, this shows up as an s where the owner's execute bit would normally be: rwsr-xr-x.

setgid (2000) — similar idea, but for the group. On an executable, it runs with the file's group privileges instead of the group of whoever ran it. On a directory, it does something different and genuinely useful: any new file created inside inherits the directory's group automatically, instead of the creating user's default group. This is the standard trick for shared team directories where everyone needs files to end up in the same group without remembering to chgrp manually every time.

Sticky bit (1000) — almost exclusively used on directories. It restricts deletion: even if a user has write access to a directory (and could therefore normally delete or rename anything in it), the sticky bit means they can only delete or rename files they themselves own. /tmp is the textbook example — it's world-writable (777) so any process can drop temp files there, but the sticky bit stops one user from deleting another user's temp files. Symbolically it appears as t in the other-execute slot: rwxr-xr-t.

You combine these by adding their weights the same way you add r, w, x: a directory that's setgid and has permissions 755 becomes 2755. A world-writable sticky directory is 1777, which is exactly what /tmp uses in practice.

Ownership: chown and chgrp

Permissions decide what a class can do; ownership decides who's in the user and group classes in the first place. These usually travel together:

chown jarret file.txt          # change the owner
chgrp devs file.txt            # change the group
chown jarret:devs file.txt     # change both at once
chown -R jarret:devs project/  # recursively, for a whole directory tree
Enter fullscreen mode Exit fullscreen mode

You generally need root (or sudo) to change ownership to another user, for obvious reasons — otherwise anyone could hand their files off to someone else to dodge disk quotas or audit trails.

Debugging Real Permission Errors

Knowing the theory is one thing — recognizing it in the wild is another. A few situations you'll run into constantly:

"Permission denied" when running a script you just wrote. Nine times out of ten, you forgot the execute bit. Creating a file with a text editor gives it 644 by default, which has no execute permission for anyone. chmod +x script.sh (shorthand for chmod a+x) fixes it without you needing to work out the full octal mode.

SSH refuses your private key with "UNPROTECTED PRIVATE KEY FILE". SSH deliberately checks that your private key isn't readable by group or other, because a key anyone on the system can read isn't really private. The fix is almost always chmod 600 ~/.ssh/id_rsa — owner read/write, nobody else anything.

A web server can't read files you just deployed. This is usually an ownership mismatch, not a permission mismatch — the files are owned by your deploy user, but the web server runs as www-data or nginx, which falls into the other class and may not have read access if the mode is something tight like 600. The fix is either loosening the mode to 644/755 for other, or better, putting both accounts in a shared group and using 640/750 with chgrp.

A cron job works when you run it manually but silently fails on schedule. Cron often runs as a different user (or with a stripped-down environment) than your interactive shell. If the script or the files it touches aren't readable/executable by whatever user cron runs as, it fails quietly with no terminal to show you the error — check /var/log/syslog or journalctl -u cron for the actual permission denial.

Docker volume mounted from the host has the wrong permissions inside the container. Containers frequently run processes as a different UID than your host user, even though the filesystem UID numbers are shared. A file that's 644 and owned by UID 1000 on your host might be unreadable to a container process running as UID 999. This one isn't solved by chmod alone — you typically need to align the UID the container runs as with the UID that owns the mounted files.

In every one of these cases, the fix falls out naturally once you ask the right question: which class (user/group/other) is the process actually running as, and does that class have the permission it needs? That's the entire debugging methodology — everything above is just that question applied to a specific tool.

Putting It Together

Permissions on Linux really come down to three ideas stacked on top of each other: three classes (user, group, other), three abilities per class (read, write, execute), and one compact way to write all nine bits as three digits by adding 4, 2, and 1. Everything else — setuid, setgid, sticky bits, symbolic shorthand — is a small extension on top of that same base logic, not a separate system to memorize.

Next time you type chmod 644 without thinking about it, you'll know exactly what those two digits are doing — and more importantly, you'll be able to work out the right number for a situation you haven't seen a Stack Overflow answer for yet.


That's the whole model — three classes, three abilities, one bit of addition, and a few special cases layered on top. If you've got a permissions war story of your own (a 777 you regret, a setgid trick that saved a shared directory, a Docker UID mismatch that took way too long to track down), drop it in the comments — I'd genuinely like to hear it.

And if this was useful, a follow or a ❤️ helps more people find it. Thanks for reading!

Top comments (0)