Permissions

5 min read

Core idea

One owner, one group, everyone else — three bits each

Linux inherits its permission model from 1970s Unix, where the basic insight was that a multiuser computer needs some access control but cannot afford the complexity of full access-control lists for every file. The compromise is elegant: every file has exactly one owner (a user) and one group, plus three permission triplets — read, write, execute — for user (the owner), group (members of the file's group), and other (everyone else). Nine bits, three classes, three actions. That is the entire model.

It is not the most expressive permission system in the world, but it has lasted fifty years because it is small enough to reason about, fast enough to check on every syscall, and composable enough to express most real-world policies through groups and the setuid/setgid/sticky bits.

The shell exposes the model through three verbs

Three commands change permissions: chmod (change the mode bits), chown (change the owner and/or group), and umask (set the default permissions removed from newly created files). Two commands change identity: su (substitute user, start a shell as someone else) and sudo (run one command as someone else, after policy check). Together they implement the principle of least privilege: stay as yourself unless a specific task requires elevation, then elevate only for that task.

Why it matters

It is the security boundary every Linux service trusts

Every system service — your web server, your database, your SSH daemon — stays sandboxed because file permissions stop it from reading /etc/shadow or writing into /root. When permissions are wrong, the boundary is wrong: a world-writable /etc/passwd is game over; a world-readable private SSH key gives an attacker your identity. Understanding the model is not optional for anyone who runs a service on a Linux box.

It is the model that survived the Windows alternative

Windows for decades let users run as Administrator by default. The result was a generation of malware that simply inherited the user's full privileges. Modern Linux distributions (and now macOS, and even modern Windows via UAC) converged on the Unix pattern: regular accounts run with restricted privileges, and elevation requires deliberate, authenticated action through sudo. The reason this matters is operational, not theoretical — when a process is compromised, the permissions on the files it can touch determine how bad the day gets.

Key takeaways

Mental model

How the kernel resolves a permission check

When a process tries to read, write, or execute a file, the kernel asks a chain of three questions in order. The first match wins.

How the kernel resolves a permission check

Octal as a packed triplet

Octal as a packed triplet

Practical application

  1. Read modes before changing them. Run ls -l (or stat) before any chmod or chown. The current mode tells you what's already permitted and who already owns the file — half the time the right answer is "nothing to change."

  2. Use symbolic chmod for adjustments, octal for resets. chmod +x script.sh adds execute to all three classes without touching the read/write bits. chmod 644 file resets the entire mode. Reach for symbolic when modifying, octal when defining.

  3. Use sudo, not su -, by default. sudo command runs one command as root and returns you to your own shell. sudo -i opens a root shell when you genuinely need many commands. Avoid logging in as root or running a long su - session — every minute as root is a minute where a typo can wreck the system.

  4. For shared directories, set the setgid bit on the directory. chmod 2775 /srv/shared makes new files inside inherit the group ownership of /srv/shared. Combined with umask 002 for the group's members, this gives a working "team folder" without per-file ownership fixes.

  5. Audit setuid binaries periodically. Run find / -perm -4000 -type f 2>/dev/null to list every setuid program on the system. There should be a small, known set (passwd, sudo, ping, a handful of others). An unexpected setuid binary in a user-writable location is a likely backdoor.

  6. Never make secrets group- or world-readable. SSH keys, API tokens, and config files containing passwords should be chmod 600 (or 400). Many tools refuse to use a key that is not — SSH itself will reject a 644 private key with a permissions-too-open error.

Example

Building a shared "team" directory step by step

Imagine two users alice and bob need to collaborate on files under /srv/team. The naïve approach — mkdir /srv/team && chmod 777 /srv/team — works but is wide open to every user on the box. The correct setup uses a group and the setgid bit.

# As root (or via sudo):
$ groupadd team
$ usermod -aG team alice
$ usermod -aG team bob

$ mkdir /srv/team
$ chown :team /srv/team           # group owner becomes 'team'
$ chmod 2775 /srv/team             # 2 = setgid; 7=rwx user, 7=rwx group, 5=r-x other

Now any file alice creates under /srv/team inherits the team group (because of setgid on the parent directory), and bob — who is in team — can read it. Outsiders can list the directory (mode 5 = r-x for other) but cannot write.

There's one remaining wart: alice's umask defaults to 022, which means files she creates are 644 (rw-r--r--), and bob cannot write to them even though he's in the group. The fix is to set umask 002 in alice's and bob's ~/.bashrc so newly created files are 664 (rw-rw-r--) and group writes are allowed.

This is the canonical setgid + umask pattern. It is what GitHub's repository file modes used internally for years, what most enterprise NFS shares use, and what every multi-developer build directory should use.

Why setuid on a script doesn't work the way you think

Suppose you write a Python script that needs to read a privileged log file, and you try:

$ sudo chown root /usr/local/bin/check-logs.py
$ sudo chmod 4755 /usr/local/bin/check-logs.py    # 4 = setuid bit

You'd expect non-root users running check-logs.py to read the file with root's permissions. They cannot. Linux ignores the setuid bit on scripts (since the early 1990s) because the kernel's race between checking the setuid bit and the interpreter opening the file was unfixable in the presence of #!/usr/bin/env-style indirections.

The correct fix is one of: (a) wrap the script in a tiny C program that is itself setuid root; (b) grant sudo-rules to specific users running specific commands; or (c) use POSIX capabilities (setcap cap_dac_read_search+ep) to give the binary only the specific privilege it needs. The third option is now standard practice in modern Linux — set the file's individual capability instead of giving it the entire root identity.

Continue exploring

Tags