Virtualization · Sandboxing
How Capability Sets Split Root Authority
The five capability sets a Linux process carries, the transitions between them at exec, and why dropping a capability is not the same as dropping a file permission.
The traditional Unix model gives a process exactly one bit of privilege: the effective UID is either 0 or it is not. That bit is too coarse to be useful in a sandbox and too coarse to be safe outside one, because a program that needs to bind a port below 1024 receives the same authority as a program that can load a kernel module. Linux capabilities replace the single bit with a set of named permissions, and replace the single check with a set of sets that transform at exec time.
The five sets and what each one means
A process carries five capability sets, and the distinction between them is the whole model:
- Permitted (
P'(permitted)) is the ceiling. A capability must be in this set for the process to be able to raise it into Effective. It is the authority the process has available, whether or not it is currently using it. - Effective (
P'(effective)) is what the kernel actually checks. Every capability check in the kernel tests the Effective set, with a small number of exceptions that test Permitted. - Inheritable (
P'(inheritable)) survivesexecveand participates in the file-capability computation. It is rarely used, because file capabilities superseded it for the purpose it was designed for. - Bounding (
P'(bounding)) is a mask that limits which capabilities a process can ever gain, including through a set-user-ID exec or a file capability. A capability dropped from Bounding cannot be regained by any means short of a new user namespace, where the bounding set starts full again. - Ambient (
P'(ambient)) is raised byPR_CAP_AMBIENT_RAISEand makes capabilities present in both Permitted and Inheritable survive an exec of a non-privileged binary, so a program does not lose its capability merely by executing a helper.
The Effective set is the one that matters for enforcement. A process holding CAP_NET_BIND_SERVICE in Permitted but not Effective cannot bind a privileged port; it must call capset to raise the bit first, which succeeds because the bit is in Permitted. A sandbox that wants to prevent an operation while leaving it available for a specific moment manipulates the Effective set; a sandbox that wants to prevent it permanently manipulates Bounding.
Exec recomputes everything
At execve, the kernel computes new sets from the old ones and from the file’s metadata. For a file with no capabilities and no set-user-ID bit, the rule is:
P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable)
P'(bounding) = P(bounding)
For a root-owned file with the set-user-ID bit set, the special case applies: the file capability sets F(inheritable) and F(permitted) are notionally full, so the new Permitted becomes P(inheritable) | P(bounding) and Effective becomes that same value. Bounding is what keeps this bounded, and it is why capsh --drop and the PR_CAPBSET_DROP operation are the correct way to remove a capability permanently rather than clearing Permitted.
The consequence for sandboxing is direct: clearing Permitted is not enough, because a set-user-ID-root binary executed later restores Permitted up to Bounding. Clearing Effective only lasts until the next capset. Dropping from Bounding is the only operation whose effect survives an exec of a privileged binary.
File capabilities and their verification
A file capability is stored in the security.capability extended attribute, a versioned structure holding Permitted and Inheritable masks for the file. setcap cap_net_bind_service=+ep /usr/bin/program writes it. The kernel verifies at exec that the file’s capabilities are a subset of what the process may gain, and refuses the exec if the file is writable by anyone other than its owner, or if it lives on a filesystem mounted with nosuid.
The verification rules are worth stating because they are a common source of “why did my container lose its capability” questions. A file capability is ignored if the filesystem is mounted nosuid, if the file is not owned by a UID mapped into the current user namespace, or if the capability would raise a bit not in the process’s Bounding set. Inside a container with a user namespace, the file’s owner is a host UID; if that UID is not mapped into the container’s namespace, the kernel sees an unmapped owner and treats the file as though it had no capabilities. The capability is present in the extended attribute and absent at exec, with no error beyond the program’s own failure.
Root is not a capability
A process with effective UID 0 gets a full Effective and Permitted set at exec, but that is a consequence of the UID, not the definition of privilege. The kernel’s checks are written against capabilities: CAP_SYS_ADMIN for mount, CAP_NET_ADMIN for interface configuration, CAP_DAC_OVERRIDE for bypassing file permission bits, CAP_SYS_PTRACE for inspecting another process. A process with CAP_DAC_OVERRIDE reads any file regardless of mode; a process with CAP_DAC_READ_SEARCH reads and searches directories it could not otherwise traverse but cannot write.
Because the checks are capability-based, a non-root process can hold a specific capability and exercise exactly that authority. A program that needs to bind port 443 runs as an ordinary user with CAP_NET_BIND_SERVICE in its Effective set. If it is compromised, the attacker has one capability rather than the few dozen that make up root’s authority. That is the entire argument for capabilities over setuid-root, and it only works if the capability is genuinely narrow. CAP_SYS_ADMIN is famously not, since it gates mounts, namespace creation, ioctl on many devices, and a long list of administrative operations. A sandbox that grants CAP_SYS_ADMIN has granted most of root under a different name. The user namespace is the one context where the same bit is worth less, because a check tests the caller’s capability against the user namespace that owns the object, not against the host.
The ordering of drops matters
A sandbox setup sequence has an order that is not arbitrary, and getting it wrong produces a process that either fails to start or starts with more authority than intended:
- Create the user namespace, if one is being used. Bounding is reset to full inside a new user namespace, so any
PR_CAPBSET_DROPperformed before this step is undone. - Drop from Bounding with
prctl(PR_CAPBSET_DROP, cap), iterating over every capability that should be permanently unavailable. - Set
no_new_privswithprctl(PR_SET_NO_NEW_PRIVS, 1). This prevents set-user-ID and file-capability elevation at exec entirely, and is a prerequisite for installing a seccomp filter withoutCAP_SYS_ADMIN. - Clear Permitted, Effective, and Ambient with
capsetandPR_CAP_AMBIENT_CLEAR_ALL, keeping only what the payload needs. - Install the seccomp filter.
execvethe payload.
Steps 2 and 3 are the ones that survive the exec; steps 1 and 4 are preparatory. no_new_privs and Bounding overlap in effect, since both prevent elevation at exec, but they work differently: no_new_privs suppresses the elevation for all binaries, while Bounding caps which capabilities could be gained. Setting both is the usual practice because each has failure modes the other does not cover.
Ambient capabilities and the helper-binary problem
Before ambient capabilities, a program holding CAP_NET_BIND_SERVICE lost it the moment it executed any other binary, including a shell script helper. The workaround was to keep the capability in Inheritable and have every helper carry a file capability, which is impractical. Ambient capabilities fix this by making the capability survive an exec of a non-privileged file.
The rules are restrictive by design: a capability can be raised to Ambient only if it is already in both Permitted and Inheritable, and raising Ambient fails if the process is not privileged with respect to its own user namespace in the required way. Ambient capabilities are cleared if the file has any capability or set-user-ID bit, and cleared entirely when a process with a non-zero UID executes a set-user-ID-root binary. For sandboxes, ambient capabilities are usually cleared rather than raised, because a payload that can spawn helpers while retaining authority has a wider footprint than one that cannot.
Where capability checks stop
Capabilities are checked at the point of the operation, and the check is on the caller’s namespace. A process in a child user namespace holding CAP_SYS_ADMIN there passes the check for operations on resources owned by that namespace, and fails it for resources owned by the parent. That asymmetry is the design, and it means capability sets are not a global privilege measure: the same bit is worth different amounts depending on which namespace owns the object being operated on.
Capabilities also do not restrict what a process does with data it already legitimately holds. A process with CAP_DAC_READ_SEARCH can read a file; once read, the bytes are the process’s, and no capability check applies to sending them anywhere. This is why the mechanisms that limit egress (network namespaces, seccomp filters on socket and connect, and the supervision patterns described in Supervising Syscalls with User Notification) are separate from the privilege model rather than derived from it.
References
- capabilities(7) — Linux manual page — man-pages project
- capabilities(3) — Linux manual page — man-pages project
- POSIX.1e draft 17: Protection, Audit and Control Interfaces — IEEE (withdrawn draft)