Module 09 · Security and protection · Lesson 1 of 3
Users, permissions and least privilege
How the kernel answers 'may this process do that?': UIDs and GIDs, mode bits and umask, ACLs, setuid and why passwd needs it, root, sudo and capabilities, and least privilege applied to a real service user.
Why it matters
A web server that must listen on port 443, a worker that must read one secrets file, a CI runner that must not read the next job's deploy key: each is a permissions decision, and the wrong one turns a small bug into a full compromise. Postgres refuses to start as root for exactly that reason.
The idea
Every process carries credentials and every object carries an owner, a group and a mode. On every open, execve, bind or unlink, the kernel compares the two and answers yes or EACCES.
Identities: UIDs and GIDs
A user ID (UID) and a group ID (GID) are small integers. The kernel knows only the numbers; names live in /etc/passwd and /etc/group, so a mounted volume may show owner 1001 with no name. A process has a primary GID, supplementary groups, a real UID (who started it) and an effective UID (what the kernel checks); they differ only after setuid.
Mode bits
Nine bits, read, write and execute for owner, group and other, written rwxr-x--- or 0750. The kernel tests one class only: if the effective UID is the owner, the owner bits decide, even when the group bits would grant more. On a directory the letters change meaning: r lists names, x traverses it and looks up an inode by name, w creates, deletes and renames entries (and needs x). The sticky bit on /tmp (1777) lets everyone create files but only owners delete their own.
umask
New files are requested with 0666 and directories with 0777; the process's umask removes bits. The usual 022 yields 0644 and 0755; a service handling secrets wants 077. umask is inherited across fork and execve, so a lax value in a unit file leaks into everything the service writes.
ACLs
Nine bits cannot say "the deploy user may read these logs too". Access control lists add named entries: setfacl -m u:deploy:rx /var/log/app grants one extra user, getfacl prints the list, and ls -l marks the file with +. A default ACL on a directory is inherited by new entries.
setuid, setgid and why passwd needs it
/etc/shadow holds password hashes and is 0640, owned by root, yet any user can change their own password. passwd carries the setuid bit (-rwsr-xr-x root): when executed, the effective UID becomes the file's owner for the life of that program, and the program itself ensures you edit only your own line. setgid on a directory makes new files inherit its group, for shared team directories. Every setuid binary is a privilege boundary (the 2022 pkexec CVE lived in one), so distributions keep the set small and give ping a file capability instead.
root, sudo and capabilities
root is UID 0, and the kernel skips discretionary checks for it. sudo is a setuid-root binary whose policy lives in /etc/sudoers. Linux capabilities split root's power into about forty flags so a program can hold one without the rest: CAP_NET_BIND_SERVICE binds ports below 1024, CAP_NET_RAW opens raw sockets, CAP_DAC_OVERRIDE bypasses the mode bits; CAP_SYS_ADMIN is the catch-all and is close to root again. Capabilities attach to a file (setcap cap_net_bind_service=+ep /usr/local/bin/app), to a systemd unit (AmbientCapabilities=) or to a container spec. Docker starts containers with fourteen; --cap-drop ALL removes those too.
Discretionary access control
All of this is discretionary access control (DAC): the owner decides, and a process with the owner's identity may hand the object to anyone. A compromised process running as the application user can read every file that user owns and chmod them wider, which is why the next lesson adds controls the owner cannot relax.
| Mechanism | Granularity | Who sets it | Typical use |
|---|---|---|---|
| Mode bits | Owner, group, other; rwx each | Owner or root, chmod | Almost every file |
| ACLs | Named users and groups per file | Owner or root, setfacl | One extra reader without a new group |
| setuid / setgid | Whole program runs as the file owner | Root marks the binary | passwd, sudo, shared directories |
| Capabilities | One privileged operation | Root via setcap, systemd or container spec | Bind port 443 without root |
Hardening a service user
- 1
Dedicated system user
useradd --system --no-create-home --shell /usr/sbin/nologin app. - 2
Own only what it writes
Code and configroot:root, so a compromised process cannot rewrite its own binary. Dataapp:app 0750. Secretsroot:app 0640. - 3
Low port without root
GrantCAP_NET_BIND_SERVICE, setnet.ipv4.ip_unprivileged_port_start=0, or let systemd open the socket. - 4
Tighten the unit
UMask=077,PrivateTmp=yes,ProtectSystem=strict,NoNewPrivileges=yes; the last stops setuid binaries raising the process back to root. - 5
Containers
USER appin the Dockerfile; in KubernetesrunAsNonRoot: trueandcapabilities.drop: [ALL]. - 6
Verify
CapEff: 0000000000000000in/proc/PID/statusand acat /etc/shadowthat fails.
Trade-offs
Least privilege costs operability. Things fail with EACCES at 2 a.m., and the quickest fix, chmod -R 777 or "just run it as root", is how privilege creeps back. CAP_SYS_ADMIN is nearly root, and a setuid binary left in the image undoes the scheme unless no_new_privs is set. And DAC protects nothing from the owner: a confused deputy has every right the application user has.
In an interview
Bring it up when a design has any service touching user data: "The API runs as a dedicated user with no shell, reads secrets from a root-owned file it cannot modify, and binds 443 through CAP_NET_BIND_SERVICE rather than running as root." On passwd: "It is setuid root, so the effective UID becomes 0 for that program only, and the program enforces that you edit your own line."
Check yourself
A containerised Go service must listen on port 443 directly. It runs as UID 1001 and the bind fails with 'permission denied'. What is the right fix?