// linux fundamentals — module 04

Module 4 — Users, Groups, and Permissions Under the Hood

Thesis: Linux security day-to-day is one small, elegant model: every process runs as a numeric user, every inode carries an owner and nine permission bits, and the kernel runs the same short checking procedure on every single access. Learn the actual procedure — not just chmod 755 folklore — and "permission denied" stops being weather and becomes physics.

Prerequisite: Module 3 — Everything Is a File — permissions live in the inode, and you know what an inode is.


4.1 Users are numbers

To the kernel, a user is a number: the UID (user ID). Groups likewise: GIDs. Names like kroma are a userland convenience — a lookup table maps names to numbers for display and login, and the kernel never sees the names at all. (Same names-vs-identity moral as inodes and UUIDs; it's a pattern now.)

Where the identity actually sits: every process runs as some UID (plus a primary GID and a list of supplementary groups). It's part of the process bookkeeping from §2.5. When you log in, the login machinery verifies your password and starts your shell with your UID; every program you launch inherits it (inheritance mechanics in Module 5). So "you" on a Linux system is the UID your processes carry — nothing more.

Three kinds of users on every system:

  • root, UID 0 — special-cased in the kernel: for UID 0, most permission checks are simply skipped (§4.8).
  • Regular users — humans, usually UID 1000 and up.
  • System users — non-human accounts (www-data, nobody, systemd-network…) that exist so services can run with minimal power. This is the security payoff of the whole model: a web server running as www-data that gets hacked can only do what www-data can do — which is almost nothing. Damage containment by UID.

4.2 The account databases: three files in /etc

User accounts are plain text files in /etc (§3.2 promised you'd be reading it). Look at each:

/etc/passwd — one line per user, seven colon-separated fields:

kroma:x:1000:100:Kroma:/home/kroma:/run/current-system/sw/bin/zsh

name : password-placeholder : UID : primary GID : comment : home directory : login shell. Readable by everyone (lots of tools need name↔︎UID lookups). The x means "password stored elsewhere" —

/etc/shadow — the actual password data, one line per user, readable only by root. Passwords are stored as salted hashes: a one-way mathematical fingerprint. Logging in re-hashes what you type and compares fingerprints; nobody — including root — can read a password back out. (Historically hashes lived in world-readable passwd; offline cracking is why they moved to shadow. The name stuck.)

/etc/group — group name : GID : member list. A user has one primary group (the GID field in passwd — new files get it, §4.4) plus any number of supplementary groups from this file. Groups exist to grant access to categories of things: membership in wheel (or sudo) gates sudo, video gates the GPU device files, docker gates the Docker socket — that last one is famously equivalent to root, an assertion you'll be able to evaluate yourself by the end of this module.

Run id and read your whole identity: uid=1000(kroma) gid=100(users) groups=100(users),1(wheel)….

4.3 The nine bits (plus three)

Now the other half. Every inode (§3.4) stores its owner UID, its group GID, and a permission field of twelve bits — nine main ones, in three triads:

-  rwx  r-x  r--
│   │    │    └── other (everyone else)
│   │    └────── group (processes in the file's group)
│   └─────────── user  (the file's owner)
└── file type (Module 3)

For a regular file: r = read the contents · w = modify them · x = execute it as a program (this bit is the only thing that makes something a "command" — nothing about the filename, no extension).

For a directory — recall §3.4: a directory is a table of name→inode entries; the meanings follow from that:

  • rlist the table (read the names)
  • wedit the table: create, delete, rename entries — see the gotcha below
  • xpass through: use the directory in a path, reach the inodes behind its entries. Called the search bit.

The classic gotchas, resolved by the table model: a directory with r but not x lets you see names but touch nothing behind them; with x but not r, you can reach files inside if you already know their names — a deliberate technique, not a bug. And most surprising to newcomers: deleting a file requires w on the directory, not on the file — deletion is unlink (§3.4), an edit to the table. The file's own bits are irrelevant to its deletion.

4.4 Reading ls -l, and octal

You can now read every column of ls -l:

-rw-r--r--  1  kroma  users  4523  Jul 11 12:03  notes.md
    │       │    │      │      │        │           └ name
    │       │    │      │      │        └ last-modified time
    │       │    │      │      └ size (bytes)
    │       │    │      └ group (GID, shown as a name)
    │       │    └ owner (UID, shown as a name)
    │       └ hard-link count (§3.4)
    └ type + nine bits

Octal notation. Each triad is three bits; read them as a binary number: r=4, w=2, x=1, added. So rwx=7, rw-=6, r-x=5, r--=4. Three triads → three digits:

  • 755 = rwxr-xr-x — owner everything; everyone else read/execute. The standard for programs and directories.
  • 644 = rw-r--r-- — owner read/write; everyone else read. The standard for ordinary files.
  • 600 = rw------- — owner only. Secrets (SSH keys demand this).
  • 700 = rwx------ — a private directory.

Practice until conversion is instant — both directions. It's a lookup table with eight entries; you'll use it for life.

Changing things:

chmod 644 file             # octal: set all nine bits at once
chmod u+x script.sh        # symbolic: u/g/o/a  +/-/=  r/w/x — surgical single-bit edits
chmod -R g+w shared/       # recursive
chown alice file           # change owner (root only — otherwise you could gift files away)
chown alice:devs file      # owner and group together
chgrp devs file            # group only (owner may, into groups they belong to)

Where do a new file's bits come from? Your umask — a per-process mask of bits to withhold from new files. Default 022 withholds group-write and other-write: new files arrive 644, new directories 755. Run umask to see yours; set umask 077 and everything you create is private by default. (Files also arrive owned by your UID and your primary GID — §4.2's fields, at work.)

4.5 How the kernel actually decides

The center of the module. On every access — every open, every unlink, every execve (§2.2: syscalls are where checks happen; there is no other path, so there is no way around them) — the kernel runs this exact procedure with the process's UID/GIDs and the inode's owner/group/bits:

  1. UID 0? Permission granted (execute needs at least one x bit somewhere). Done.
  2. Process UID == inode's owner? Use the owner triad. Done — the other triads are never consulted.
  3. Else, any of the process's GIDs == inode's group? Use the group triad. Done.
  4. Else use the other triad.

The crucial phrase is first match wins, then stop. This produces the model's one famous counterintuitive corner: file ----rw-rw- owned by you is unreadable by you — you matched the owner triad, it says no, and the kernel never looks further. Owner match isn't a privilege; it's a routing decision.

Also note what's not in the procedure: filenames, file extensions, "administrator prompts," heuristics. Nine bits, two IDs, four steps. When you hit "Permission denied," the debugging recipe is mechanical: id (who am I?), ls -l on the file and ls -ld on every directory in the path (x needed on each — §4.3), walk the four steps. The answer is always in there.

4.6 The three special bits: setuid, setgid, sticky

The twelfth-through-tenth bits solve three real problems, and two of them you've already brushed against:

setuid (4xxx, shown as s in the owner triad's x slot). Problem: you change your password with passwd, which must write /etc/shadow — root-only (§4.2). How can your unprivileged process do that? Because /usr/bin/passwd has the setuid bit: when executed, the process runs with the file owner's UID — root — regardless of who launched it. The program itself is trusted to only touch your own shadow entry.

Understand this bit deeply, because it's double-edged: setuid-root binaries are deliberate, permanent doors through the entire permission model, and therefore the first place attackers look. Every one must be short, paranoid, and audited; a setuid binary with any bug (or a setuid shell script — forbidden by the kernel for this reason) is a privilege-escalation gift. Finding forgotten setuid binaries is a standard security-audit move — you'll do the find in the lab.

setgid (2xxx, s in the group triad). On a file: like setuid but for the group. On a directory, something different and very useful: new files created inside inherit the directory's group instead of the creator's primary group — how shared team directories stay shared.

sticky (1xxx, t in the other triad, on directories). §3.2 promised the /tmp trick: /tmp is writable by everyone (w on the directory = anyone can delete any entry, §4.3 — disaster in a shared dir). Sticky bit: in this directory, you may only delete entries you own. ls -ld /tmpdrwxrwxrwt. That final t is the whole story.

4.7 root, and sudo

root = UID 0 = step 1 of §4.5: file permissions simply don't apply. Plus the other restricted actions — binding low network ports (Module 8), mounting (Module 3), loading kernel modules (Module 2), chown — are gated on UID 0 (technically on capabilities, §4.9). Root is not a "mode" or a special program; it's a number that short-circuits checks.

Two ways to wield it:

  • Log in as root (su -, or directly): a root shell, every command privileged, no logging, easy to forget which window is loaded. Discouraged.
  • sudo cmd: run one command as root, authenticating as yourself. sudo checks /etc/sudoers (edit only via visudo, which syntax-checks — a broken sudoers can lock everyone out of root) for whether you're allowed — typically via wheel/sudo group membership (§4.2). Advantages: per-command logging, fine-grained policy (specific users → specific commands), and no shared root password.

How does sudo, run by plain you, become root? Look at ls -l $(which sudo): it's setuid-root. The special bit from §4.6 is the mechanism under the everyday tool. The model is closed — every piece explains the others.

4.8 Capabilities: slicing up root

"All-or-nothing" is the classic model's weakness: a program that only needs to bind port 80 shouldn't get all of root. Modern Linux therefore splits root's power into ~40 named capabilities: CAP_NET_BIND_SERVICE (low ports), CAP_NET_RAW (raw sockets — ping pings this way), CAP_SYS_ADMIN (the infamous grab-bag, "half of root in one flag"), CAP_CHOWN, CAP_KILL, …

Capabilities can be attached to files (like a surgical setuid — getcap /usr/bin/ping may show cap_net_raw+ep on non-NixOS distros) and are held per-process (grep Cap /proc/$$/status — §2.7 knowledge, reused). Being root = holding all of them; the point is granting one instead. Modern service managers (Module 7: AmbientCapabilities= in systemd units) and container runtimes speak capabilities natively, so you'll meet them again.

For completeness — beyond this course but worth naming so you're never surprised: Linux also supports ACLs (per-file lists granting bits to arbitrary extra users/groups — getfacl; marked by a + after the nine bits in ls -l) and mandatory-access-control systems (SELinux, AppArmor) that add another checking layer after §4.5's procedure. On your own machines, the nine-bit model plus capabilities is 99% of daily reality.

4.9 → NixOS

Three connections, each now transparent:

  • The Nix store is read-only, owned by root. /nix/store contents arrive with r-xr-xr-x-style bits and no write access for anyone — using exactly this module's mechanism to guarantee that installed software is immutable. "Packages can't be tampered with or drift" is just §4.5, deployed as architecture.
  • Users are declared. Instead of imperative useradd mutating /etc/passwd, you write users.users.kroma = { isNormalUser = true; extraGroups = [ "wheel" ]; } and NixOS generates the passwd/group entries. Knowing what those files contain (§4.2) is what makes the declaration meaningful — you know exactly what it produces.
  • Setuid needs special handling. Store files can't be setuid (immutable, remember) — so NixOS builds /run/wrappers/bin/, a directory of generated setuid wrapper binaries (passwd, sudo…) declared via security.wrappers. When you meet it, it won't be a mystery — it'll be §4.6, relocated on purpose.

Lab 4

  1. Read your identity, and the databases. Run id. Then find your line in /etc/passwd and name all seven fields out loud. Find your groups in /etc/group. Try cat /etc/shadow (denied — good), then sudo cat /etc/shadow | head -3 and confirm: hashes, not passwords. Check ls -l /etc/shadow and explain its permissions with §4.5.

  2. Predict, then verify. In a scratch directory, for each of: chmod 640 f, chmod 711 f, chmod u-r f, chmod o+w fwrite down the expected ls -l string first, then run and check. Do not continue until you're 4-for-4 twice in a row.

  3. Experience the owner-match trap. touch mine; chmod 044 mine; cat mine — denied, on your own file readable by everyone else. Recite the four steps to explain it. Fix it.

  4. Directory bits, all three. Make dir/secret.txt with some content, then experiment: chmod 600 dir (list but not enter — try ls dir vs cat dir/secret.txt), chmod 100 dir (enter but not list — ls dir fails, yet cat dir/secret.txt works!), then chmod 500 dir and try rm dir/secret.txt — denied despite the file being yours, because deletion edits the table. Restore 755 and reflect: all four results follow from "a directory is a table."

  5. Hunt setuid binaries like an auditor.

    find /usr/bin -perm -4000 -ls 2>/dev/null

    (On NixOS: ls -l /run/wrappers/bin instead.) For each hit — expect passwd, sudo, su, mount — say why it needs to cross the privilege boundary. Confirm the s in each listing. Then ls -ld /tmp and find the t.

  6. Watch the kernel say no. Combine Module 2 with this one:

    strace -e openat cat /etc/shadow

    Find the line: openat(... "/etc/shadow" ...) = -1 EACCES (Permission denied). That's §4.5 executing, visible. The error you'll debug for the rest of your life, caught in the act.

  7. umask. Run umask, create a file and a directory, verify their bits match the prediction (666−mask for files, 777−mask for dirs). Set umask 077, repeat, verify. New shell — what's the umask now, and why (Module 1 §1.8)?


✅ Mastery Check — do not proceed until true

Answer out loud, without notes:

  1. What is a UID, where does a process get one, and why do system users like www-data exist?
  2. Name the seven fields of an /etc/passwd line. Why are hashes in /etc/shadow instead, and why can't even root recover a password from them?
  3. Recite the kernel's four-step permission check exactly, including what "first match wins" causes for a 044 file you own.
  4. Convert instantly, both directions: 755, 644, 600, 750, rwxr-x---, rw-r-----.
  5. What do r, w, x each mean on a directory? Why does deleting a file not require write permission on the file?
  6. Explain setuid using passwd end-to-end, and why setuid binaries are prime security targets. What do setgid and sticky do on directories, and which one is on /tmp?
  7. What actually makes root powerful — in terms of the checking procedure? Give two reasons sudo beats a root shell, and explain how sudo itself obtains root power.
  8. What problem do capabilities solve? Give one concrete capability and its use.

And perform cold:

  • Debug any "Permission denied" mechanically: id, ls -l, ls -ld on each path component, four steps.
  • Set any permission in both octal and symbolic form, predicting ls -l before running.
  • Find every setuid binary on a system with one command.

When all of that is effortless: Module 5 — Processes, Signals, and IO