Back to the map

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.

CoreMust-knowJunior 12 min Updated Nov 2026

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.

Process credentialsuid 1001 · gid 1001 · groups · CapEffFile inodeowner 0 · group 33 · mode 0640 · ACLKernel check on open()CAP_DAC_OVERRIDE? else owner bits, else ACL or group bits, else other bitsallowed, or -EACCES
One permission check: a bypassing capability first, then exactly one class of bits (owner, ACL or group, other).

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.

MechanismGranularityWho sets itTypical use
Mode bitsOwner, group, other; rwx eachOwner or root, chmodAlmost every file
ACLsNamed users and groups per fileOwner or root, setfaclOne extra reader without a new group
setuid / setgidWhole program runs as the file ownerRoot marks the binarypasswd, sudo, shared directories
CapabilitiesOne privileged operationRoot via setcap, systemd or container specBind port 443 without root
Capabilities are the only one that shrinks root itself.

Hardening a service user

  1. 1

    Dedicated system user

    useradd --system --no-create-home --shell /usr/sbin/nologin app.
  2. 2

    Own only what it writes

    Code and config root:root, so a compromised process cannot rewrite its own binary. Data app:app 0750. Secrets root:app 0640.
  3. 3

    Low port without root

    Grant CAP_NET_BIND_SERVICE, set net.ipv4.ip_unprivileged_port_start=0, or let systemd open the socket.
  4. 4

    Tighten the unit

    UMask=077, PrivateTmp=yes, ProtectSystem=strict, NoNewPrivileges=yes; the last stops setuid binaries raising the process back to root.
  5. 5

    Containers

    USER app in the Dockerfile; in Kubernetes runAsNonRoot: true and capabilities.drop: [ALL].
  6. 6

    Verify

    CapEff: 0000000000000000 in /proc/PID/status and a cat /etc/shadow that 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?

Users, permissions and least privilege · Backplane