Lesson 3 of 9 · 6 min read
The Linux Privilege Model: Permissions, setuid, Capabilities and MAC
In this lesson
TL;DR: Four layers decide what a Linux process may do. Discretionary permissions: owner, group and other bits (plus ACLs) set by each file's owner. Identity changes: setuid and setgid executables run as their owner, so each is privileged code reading attacker input. Capabilities: root split into separate powers; a non-root process can hold dangerous ones, and
CAP_SYS_ADMINis close to root. Mandatory access control: SELinux or AppArmor policy that even a compromised owner or root in a confined domain cannot change. Least privilege for a service means working all four.
Before you read
Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.
1A directory is mode 777 with no sticky bit. You own a file inside it with mode 600. Can another user delete your file?
Layer 1: discretionary permissions
Each file has an owner, a group and nine bits. The kernel picks one set: owner bits if the process's effective user ID owns the file, otherwise group bits if any of its groups match, otherwise the other bits. The sets do not combine, so a file can deny its owner while allowing strangers.
On directories the bits mean different things: read lists names, write creates and removes entries, execute allows traversal. Root bypasses these checks, and the owner can always change them, which is why they are called discretionary: whoever controls the owner's account controls the rules.
Layer 2: setuid and setgid
A setuid executable runs with an effective user ID equal to the file's owner. That is how passwd lets you change your own password in a file only root can write. The kernel tracks three IDs per process: real (who started it), effective (used for permission checks) and saved (lets a program switch back). A setuid program that drops privileges must change all three, or it can regain root later.
Every setuid-root binary is root code that reads attacker-controlled arguments, environment variables and files, so bugs in them are privilege escalations. Linux ignores setuid on scripts, and in setuid programs the dynamic loader runs in secure-execution mode: it ignores preload entries containing a slash and only preloads setuid-marked libraries from the standard system directories, which closes the classic LD_PRELOAD attack. no_new_privs, available since Linux 3.5, lets a process opt out of gaining privilege entirely: once set it cannot be unset, it is inherited by children and preserved across exec, and setuid bits and file capabilities have no effect for that process tree. Sandboxes and systemd's NoNewPrivileges=yes rely on it.
Layer 3: capabilities
Historically the kernel asked "is this UID 0?". Capabilities let it ask a narrower question for each privileged operation:
| Capability | Lets a process... | Risk |
|---|---|---|
CAP_NET_BIND_SERVICE | Bind ports below 1024 | Low |
CAP_DAC_OVERRIDE | Ignore file permission checks | Reads and writes any file |
CAP_SYS_PTRACE | Debug any process | Injects code into privileged processes |
CAP_SYS_MODULE | Load kernel modules | Kernel code execution |
CAP_SYS_ADMIN | Mount, many namespace and admin operations | Overloaded; close to root |
Capabilities live in per-thread sets. The bounding set is the ceiling a process tree can never exceed, so dropping from it is durable. Ambient capabilities let a non-root service keep a single capability across exec. Files can carry capabilities, a narrow replacement for setuid root.
The practical rule: drop everything, add back the one capability an operation needs, and prefer a design change (a socket passed in by the init system, a broker process) over a broad grant.
Some numbers and history make the model concrete. The kernel defines 41 capabilities, numbered 0 to 40; the newest were split out of CAP_SYS_ADMIN because it had become a second root: CAP_PERFMON and CAP_BPF in Linux 5.8, CAP_CHECKPOINT_RESTORE in 5.9. The capabilities man page puts it bluntly, advising against choosing CAP_SYS_ADMIN if you can possibly avoid it. File capabilities arrived in 2.6.24, the per-thread bounding set in 2.6.25 and the ambient set in 4.3. Inside a user namespace, a process can hold a full set of capabilities, but they apply only to resources that namespace governs: loading kernel modules or setting the system clock still needs privilege in the initial namespace.
Worked example: reading a process's real privilege
A service runs as the non-root user app. Its /proc/<pid>/status shows:
| Line | Value | Decoded |
|---|---|---|
CapEff | 0000000000200000 | Only bit 21 set (0x200000 = 2^21): CAP_SYS_ADMIN. Close to root despite the UID |
CapBnd | 000001ffffffffff | 2^41 − 1: all 41 capabilities. Nothing dropped from the ceiling |
NoNewPrivs | 0 | Setuid binaries and file capabilities can still raise privilege |
The fix in a systemd unit: an empty CapabilityBoundingSet=, NoNewPrivileges=yes, and, only if the service binds port 443, AmbientCapabilities=CAP_NET_BIND_SERVICE (bit 10). Re-reading should show CapEff: 0000000000000400 and NoNewPrivs: 1. If the service needed CAP_SYS_ADMIN only to load eBPF programs, Linux 5.8 and later let you grant CAP_BPF (plus CAP_PERFMON for tracing) instead.
- 1
1 · User ID
app, not root
- Looks unprivileged
- Says little on its own
Check groups and capabilities next
- 2
2 · Effective capabilities
CapEff
- Before: bit 21, CAP_SYS_ADMIN
- After: bit 10 only, bind port 443
Decides what privileged calls succeed
- 3
3 · Bounding set
CapBnd
- Before: all 41
- After: empty or the one needed
The ceiling for everything it executes
- 4
4 · no_new_privs
NoNewPrivs
- Before: 0, setuid still works
- After: 1, cannot gain privilege
Irreversible and inherited
- 5
5 · Mandatory access control
SELinux or AppArmor profile
- Confines even root in the process
- Denials are signals, not noise
Survives a compromise of the process
The UID misleads
A non-root UID with CAP_SYS_ADMIN is close to root.
Layer 4: mandatory access control
MAC adds a check after the discretionary one; both must pass, and the policy belongs to the administrator, not the process.
- SELinux labels every process and object (for example
httpd_tandhttpd_sys_content_t) and allows only interactions the policy lists. A web server in its domain cannot read home directories or run a shell from/tmp, even as root. Labels travel with files, and multi-category labels keep containers apart. - AppArmor attaches a profile to a program path listing the files, capabilities and network access it may use. Simpler to write; path aliases (hard links, bind mounts) need care.
When MAC blocks something, read the denial, decide whether the access is intended, and fix it narrowly (relabel, enable a specific boolean, add one rule). Turning MAC off removes the containment for the next compromise.
Protecting: Root on the host
- 1
1 · Dedicated user, tight permissions
nothing writable outside its data
- Stops: Rewrites another service's config
- 2
2 · no_new_privs
setuid no longer elevates
- Stops: Abuses a setuid binary
- 3
3 · Empty capability bounding set
no CAP_SYS_ADMIN, no CAP_SYS_MODULE
- Stops: Uses a broad capability to mount or load a module
- 4
4 · SELinux or AppArmor profile
policy the process cannot change
- Stops: Spawns a shell and reads keys
Worked example: a web service's effective privilege
A service runs as app, not root. Before calling it low-privilege, check:
CapEffin/proc/<pid>/status: does it holdCAP_SYS_ADMINorCAP_DAC_OVERRIDE?- Groups: is
appindocker(the Docker socket is root) ordisk? sudo -l -U app: any rule allowing an editor, interpreter or writable script?- Writable paths: can
appwrite a script root's cron runs, or a directory in a root service's library path? - MAC: is the service confined, or running unconfined?
Any "yes" in steps 1 to 4 means the service is effectively root after compromise. A hardened systemd unit (NoNewPrivileges=yes, empty CapabilityBoundingSet=, ProtectSystem=strict, a system call filter) plus a MAC profile turns each answer into "no".
Misconceptions interviewers listen for
- "Non-root means low privilege." Groups such as
dockerordisk, capabilities and sudo rules decide. - "Owner, group and other permissions combine." Exactly one set applies: owner if you own the file, otherwise group if you are in it, otherwise other.
- "Dropping the effective UID is enough." Real, effective and saved IDs must all change, or the process can switch back.
- "Set SELinux to permissive to fix the denial." That removes containment for the next compromise; fix the policy for the specific access instead.
- "Root in a user namespace is real root." Its capabilities apply only to resources that namespace governs.
What if · interview follow-ups
Hold your answer as the constraints change
An attacker has code execution in a web service running as the user app on a Linux host. You are asked how bad it is and how to make the next one less bad.
Start here
What if the team says it is fine because the service is not root?
What else decides privilege besides the UID?
Check effective and bounding capabilities in /proc, group memberships such as docker that are root-equivalent, sudo rules, writable files that root later executes, the mandatory access control profile and NoNewPrivs. Any one of those can make app nearly as powerful as root.
Carry it forwardPrivilege is the sum of UID, groups, capabilities, sudo rules and confinement, not the UID alone.
Weak answerStopping the assessment at 'not root'.
Constraint changes
What if it holds only CAP_NET_BIND_SERVICE?
What does that capability allow?
Binding ports below 1024, which is low risk on its own. Even so, socket activation can remove it entirely: the init system binds the port and passes the socket in, so the process never holds the capability.
Carry it forwardPrefer designs that remove a capability over granting even a small one.
Weak answerTreating any capability as a critical finding.
Constraint changes
What if it holds CAP_SYS_ADMIN because it loads eBPF programs?
What did Linux 5.8 change?
CAP_SYS_ADMIN is overloaded and close to root: it allows mounts, namespace operations and much more. On Linux 5.8 and later, grant CAP_BPF, and CAP_PERFMON if tracing is needed, instead, and empty the bounding set of everything else.
Carry it forwardUse the narrow capabilities split out of SYS_ADMIN where the kernel offers them.
Weak answerKeeping SYS_ADMIN because it is the documented requirement.
Constraint changes
What if a vendor ships a setuid-root helper you cannot remove?
How do you limit who can run it and what it can do?
Mount other filesystems nosuid, confine the helper with a mandatory access control profile, set no_new_privs on services that never need it so they cannot benefit from it, restrict who can execute it, and track its vulnerabilities. Ask the vendor for a file-capability or broker design.
Carry it forwardEvery setuid-root binary is root code reading attacker input; contain what you cannot remove.
Weak answerAccepting it without compensating controls.
Constraint changes
What if the compromised process is root inside a container?
Is there a user namespace?
Without a user namespace it is UID 0 on the host behind the container's other boundaries, so a kernel bug or a dangerous mount means host compromise. With a user namespace it maps to an unprivileged host user, though unprivileged user namespaces also expose more kernel code to attackers, which is why some platforms restrict them. Run containers as non-root, with user namespaces, dropped capabilities and seccomp.
Carry it forwardContainer root is host root unless a user namespace remaps it.
Weak answerAssuming the container makes root harmless.
Now defend it
As the defender, what would you monitor on Linux hosts for privilege abuse?
Processes running with unexpected effective capabilities or NoNewPrivs off, new setuid or file-capability binaries, mandatory access control denials (as signals, not noise), changes to sudoers and to membership of root-equivalent groups, and ptrace or module-loading activity from services.
Primary references
References for the mechanisms discussed above, accessed 6 November 2026:
How this shows up in interviews
Questions such as "how do Unix permissions work?", "what are capabilities?", "SELinux or AppArmor?" and "a non-root service is compromised; can the attacker get root?" all draw on this model. Interviewers check the single-set evaluation rule, directory semantics, real versus effective IDs, why CAP_SYS_ADMIN is dangerous, and whether you know both checks must pass under MAC.
Say it in 30 seconds: "Linux privilege has four layers. Discretionary permissions set by owners, which a compromised owner can change. setuid binaries that run as their owner and are escalation surfaces. Capabilities that split root, where some, like CAP_SYS_ADMIN, are nearly root anyway. And MAC, SELinux or AppArmor, whose policy the process can't change. For a service I drop all capabilities, set no_new_privs, give it a dedicated user with no risky groups, and confine it with a MAC profile."
Check yourself
Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.
1A setuid-root program calls seteuid(getuid()) to drop privileges and then processes user input. What is wrong?
2SELinux denies a web server reading a new content directory. What is the right fix?
3A non-root service shows CapEff 0000000000200000. What does it hold?
4What does setting no_new_privs guarantee?