Capability Sets and the Bounding Set
A Linux thread does not carry a single list of “capabilities it has.” It carries five distinct capability sets, each a 64-bit mask, and the relationships between them are the whole game. The permitted set is the ceiling — the superset of what the thread may make effective. The effective set is what the kernel actually checks right now (the only set consulted at a
capable()decision point). The inheritable set is what is preserved acrossexecve()under the original (pre-ambient) rules. The bounding set is a per-thread mask — a hard upper limit on what can ever be acquired, which can only be reduced, never raised. The ambient set, added in Linux 4.3, is the set of capabilities preserved across anexecve()of an unprivileged program, closing a usability gap in the inheritable mechanism. Getting these five right — and understanding that dropping the bounding set is a one-way ratchet — is the heart of least privilege and a core container-hardening step (capabilities(7)).
This note assumes you already know what capabilities are and why they exist — see POSIX Capabilities for the model, the history, the CAP_* list, the count at 6.12 (CAP_LAST_CAP = 40), and the capget/capset interface. Here we dissect the five sets, the securebits flags that bend their rules, and the bounding-set drop that hardens containers. The precise execve() formula that recomputes all five at program load lives in Capability Transitions Across execve; the file side lives in File Capabilities and Ambient Capabilities.
Mental Model — Five Sets, One Ratchet
Picture the five sets as nested constraints on a single question: which CAP_* bits will the kernel honour for this thread? The effective set answers it directly; the others constrain how the effective set can be populated, now and after future execves.
flowchart TB BND["BOUNDING set (CapBnd)<br/>hard ceiling — only shrinks,<br/>one-way via PR_CAPBSET_DROP"] PERM["PERMITTED set (CapPrm)<br/>the usable ceiling<br/>(subset constrained by bounding at execve)"] EFF["EFFECTIVE set (CapEff)<br/>what capable() checks NOW<br/>must be subset of permitted"] INH["INHERITABLE set (CapInh)<br/>survives execve via the OLD rules<br/>(AND-ed with file inheritable)"] AMB["AMBIENT set (CapAmb)<br/>survives execve of UNPRIVILEGED binaries<br/>subset of (permitted AND inheritable)"] BND -->|"caps what execve can put in"| PERM PERM -->|"effective ⊆ permitted"| EFF PERM -->|"ambient ⊆ permitted ∩ inheritable"| AMB INH -->|"ambient ⊆ permitted ∩ inheritable"| AMB INH -.->|"combined with file caps at execve"| PERM
The five capability sets and their constraint relationships. What it shows: the bounding set is the outermost ceiling (only ever shrinks); permitted is the usable ceiling under it; effective must be a subset of permitted; ambient must be a subset of both permitted and inheritable; inheritable feeds the permitted set across execve. The insight to take: these are constraints, not a flow — the effective set is the only one the kernel reads at decision time, and every other set exists to bound how effective gets populated, with the bounding set acting as a one-way ratchet that can permanently amputate a privilege from a process tree.
The single most important reduction: the kernel’s actual privilege check reads only the effective set (cap_raised(cred->cap_effective, cap) in cap_capable(), commoncap.c v6.12). The other four sets are bookkeeping that determines what can be in cap_effective today and after the next program load.
The Five Sets, One by One
All five definitions are from capabilities(7); they live in the thread’s [[Process Credentials and struct cred|struct cred]] as cap_permitted, cap_effective, cap_inheritable, cap_bset, and cap_ambient. You can read them live from /proc/PID/status as the hex fields CapPrm, CapEff, CapInh, CapBnd, CapAmb.
Permitted
“A limiting superset for the effective capabilities that the thread may assume.” A capability must be in the permitted set before the thread can raise it into the effective set. Permitted also limits what can be added to the inheritable set (without CAP_SETPCAP). Critically, once a capability is dropped from permitted, the thread cannot re-acquire it except by an execve() of a file (or setuid-root program) that grants it back. This irreversibility is what makes “drop your permitted set after startup” a genuine security boundary, not a soft suggestion.
Effective
“The set of capabilities used by the kernel to perform permission checks for the thread.” This is the only set consulted at a capable() / ns_capable() call (per commoncap.c). A thread can hold a capability in permitted but keep it out of effective — dormant, ready to be raised only for the brief window it is needed (the classic “raise privilege, do the operation, lower it again” pattern). The constraint enforced by capset is effective ⊆ permitted: you can never make a capability effective that is not also permitted.
Inheritable
“A set of capabilities preserved across an execve(2).” This is the original (pre-4.3) inheritance mechanism, and it is subtler than its name suggests. The inheritable set is not automatically granted across exec; it is AND-ed with the new program’s file inheritable set to decide which capabilities land in the post-exec permitted set. In symbols (full formula in Capability Transitions Across execve): the contribution is P(inheritable) & F(inheritable). So an inheritable capability only “survives” if the executed file also lists it as inheritable. In practice this made inheritable hard to use — it requires cooperation from the target binary’s file caps — which is exactly the gap the ambient set fills.
Bounding
“A mechanism that can be used to limit the capabilities that are gained during execve(2).” The bounding set is a per-thread mask (per-thread since Linux 2.6.25; before that it was a single system-wide value). At execve, it caps the file permitted contribution: F(permitted) & P(bounding) — a capability the file would grant is silently dropped if it is not in the bounding set. Two facts make the bounding set special:
- It can only shrink. There is no operation to add a capability to the bounding set. The only mutation is
prctl(PR_CAPBSET_DROP, cap), which removes a single capability and “any children of the calling thread will inherit the newly reduced bounding set” (PR_CAPBSET_DROP(2const)). Dropping requiresCAP_SETPCAP; without it you get-EPERM. It is a one-way ratchet — irreversible for the process and its whole future descendant tree. - It does not mask inheritable. A subtle point from capabilities(7): the bounding set masks the file permitted path but not the inheritable path. A thread can still gain a capability via
P(inheritable) & F(inheritable)even if that capability is outside its bounding set. The bounding set is therefore not a complete firewall against re-acquisition; it is specifically a brake on the file-permitted (setcap / setuid-root) route.
Read the current bounding set with prctl(PR_CAPBSET_READ, cap), which returns 1 if cap is present.
Ambient
“A set of capabilities preserved across an execve(2) of a program that is not privileged” (Linux 4.3+; capabilities(7)). The ambient set exists because inheritable was nearly unusable: it required the executed binary to carry matching file-inheritable bits, which ordinary binaries (a shell, a Python interpreter, a helper) do not have. The ambient set lets a privileged launcher hand capabilities to a plain, un-setcap’d binary. It obeys a strict invariant:
“No capability can ever be ambient if it is not both permitted and inheritable.” — capabilities(7)
So ambient ⊆ (permitted ∩ inheritable) at all times, and the kernel automatically lowers an ambient capability the moment its permitted or inheritable counterpart is lowered. Two more rules give it safe semantics: (a) at execve, ambient capabilities are added to the new permitted set and (if the file’s effective bit is set, or via P'(eff)=P'(amb)) to the effective set; (b) executing a binary that has any file capabilities, or that changes UID/GID via the setuid/setgid bits, clears the entire ambient set — preventing an ambient capability from leaking into a privilege transition it was not meant for. Ambient gains also do not trigger the dynamic linker’s secure-execution mode.
Modifying the Sets — prctl, capset, and the Ambient Operations
The sets are mutated through three interfaces. capset(2) (see POSIX Capabilities) writes the permitted/effective/inheritable triple subject to the monotonicity rules (permitted can only shrink, effective ⊆ permitted, inheritable additions need the capability already in permitted/inheritable-or-CAP_SETPCAP and within bounding). The bounding and ambient sets are touched only through prctl(2), handled in cap_task_prctl() in commoncap.c (v6.12):
PR_CAPBSET_READ— returns whether a capability is raised in the bounding set.PR_CAPBSET_DROP— callscap_prctl_drop()to remove a capability from the current thread’s bounding set; requiresCAP_SETPCAP. One-way.PR_CAP_AMBIENTwith four sub-operations (PR_CAP_AMBIENT(2const), Linux 4.3+):PR_CAP_AMBIENT_RAISE— add a capability to the ambient set. Fails unless the capability is in both permitted and inheritable, andSECBIT_NO_CAP_AMBIENT_RAISEis not set.PR_CAP_AMBIENT_LOWER— remove a single capability from the ambient set.PR_CAP_AMBIENT_IS_SET— query a single capability.PR_CAP_AMBIENT_CLEAR_ALL— empty the ambient set.
The securebits Flags
A small set of per-thread securebits flags overrides the kernel’s default capability-and-UID interaction logic — chiefly the legacy magic that ties capabilities to “is UID 0?”. They are defined in include/uapi/linux/securebits.h (v6.12) and read/written via prctl(PR_GET_SECUREBITS) / prctl(PR_SET_SECUREBITS) (setting requires CAP_SETPCAP). Each flag has a paired *_LOCKED bit that makes the setting irreversible:
SECBIT_NOROOT(bit 0) — the kernel does not grant capabilities when a setuid-root program is executed or a UID-0 process callsexecve(). This severs the “root automatically gets all capabilities at exec” behaviour, forcing capabilities to flow only through file/ambient/inheritable mechanics.SECBIT_NO_SETUID_FIXUP(bit 2) — stops the kernel from automatically adjusting the permitted/effective/ambient sets when the thread’s UIDs transition between zero and nonzero. By default, when a process drops from UID 0 to a normal UID, the kernel clears its capabilities (“setuid fixup”); this bit disables that, so a process can change UID without losing capabilities.SECBIT_KEEP_CAPS(bit 4) — allows a thread that has one or more zero UIDs to retain its permitted capabilities when all of its UIDs become nonzero. It is cleared on everyexecve()and is ignored ifSECBIT_NO_SETUID_FIXUPis set. (This is the bit behind the olderprctl(PR_SET_KEEPCAPS)shortcut.)SECBIT_NO_CAP_AMBIENT_RAISE(bit 6) — disallows raising ambient capabilities viaPR_CAP_AMBIENT_RAISE, useful to lock down a process so it cannot grow its ambient set further.
Each *_LOCKED variant (bits 1, 3, 5, 7) prevents further changes to its base flag. SECURE_ALL_BITS is the OR of bits 0/2/4/6 and SECURE_ALL_LOCKS is SECURE_ALL_BITS << 1 (securebits.h). Securebits matter mainly to programs that deliberately decouple capabilities from UID — the canonical use is a daemon that wants to keep a capability after dropping from root to an unprivileged UID, the inverse of the usual “lose everything on UID change” default.
A Worked Example — Dropping the Bounding Set for a Container
The most consequential real-world use of these sets is dropping the bounding set as a container-hardening step. A container runtime computes the capabilities the container should ever be able to hold, then PR_CAPBSET_DROPs every other capability before execve-ing the container’s init — so even a setuid-root binary inside the container, or a future execve of a file with file capabilities, can never re-acquire the amputated privileges.
#include <sys/prctl.h>
#include <linux/capability.h>
#include <errno.h>
/* Drop every capability EXCEPT the ones in `keep[]` from the bounding set.
* After this, no execve in this process or any descendant can ever
* regain a dropped capability via file capabilities or setuid-root. */
int harden_bounding_set(const int *keep, int n_keep)
{
for (int cap = 0; cap <= CAP_LAST_CAP; cap++) { /* CAP_LAST_CAP == 40 at v6.12 */
int wanted = 0;
for (int i = 0; i < n_keep; i++)
if (keep[i] == cap) { wanted = 1; break; }
if (wanted)
continue;
if (prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) == -1) {
if (errno == EINVAL) /* cap number above this kernel's CAP_LAST_CAP */
continue;
return -1; /* EPERM => we lack CAP_SETPCAP */
}
}
return 0;
}Walking it: the loop iterates from capability 0 to CAP_LAST_CAP (which is 40 at v6.12, per capability.h — but the code reads the macro so it is correct on any kernel). For each capability not in the keep-list, prctl(PR_CAPBSET_DROP, cap, ...) removes it from the bounding set. An EINVAL means the capability number is above this kernel’s CAP_LAST_CAP (older kernel, fewer caps) and is skipped; an EPERM means the caller lacks CAP_SETPCAP and the whole hardening fails. Because the drop is irreversible and inherited by children, the container’s entire process tree is now permanently capped. The same effect is what systemd’s CapabilityBoundingSet= directive produces.
You can watch the result with capsh and /proc/self/status:
$ capsh --drop=cap_sys_admin,cap_net_admin -- -c 'grep CapBnd /proc/self/status'
CapBnd: 000001ffffffffff # full set is 0..40; the two dropped bits are now clearThe hex 000001ffffffffff is the full bounding set on a 6.12-era kernel: bits 0 through 40 set (41 capabilities), confirming CAP_LAST_CAP == 40. After dropping two, those specific bits would read as 0 in the mask.
Uncertain
Verify: the full-bounding-set bitmask on a stock Linux 6.12/6.18 kernel is
0x1ffffffffff(41 bits, caps 0–40), matchingCAP_LAST_CAP == 40. Confirmed by a live read of/proc/self/statuson a 6.19 system (CapBnd: 000001ffffffffff) and by capability.h v6.12. Reason flagged: the numeric mask is version-dependent — a kernel with more capabilities would show a wider mask. To resolve for a specific kernel: read/proc/sys/kernel/cap_last_cap(gives the highest cap number) on that running kernel.#uncertain
Failure Modes and Common Misunderstandings
“I dropped the bounding set, so the process has no privileges now.” Wrong on two counts. First, dropping a capability from bounding does not remove it from the permitted or effective sets of the current process — those keep working until the next execve; bounding only constrains what execve can re-grant. To strip the running process you must also clear permitted/effective via capset. Second, as noted above, bounding does not mask the inheritable path, so a capability can technically still re-enter via P(inheritable) & F(inheritable) on a binary with matching file caps. Hardening usually drops bounding and the inheritable set and sets no_new_privs.
Inheritable set “doesn’t work.” The most common surprise: a launcher raises a capability in its inheritable set, execves a normal binary, and the capability vanishes. That is correct behaviour — inheritable is AND-ed with the file’s inheritable bits, and a plain binary has none. The fix is the ambient set, which is exactly what it was designed for. People reach for inheritable when they want ambient.
Ambient silently empties. A process carefully raises ambient capabilities, then execves a setuid binary (or one with file caps) and the ambient set is gone. By design — executing a privileged file or a UID/GID-changing setid binary clears the entire ambient set (capabilities(7)), so an ambient capability cannot ride along into a privilege transition.
Effective-without-permitted. Trying to capset a capability into the effective set that is not in the (new) permitted set returns -EPERM; effective is always a subset of permitted.
Alternatives and When to Choose Them
The five-set machinery is the only way Linux models per-thread capability state — there is no simpler alternative within the capabilities mechanism. The relevant choice is which set to manipulate for a given goal:
- Want a privilege gone forever from a subtree? Drop it from the bounding set (one-way) and pair with no_new_privs so no setuid binary can sneak it back. This is the container-hardening choice.
- Want a plain binary to start with a capability? Use the ambient set in the launcher, not inheritable.
- Want a daemon to keep a capability after dropping to a normal UID? Use
SECBIT_KEEP_CAPS/PR_SET_KEEPCAPS(orSECBIT_NO_SETUID_FIXUP), then trim the permitted/effective sets to just what is needed. - Want a binary to carry its own capabilities, no launcher needed? Use file capabilities — see File Capabilities and Ambient Capabilities.
Stepping out of capabilities entirely: Seccomp and seccomp-BPF (syscall filtering) and SELinux/AppArmor (mandatory access control) confine on orthogonal axes and are normally combined with capability dropping, not chosen instead of it.
Production Notes
Container runtimes lean hard on the bounding set. Docker (moby) computes a default allowlist of 14 capabilities and drops everything else from the bounding set of every container (Docker run reference); --cap-drop=ALL --cap-add=... is the standard hardening idiom, and the dropped bits are gone from the bounding set so even a misconfigured setuid-root binary inside cannot escalate to them. Kubernetes surfaces the same control through securityContext.capabilities.drop: ["ALL"] (see SecurityContext and Pod Security Standards). systemd services use CapabilityBoundingSet= (bounding), AmbientCapabilities= (ambient, to grant a plain ExecStart binary a capability without setcap), and SecureBits= to set securebits declaratively — letting an admin run, say, an unprivileged service that still holds CAP_NET_BIND_SERVICE via the ambient set, with everything else dropped from bounding. The combination “drop bounding to a minimal allowlist + ambient-grant exactly what’s needed + no_new_privs” is the modern least-privilege recipe and the practical reason these five sets matter.
See Also
- POSIX Capabilities — the model, history,
CAP_*list, count at 6.12, andcapget/capset; the parent concept this note extends - File Capabilities and Ambient Capabilities — the file side of inheritance and the ambient mechanism in depth
- Capability Transitions Across execve — the exact
P'(perm/eff/inh/amb/bounding)formula that recomputes all five sets at program load - CAP_SYS_ADMIN and the Capability Granularity Problem — why one bit dominates the bounding set’s contents
- no_new_privs and Privilege Escalation Control — the keystone that makes bounding-set drops un-escapable across exec
- Process Credentials and struct cred — where
cap_permitted/cap_effective/cap_inheritable/cap_bset/cap_ambientlive - Linux Security MOC — the parent map (section B, “Splitting Root — POSIX Capabilities”)
- Linux Containers and Isolation MOC — bounding-set dropping is a core container-hardening step