// 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 755folklore — 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 aswww-datathat gets hacked can only do whatwww-datacan 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:
- r — list the table (read the names)
- w — edit the table: create, delete, rename entries — see the gotcha below
- x — pass 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:
- UID 0? Permission granted (execute needs at least one x bit somewhere). Done.
- Process UID == inode's owner? Use the owner triad. Done — the other triads are never consulted.
- Else, any of the process's GIDs == inode's group? Use the group triad. Done.
- 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 /tmp → drwxrwxrwt. 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.sudochecks/etc/sudoers(edit only viavisudo, which syntax-checks — a broken sudoers can lock everyone out of root) for whether you're allowed — typically viawheel/sudogroup 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/storecontents arrive withr-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
useraddmutating/etc/passwd, you writeusers.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 viasecurity.wrappers. When you meet it, it won't be a mystery — it'll be §4.6, relocated on purpose.
Lab 4
Read your identity, and the databases. Run
id. Then find your line in/etc/passwdand name all seven fields out loud. Find your groups in/etc/group. Trycat /etc/shadow(denied — good), thensudo cat /etc/shadow | head -3and confirm: hashes, not passwords. Checkls -l /etc/shadowand explain its permissions with §4.5.Predict, then verify. In a scratch directory, for each of:
chmod 640 f,chmod 711 f,chmod u-r f,chmod o+w f— write down the expectedls -lstring first, then run and check. Do not continue until you're 4-for-4 twice in a row.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.Directory bits, all three. Make
dir/secret.txtwith some content, then experiment:chmod 600 dir(list but not enter — tryls dirvscat dir/secret.txt),chmod 100 dir(enter but not list —ls dirfails, yetcat dir/secret.txtworks!), thenchmod 500 dirand tryrm dir/secret.txt— denied despite the file being yours, because deletion edits the table. Restore755and reflect: all four results follow from "a directory is a table."Hunt setuid binaries like an auditor.
find /usr/bin -perm -4000 -ls 2>/dev/null(On NixOS:
ls -l /run/wrappers/bininstead.) For each hit — expectpasswd,sudo,su,mount— say why it needs to cross the privilege boundary. Confirm thesin each listing. Thenls -ld /tmpand find thet.Watch the kernel say no. Combine Module 2 with this one:
strace -e openat cat /etc/shadowFind 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.umask. Run
umask, create a file and a directory, verify their bits match the prediction (666−mask for files, 777−mask for dirs). Setumask 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:
- What is a UID, where does a process get one, and why do system users like
www-dataexist? - Name the seven fields of an
/etc/passwdline. Why are hashes in/etc/shadowinstead, and why can't even root recover a password from them? - Recite the kernel's four-step permission check exactly, including what "first match wins" causes for a
044file you own. - Convert instantly, both directions:
755,644,600,750,rwxr-x---,rw-r-----. - What do r, w, x each mean on a directory? Why does deleting a file not require write permission on the file?
- Explain setuid using
passwdend-to-end, and why setuid binaries are prime security targets. What do setgid and sticky do on directories, and which one is on/tmp? - What actually makes root powerful — in terms of the checking procedure? Give two reasons
sudobeats a root shell, and explain howsudoitself obtains root power. - What problem do capabilities solve? Give one concrete capability and its use.
And perform cold:
- Debug any "Permission denied" mechanically:
id,ls -l,ls -ldon each path component, four steps. - Set any permission in both octal and symbolic form, predicting
ls -lbefore running. - Find every setuid binary on a system with one command.
When all of that is effortless: Module 5 — Processes, Signals, and IO