POSIX Capabilities
Traditional UNIX has a binary privilege model: the superuser (user ID 0, “root”) bypasses every kernel permission check, and everyone else is bound by them. Linux capabilities — historically called “POSIX capabilities” after the draft standard they descend from — break that monolithic root power into a set of individually grantable bits, so that a program needing only one root-like privilege need not hold them all. A web server that must bind TCP port 80 needs only
CAP_NET_BIND_SERVICE(bind a socket to a port below 1024); it does not need the power to load kernel modules, override file permissions, or change any file’s owner. The model is enforced inside the kernel by replacing the old “is this process UID 0?” test with “is the relevantCAP_*bit raised in this thread’s effective set?” — a check that goes through the Linux Security Module (LSM) layer viasecurity_capable()(capabilities(7); kernel/capability.c). At Linux 6.12 there are 41 capabilities, numbered 0 through 40, withCAP_LAST_CAP == CAP_CHECKPOINT_RESTORE == 40(include/uapi/linux/capability.h, v6.12).
Version pin. Everything below is read against Linux 6.12, a maintained long-term-support (LTS) line; mainline had moved into the 7.x series by the time of writing (this vault’s host runs 7.1.8). Where a fact was checked against other releases — and several were, because “when did this change?” is exactly the question capability documentation gets wrong — the release is named inline. Live command output was produced on Fedora 44, kernel 7.1.8, libcap 2.78, on 2026-09-04.
This note is the hub of the capability cluster. It owns the model, the history, the complete catalogue of CAP_* constants, the path a check actually takes from a system call down to a bit test, the capget/capset application binary interface (ABI), the libcap tooling, and what real container runtimes do with all of it. Five sibling notes own the machinery in depth and are cross-linked at the point of use rather than restated here: Capability Sets and the Bounding Set (the five per-thread sets and securebits), Capability Transitions Across execve (the transition code path line by line), File Capabilities and Ambient Capabilities (the on-disk attribute and the ambient set), CAP_SYS_ADMIN and the Capability Granularity Problem, and no_new_privs and Privilege Escalation Control. Because every capability check is evaluated relative to a user namespace, User Namespaces is the reciprocal note on the namespace axis: it owns the namespace-relative privilege model, and this note owns the bits themselves.
Mental Model — a Partition of One if Statement
The right way to think about capabilities is as a partition of the single if (uid == 0) check that pervaded early UNIX kernels. Imagine every privileged operation in the kernel — mounting a filesystem, opening a raw socket, sending a signal to an unrelated process, overriding a file’s read permission — once guarded by a test for root. Capabilities cut that one test into 41 independent tests, each guarding a category of privilege, and let an administrator hand out exactly the categories a program needs.
flowchart TB subgraph OLD["Classic UNIX privilege model — one test"] R["uid == 0 ?<br/>grants ALL privileges<br/>indivisibly"] end subgraph NEW["POSIX capabilities model — 41 tests"] direction TB C0["CAP_CHOWN (0)<br/>change file owner"] C1["CAP_DAC_OVERRIDE (1)<br/>bypass file perms"] C7["CAP_SETUID (7)<br/>change process UIDs"] C10["CAP_NET_BIND_SERVICE (10)<br/>bind ports < 1024"] C12["CAP_NET_ADMIN (12)<br/>configure networking"] C21["CAP_SYS_ADMIN (21)<br/>vast catch-all"] CN["...41 caps total,<br/>0 .. CAP_LAST_CAP = 40"] end R -->|"split into individually<br/>grantable bits"| NEW
The conceptual shift capabilities introduce. What it shows: the single all-or-nothing root test on the left becomes a vector of 41 independent privilege bits on the right, each guarding a specific class of operation. The insight to take: a process can now hold CAP_NET_BIND_SERVICE without holding CAP_SYS_ADMIN or CAP_DAC_OVERRIDE — the whole point of least privilege — but the partition is only as good as its granularity, which is why the lopsided CAP_SYS_ADMIN bucket is the model’s central weakness (quantified below).
Three corrections that beginners almost always need, in order of how much damage the misunderstanding does:
A capability is not a UID. A process can be UID 0 with an empty capability set (no privileges at all), or a non-root UID holding a single capability. The kernel checks the capability, not the UID. Root’s apparent omnipotence is an emergent property — a root process simply starts with a full capability set, and the kernel adds compatibility fixups that re-grant capabilities when a process becomes UID 0 — not a special case inside the permission checks themselves. On the machine used for this note, an ordinary login shell reports CapEff: 0000000000000000 and CapBnd: 000001ffffffffff: no privileges held, but the ceiling is still the full 41 bits.
A capability is not a permission to touch a specific object. CAP_NET_BIND_SERVICE means “bind any port below 1024,” not “bind port 443.” Capabilities have no object dimension at all; that is what a mandatory access control module such as SELinux or AppArmor adds, and it is the reason the two layers compose rather than compete.
A capability is relative to a user namespace. capable(cap) is defined in v6.12 as literally return ns_capable(&init_user_ns, cap); — the “global” check is just the special case where the namespace is the initial one. A process can hold every bit inside its own user namespace and none outside it. That axis is owned by User Namespaces; this note assumes the initial namespace unless it says otherwise.
Where a Capability Check Actually Happens
Documentation tends to describe capabilities as if the kernel consults a table. It does not. A capability test is a last-resort override reached only after the ordinary discretionary access control (DAC) checks have already said no, and it is worth tracing one concrete path end to end, because the shape of that path explains several otherwise baffling behaviours.
Take open("/etc/shadow", O_RDONLY) by an unprivileged process. Path resolution ends in inode_permission() in fs/namei.c (v6.12), which runs a short fixed sequence:
int inode_permission(struct mnt_idmap *idmap, struct inode *inode, int mask)
{
retval = sb_permission(inode->i_sb, inode, mask); /* read-only fs -> -EROFS */
if (retval) return retval;
if (unlikely(mask & MAY_WRITE)) {
if (IS_IMMUTABLE(inode)) return -EPERM; /* chattr +i */
if (HAS_UNMAPPED_ID(idmap, inode)) return -EACCES;
}
retval = do_inode_permission(idmap, inode, mask); /* -> generic_permission() */
if (retval) return retval;
retval = devcgroup_inode_permission(inode, mask);
if (retval) return retval;
return security_inode_permission(inode, mask); /* LSM hook */
}Reading it symbol by symbol: mask is the requested access (MAY_READ, MAY_WRITE, MAY_EXEC); idmap is the mount’s identity map, relevant on ID-mapped mounts. Note the ordering — a read-only filesystem and the immutable inode flag beat every capability, which is why CAP_DAC_OVERRIDE cannot write to a file on a read-only mount and why chattr +i is not defeated by root alone (it takes CAP_LINUX_IMMUTABLE to clear the flag first). The capability layer sits inside generic_permission(), and the LSM hook runs after it, so an LSM can only subtract.
generic_permission() first calls acl_permission_check(), which is pure classic UNIX: if the caller’s filesystem UID owns the inode, use the owner triad of the mode bits; else consult POSIX ACLs if present; else the group triad if the caller is in the file’s group; else the other triad. Only when that returns -EACCES does the capability machinery engage:
ret = acl_permission_check(idmap, inode, mask);
if (ret != -EACCES)
return ret; /* granted (or a different error) — no cap consulted */
if (S_ISDIR(inode->i_mode)) {
if (!(mask & MAY_WRITE))
if (capable_wrt_inode_uidgid(idmap, inode, CAP_DAC_READ_SEARCH)) return 0;
if (capable_wrt_inode_uidgid(idmap, inode, CAP_DAC_OVERRIDE)) return 0;
return -EACCES;
}
mask &= MAY_READ | MAY_WRITE | MAY_EXEC;
if (mask == MAY_READ)
if (capable_wrt_inode_uidgid(idmap, inode, CAP_DAC_READ_SEARCH)) return 0;
if (!(mask & MAY_EXEC) || (inode->i_mode & S_IXUGO))
if (capable_wrt_inode_uidgid(idmap, inode, CAP_DAC_OVERRIDE)) return 0;
return -EACCES;Two details in that fragment are load-bearing and almost never documented outside the source. First, CAP_DAC_READ_SEARCH is tried before CAP_DAC_OVERRIDE for read-only accesses, so a process holding only the weaker capability gets read and directory-search access without gaining write access — the granularity is real. Second, look at the guard on the final CAP_DAC_OVERRIDE: !(mask & MAY_EXEC) || (inode->i_mode & S_IXUGO). CAP_DAC_OVERRIDE will not let you execute a file that has no execute bit set for anybody. At least one of user/group/other execute must be present. This is a deliberate refusal to make “chmod 644” mean “executable by root”, and it routinely surprises people who assume the capability is unconditional.
capable_wrt_inode_uidgid() (in kernel/capability.c) is the filesystem-specific wrapper: it is ns_capable(ns, cap) && privileged_wrt_inode_uidgid(ns, idmap, inode), the second conjunct requiring that both the inode’s UID and GID have mappings in the caller’s user namespace. That extra conjunct is why CAP_DAC_OVERRIDE inside an unprivileged container does not open host-owned files; the reasoning is developed in User Namespaces. From there the call reaches security_capable(), the LSM entry point, which lands in cap_capable() in security/commoncap.c when no module overrides the decision — and the only capability set cap_capable() ever reads is cred->cap_effective (the rest of its logic walks the user-namespace tree).
flowchart TB SYS["open("/etc/shadow", O_RDONLY)"] --> IP["inode_permission()"] IP --> SB{"read-only fs?<br/>immutable inode?"} SB -->|"yes"| DENY1["-EROFS / -EPERM<br/>capabilities cannot help"] SB -->|"no"| ACL["acl_permission_check()<br/>owner / POSIX ACL / group / other<br/>mode bits vs fsuid"] ACL -->|"allowed"| OK["access granted<br/>(no capability consulted)"] ACL -->|"-EACCES"| GP{"generic_permission()<br/>which override applies?"} GP -->|"read or search only"| C1["capable_wrt_inode_uidgid(CAP_DAC_READ_SEARCH)"] GP -->|"write, or exec with<br/>at least one x bit"| C2["capable_wrt_inode_uidgid(CAP_DAC_OVERRIDE)"] GP -->|"exec with NO x bit anywhere"| DENY2["-EACCES<br/>DAC_OVERRIDE deliberately<br/>does not apply"] C1 --> NSC["ns_capable(user_ns, cap)<br/>AND inode uid+gid mapped in that ns"] C2 --> NSC NSC --> SEC["security_capable() — LSM hook"] SEC --> CC["cap_capable() in commoncap.c<br/>cap_raised(cred->cap_effective, cap)?"] CC -->|"bit set"| LSM["remaining LSM hooks<br/>(SELinux / AppArmor / Landlock)<br/>may still deny"] CC -->|"bit clear"| DENY3["-EPERM"] LSM --> OK
The descent from a system call to a single bit test, for the file-access case. What it shows: DAC runs first and usually settles the question; the capability check is an override consulted only on -EACCES, it is gated on the inode’s IDs being mapped into the caller’s user namespace, and it terminates in one cap_raised() test against cap_effective. The insight to take: capabilities are neither the first nor the last word. Filesystem state (read-only, immutable) beats them from above, LSMs veto them from below, and within generic_permission() the kernel picks the weakest capability that could authorise the specific access — which is why CAP_DAC_READ_SEARCH is a genuinely smaller grant than CAP_DAC_OVERRIDE, and why neither can execute a file nobody may execute.
The same shape repeats across the kernel: the subsystem does its own ordinary checks, then calls capable(), ns_capable(), or a specialised wrapper as an override. The catalogue of those wrappers — file_ns_capable(), ptracer_capable(), bpf_capable(), and the rest, and which credentials each consults — is tabulated in User Namespaces and is not repeated here. The one fact worth carrying forward is that cap_effective is the only set anything ever tests. Every other set, every file attribute, and every prctl operation in the rest of this note exists solely to decide what ends up in cap_effective.
The Complete Catalogue — 41 Capabilities, Grouped by What They Actually Grant
The list below is the full set at v6.12, taken from include/uapi/linux/capability.h for the numbers and capabilities(7) for the semantics. Numbers matter: they are the bit positions in every capability mask you will ever read out of /proc, and they are append-only — a capability is never renumbered, and none has ever been removed. The gaps in the historical “since” column are just capabilities that predate the kernel’s own changelog discipline.
The grouping is this note’s own, chosen so that the shape of the partition is visible. The kernel does not enforce these families; capabilities(7) calls them “silos” and advises new features to pick an existing one rather than mint a bit.
mindmap root((41 capability bits, 0 to 40)) Filesystem_and_ownership CAP_CHOWN 0 CAP_DAC_OVERRIDE 1 CAP_DAC_READ_SEARCH 2 CAP_FOWNER 3 CAP_FSETID 4 CAP_LINUX_IMMUTABLE 9 CAP_MKNOD 27 CAP_LEASE 28 CAP_SETFCAP 31 Process_identity_and_privilege CAP_KILL 5 CAP_SETGID 6 CAP_SETUID 7 CAP_SETPCAP 8 CAP_SYS_PTRACE 19 CAP_SYS_NICE 23 Networking CAP_NET_BIND_SERVICE 10 CAP_NET_BROADCAST 11 CAP_NET_ADMIN 12 CAP_NET_RAW 13 IPC_and_memory CAP_IPC_LOCK 14 CAP_IPC_OWNER 15 Kernel_and_hardware CAP_SYS_MODULE 16 CAP_SYS_RAWIO 17 CAP_SYS_BOOT 22 CAP_SYS_TIME 25 CAP_SYS_TTY_CONFIG 26 CAP_WAKE_ALARM 35 CAP_BLOCK_SUSPEND 36 System_administration CAP_SYS_CHROOT 18 CAP_SYS_PACCT 20 CAP_SYS_ADMIN 21 CAP_SYS_RESOURCE 24 CAP_SYSLOG 34 CAP_CHECKPOINT_RESTORE 40 Security_subsystems CAP_AUDIT_WRITE 29 CAP_AUDIT_CONTROL 30 CAP_AUDIT_READ 37 CAP_MAC_OVERRIDE 32 CAP_MAC_ADMIN 33 Observability CAP_PERFMON 38 CAP_BPF 39
The 41 capabilities arranged into silos. What it shows: the model’s coverage is genuinely broad — filesystem, identity, network, memory, hardware, audit, mandatory access control, observability — but the branches are wildly uneven in power, and one leaf (CAP_SYS_ADMIN) is a silo unto itself. The insight to take: use this as a lookup structure. When you are deciding what to grant, find the silo your operation lives in and take the narrowest bit in it; the granularity problem is almost always solvable at the silo level (for example CAP_BPF instead of CAP_SYS_ADMIN, CAP_DAC_READ_SEARCH instead of CAP_DAC_OVERRIDE).
The reference table. The Escalates? column is analysis, not a kernel guarantee: it marks capabilities from which a determined holder has a well-understood path to full root, with the mechanism named. Michael Kerrisk’s LWN analysis credits Brad Spengler with the general observation that “the ability to be leveraged for full root privileges is a weakness of many existing capabilities.”
| # | Capability | Since | What it grants (condensed from capabilities(7)) | Escalates? |
|---|---|---|---|---|
| 0 | CAP_CHOWN | 2.2 | Make arbitrary changes to file UIDs and GIDs | Yes — chown /etc/passwd or a root-owned unit file to yourself, then edit it |
| 1 | CAP_DAC_OVERRIDE | 2.2 | Bypass file read, write, and execute permission checks | Yes — write /etc/shadow, /etc/sudoers, any unit file |
| 2 | CAP_DAC_READ_SEARCH | 2.2 | Bypass read permission on files and read/search on directories; open_by_handle_at(2); linkat() with AT_EMPTY_PATH | Yes — read any secret on the system; open_by_handle_at also escapes a chroot |
| 3 | CAP_FOWNER | 2.2 | Bypass checks that normally require the process’s filesystem UID to match the file’s UID (chmod, utime, inode flags, ACLs); ignore the directory sticky bit on deletion; O_NOATIME on any file | Partly — can chmod files it does not own |
| 4 | CAP_FSETID | 2.2 | Do not clear set-user-ID/set-group-ID mode bits when a file is modified; set the set-GID bit on a file whose GID you do not hold | Partly — preserves setuid bits across writes |
| 5 | CAP_KILL | 2.2 | Bypass permission checks for sending signals | No, but is a system-wide denial of service |
| 6 | CAP_SETGID | 2.2 | Arbitrary manipulation of process GIDs and the supplementary group list; forge the GID in SCM_CREDENTIALS; write a GID map in a user namespace | Yes — join group 0 / any privileged group |
| 7 | CAP_SETUID | 2.2 | Arbitrary manipulation of process UIDs; forge the UID in SCM_CREDENTIALS; write a UID map in a user namespace | Yes — just call setuid(0) |
| 8 | CAP_SETPCAP | 2.2 | Add any capability from the thread’s bounding set to its inheritable set; drop bounding-set bits (PR_CAPBSET_DROP); change securebits | Partly — it can move any bounding-set bit into the inheritable set, which becomes real privilege on execve of any file whose fI carries it |
| 9 | CAP_LINUX_IMMUTABLE | 2.2 | Set the FS_APPEND_FL and FS_IMMUTABLE_FL inode flags | No — but it defeats append-only audit logs |
| 10 | CAP_NET_BIND_SERVICE | 2.2 | Bind an Internet-domain socket to a privileged port (below 1024) | No — the textbook safe grant |
| 11 | CAP_NET_BROADCAST | 2.2 | (Marked unused in capabilities(7)) socket broadcasts and multicast listening | No |
| 12 | CAP_NET_ADMIN | 2.2 | Interface configuration; firewall/nftables; routing tables; promiscuous mode; transparent-proxy binds; SO_DEBUG, SO_MARK, SO_RCVBUFFORCE | Partly — full control of the network stack; can redirect traffic |
| 13 | CAP_NET_RAW | 2.2 | Use RAW and PACKET sockets; bind to any address for transparent proxying | Partly — sniff and spoof on the local segment |
| 14 | CAP_IPC_LOCK | 2.2 | mlock/mlockall/locked mmap/shmctl; allocate huge pages | No — but it can exhaust unswappable memory |
| 15 | CAP_IPC_OWNER | 2.2 | Bypass permission checks on System V IPC objects | No |
| 16 | CAP_SYS_MODULE | 2.2 | Load and unload kernel modules (init_module, finit_module, delete_module) | Yes — arbitrary code in ring 0; total compromise |
| 17 | CAP_SYS_RAWIO | 2.2 | iopl/ioperm; /proc/kcore; FIBMAP; MSR devices; /dev/mem, /dev/kmem; raw SCSI commands; lower mmap_min_addr | Yes — direct physical memory access |
| 18 | CAP_SYS_CHROOT | 2.2 | chroot(2); change mount namespaces with setns(2) | Partly — classic chroot-escape tricks with a second chroot |
| 19 | CAP_SYS_PTRACE | 2.2 | ptrace arbitrary processes; read /proc/pid/mem, maps, exe, fd/*; process_vm_readv/writev; kcmp; get_robust_list | Yes — inject code into any root process |
| 20 | CAP_SYS_PACCT | 2.2 | acct(2) — turn process accounting on and off | No (capabilities(7) itself calls this bit “probably a mistake”) |
| 21 | CAP_SYS_ADMIN | 2.2 | mount/umount/pivot_root; quotactl; swapon/swapoff; sethostname/setdomainname; setns; fanotify_init; namespace creation; trusted.* and security.* xattrs; TIOCSTI; install a seccomp filter without no_new_privs; suspend a tracee’s seccomp; device-cgroup rules; override RLIMIT_NPROC and fs.file-max; a long tail of driver and block ioctls | Yes — see CAP_SYS_ADMIN and the Capability Granularity Problem |
| 22 | CAP_SYS_BOOT | 2.2 | reboot(2) and kexec_load(2) | Yes — kexec boots an attacker-supplied kernel |
| 23 | CAP_SYS_NICE | 2.2 | Raise scheduling priority; set real-time policies for any process; set CPU affinity and I/O priority for any process; migrate_pages, move_pages, MPOL_MF_MOVE_ALL | No — but a real-time priority is a trivial system lock-up |
| 24 | CAP_SYS_RESOURCE | 2.2 | Use ext2 reserved space; ext3 journal ioctls; override disk quotas and RLIMIT_*; raise msg_qbytes, pipe sizes, POSIX message-queue limits; PR_SET_MM; lower oom_score_adj past another process’s floor | Partly — PR_SET_MM rewrites the caller’s own memory descriptor |
| 25 | CAP_SYS_TIME | 2.2 | settimeofday, stime, adjtimex; set the hardware clock | No — but it breaks TLS validity windows, Kerberos, and audit ordering |
| 26 | CAP_SYS_TTY_CONFIG | 2.2 | vhangup(2) and privileged virtual-terminal ioctls | Partly — terminal hijacking |
| 27 | CAP_MKNOD | 2.4 | Create special files with mknod(2) | Yes — make a device node for the raw disk, then read or write it |
| 28 | CAP_LEASE | 2.4 | Establish fcntl(2) leases on arbitrary files | No |
| 29 | CAP_AUDIT_WRITE | 2.6.11 | Write records to the kernel audit log | No — but it lets you flood or forge audit entries |
| 30 | CAP_AUDIT_CONTROL | 2.6.11 | Enable/disable auditing; change audit filter rules; read audit status | Partly — disables the evidence trail |
| 31 | CAP_SETFCAP | 2.6.24 | Set arbitrary capabilities on a file. Since Linux 5.12 also required to map user ID 0 in a new user namespace | Yes — stamp cap_setuid=ep onto a binary and execute it |
| 32 | CAP_MAC_OVERRIDE | 2.6.25 | Override mandatory access control. The base kernel enforces no MAC policy; implemented for Smack | Yes where a MAC policy is the containment |
| 33 | CAP_MAC_ADMIN | 2.6.25 | Change MAC configuration or state; implemented for Smack | Yes, same reasoning |
| 34 | CAP_SYSLOG | 2.6.37 | Privileged syslog(2) operations; see kernel addresses through /proc when kptr_restrict is 1 | Partly — leaks the kernel addresses that defeat KASLR |
| 35 | CAP_WAKE_ALARM | 3.0 | Set CLOCK_REALTIME_ALARM and CLOCK_BOOTTIME_ALARM timers (wake the system) | No |
| 36 | CAP_BLOCK_SUSPEND | 3.5 | Use features that block system suspend (EPOLLWAKEUP, /proc/sys/wake_lock) | No |
| 37 | CAP_AUDIT_READ | 3.16 | Read the audit log via a multicast netlink socket | No — but it is an information-disclosure channel |
| 38 | CAP_PERFMON | 5.8 | perf_event_open(2) and BPF operations with performance implications; carved out of CAP_SYS_ADMIN | Yes — the header notes it lets BPF “read arbitrary kernel memory” via bpf_probe_read |
| 39 | CAP_BPF | 5.8 | Create all BPF map types; advanced verifier features (bounded loops, BPF-to-BPF calls, pointer arithmetic relaxations); load BTF; read xlated/JITed code; carved out of CAP_SYS_ADMIN | Partly alone; yes combined with CAP_PERFMON |
| 40 | CAP_CHECKPOINT_RESTORE | 5.9 | Write /proc/sys/kernel/ns_last_pid; use clone3()’s set_tid; read other processes’ /proc/pid/map_files symlinks; carved out of CAP_SYS_ADMIN | No |
The complete CAP_* reference at Linux 6.12, with the escalation analysis inline. What it shows: roughly a third of the bits have a documented, straightforward path to full root — CAP_SETUID most bluntly of all, since it is one setuid(0) call. The insight to take: “we dropped down to a single capability” is only meaningful if you check which capability. Dropping to CAP_SETUID, CAP_DAC_OVERRIDE, CAP_SYS_MODULE, CAP_SYS_PTRACE, CAP_SYS_RAWIO, CAP_MKNOD, CAP_SETFCAP, or CAP_SYS_ADMIN buys you essentially nothing against a hostile process; dropping to CAP_NET_BIND_SERVICE buys you a great deal.
Three of these are recent and carry a story. CAP_PERFMON (38) and CAP_BPF (39) arrived in Linux 5.8 and CAP_CHECKPOINT_RESTORE (40) in Linux 5.9, each explicitly “added to separate out … functionality from the overloaded CAP_SYS_ADMIN capability” (capabilities(7)). The carve-outs did not shrink CAP_SYS_ADMIN, though: for ABI compatibility the kernel’s convenience wrappers are disjunctions — bpf_capable() is CAP_BPF or CAP_SYS_ADMIN, and checkpoint_restore_ns_capable() is CAP_CHECKPOINT_RESTORE or CAP_SYS_ADMIN. The overloaded capability keeps every power it ever had; what changed is that new deployments no longer have to ask for it.
Uncertain
Verify: the capability count is 41 (numbers 0–40),
CAP_LAST_CAP = CAP_CHECKPOINT_RESTORE = 40. Confirmed four ways on 2026-09-04: (a) the header defines#define CAP_LAST_CAP CAP_CHECKPOINT_RESTOREat v6.1, v6.3, v6.6, v6.12, v6.18 andmaster— no capability has been added since Linux 5.9; (b)capabilities(7)(man-pages 6.18, dated 2026-02-08) lists nothing beyondCAP_CHECKPOINT_RESTORE; (c) a live read of/proc/sys/kernel/cap_last_capon the 7.1.8 host returned40and/proc/self/statusshowedCapBnd: 000001ffffffffff— 41 bits; (d)capsh --decode=0x000001ffffffffff(libcap 2.78) printed exactly 41 names,cap_chownthroughcap_checkpoint_restore. Reason this is flagged at all: capability numbers are append-only but not frozen, andCAP_VALID_MASKisBIT_ULL(CAP_LAST_CAP+1)-1, so the constant0x1ffffffffffsilently changes meaning the day a 42nd bit lands. To resolve on any other kernel: read that kernel’sinclude/uapi/linux/capability.h, orcat /proc/sys/kernel/cap_last_capon a running instance.#uncertain
There is a hard ceiling worth knowing about: capabilities(7)’s guidance to kernel developers states that “the size of capability sets is currently limited to 64 bits.” A capability set is one machine word. Internally the kernel stopped pretending otherwise in Linux 6.3: at v6.2 kernel_cap_t was still struct kernel_cap_struct { __u32 cap[2]; }, and from v6.3 it is simply typedef struct { u64 val; } kernel_cap_t (include/linux/capability.h, v6.2 versus v6.12). The user-space ABI never changed — capget/capset still exchange two 32-bit words — which is why the split-word arithmetic below is still something callers must get right.
The Five Sets — a Working Reference
A thread does not carry “the capabilities it has.” It carries five 64-bit masks, all living in its [[Process Credentials and struct cred|struct cred]], and the relationships between them are the whole model. The full treatment — each set’s semantics, the securebits flags that bend the rules, and a worked container-hardening example — belongs to Capability Sets and the Bounding Set. What follows is the reference this hub owes a reader: one table, one state machine, and the transition formula walked symbol by symbol, because these are precisely the things nobody can hold in their head.
| Set | /proc/PID/status | struct cred field | What it is | How it changes | Direction of travel |
|---|---|---|---|---|---|
| Permitted (P) | CapPrm | cap_permitted | The ceiling on what may be made effective — capabilities held | capset(2); recomputed at execve | Only shrinks within a thread’s life; grows only across execve |
| Effective (E) | CapEff | cap_effective | What capable() actually tests. The only set ever consulted at a decision point | capset(2), must stay a subset of P; recomputed at execve | Freely raised and lowered within P |
| Inheritable (I) | CapInh | cap_inheritable | Preserved verbatim across execve, but only usable if the executed file’s fI also carries the bit | capset(2); needs CAP_SETPCAP to add a bit not already held, and the bit must be in the bounding set | Unchanged by execve |
| Bounding (X) | CapBnd | cap_bset | A hard per-thread mask on what execve may put into P, and on what may be added to I | prctl(PR_CAPBSET_DROP) with CAP_SETPCAP | One-way ratchet — never rises, survives execve, inherited at fork |
| Ambient (A) | CapAmb | cap_ambient | Preserved across execve of an unprivileged file. Invariant: a bit can be ambient only if it is in both P and I | prctl(PR_CAP_AMBIENT, ...); auto-lowered whenever P or I loses the bit | Cleared wholesale by exec of a file with capabilities or a set-UID/GID bit |
The five sets side by side. What it shows: three of the five (P, E, I) are directly writable by capset(2), one (X) can only be reduced, and one (A) is a derived set that the kernel keeps pinned inside P ∩ I. The insight to take: the asymmetry is the security property. Nothing a thread can do to itself raises its own privilege; the only way a bit enters P that was not already there is an execve, and the bounding set caps even that.
A single capability bit therefore has a small, precise life cycle:
stateDiagram-v2 direction LR [*] --> Bounded : inherited at fork(2) state "In bounding set X only<br/>(allowed, not held)" as Bounded state "In permitted P<br/>(held but dormant)" as Permitted state "In effective E<br/>(capable() succeeds now)" as Effective state "In inheritable I<br/>(needs file fI at exec)" as Inheritable state "In ambient A<br/>(survives exec of ordinary file)" as Ambient state "Dropped from X<br/>(unreachable forever)" as Barred Bounded --> Permitted : execve of a file whose fP has the bit Permitted --> Effective : capset — E must be a subset of P Effective --> Permitted : capset — lower the E bit Permitted --> Inheritable : capset — needs CAP_SETPCAP, and X Inheritable --> Ambient : PR_CAP_AMBIENT_RAISE — needs P and I Ambient --> Permitted : execve of an ordinary file adds pA to pP Permitted --> Bounded : capset — drop from P Ambient --> Bounded : execve of a file with caps or setuid clears A Bounded --> Barred : PR_CAPBSET_DROP — needs CAP_SETPCAP Barred --> [*]
The life cycle of one capability bit across the five sets. What it shows: every legal move, and which system call performs it. The insight to take: two of these edges are irreversible and they are the ones that matter for hardening. Bounded → Barred (PR_CAPBSET_DROP) can never be undone, by anything, for this thread or any descendant — this is what container runtimes use. Permitted → Bounded is reversible only by execve of a file that carries the capability, which is why “drop privileges early, then exec nothing privileged” is a real guarantee rather than a convention.
The execve() Transition, Symbol by Symbol
execve(2) is the only moment capabilities can increase, so it is the only place the model can go wrong. The kernel computes five new masks from the old five plus three file attributes. This is the canonical formula, verbatim from capabilities(7):
P'(ambient) = (file is privileged) ? 0 : P(ambient)
P'(permitted) = (P(inheritable) & F(inheritable)) |
(F(permitted) & P(bounding)) | P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable) [i.e., unchanged]
P'(bounding) = P(bounding) [i.e., unchanged]
Every symbol:
P(x)is the value of thread setxbefore theexecve;P'(x)is its value after.F(x)is a file capability set, read from the executable’ssecurity.capabilityextended attribute. A file has only three:F(permitted)(fP, historically “forced”),F(inheritable)(fI, historically “allowed”), andF(effective)(fE).fEis not a mask — it is a single bit. Andy Lutomirski, introducing the ambient set, called this out as the model’s worst piece of documentation: “The libcap capability mask parsers and formatters are dangerously misleading and the documentation is flat-out wrong. fE is not a mask; it’s a single bit. This has probably confused every single person who has tried to use file capabilities” (LWN 636533). It answers one question: after the new permitted set is computed, is it copied into the effective set, or does the program have to raise its own bits withcapset?- “file is privileged” means the file has capabilities or has the set-user-ID or set-group-ID bit set.
&is bitwise AND (set intersection),|is bitwise OR (set union).
Now read each line as English:
| Line | In words | Consequence |
|---|---|---|
P'(ambient) = (privileged file) ? 0 : P(ambient) | An ordinary file keeps your ambient set; a set-UID or file-capped file wipes it | You can never smuggle ambient bits into a setuid program — that would be a privilege-escalation primitive |
(P(inheritable) & F(inheritable)) | Inheritable bits survive only where the file agrees | This term is zero for every ordinary binary on a normal system, which is why plain inheritance is useless (below) |
(F(permitted) & P(bounding)) | The file grants what it asks for, masked by the bounding set | Dropping a bounding bit permanently blocks that grant, for this thread and all descendants |
| P'(ambient) | Ambient bits land in permitted unconditionally | The Linux 4.3 addition that made inheritance usable |
P'(effective) = F(effective) ? P'(permitted) : P'(ambient) | With fE set, everything permitted is immediately effective; without it, only the ambient bits are | A setcap 'cap_x=p' binary starts with the capability dormant and must call capset to use it |
P'(inheritable), P'(bounding) unchanged | Both pass straight through execve | The bounding set is the durable policy; inheritable is the durable request |
The kernel implements exactly this in bprm_caps_from_vfs_caps() in security/commoncap.c (v6.12), with the formula written in the comment above it:
/*
* pP' = (X & fP) | (pI & fI)
* The addition of pA' is handled later.
*/
new->cap_permitted.val =
(new->cap_bset.val & caps->permitted.val) |
(new->cap_inheritable.val & caps->inheritable.val);
if (caps->permitted.val & ~new->cap_permitted.val)
/* insufficient to execute correctly */
ret = -EPERM;That trailing -EPERM is the capability-dumb binary safety check and it is not decoration. If the file’s fE bit is set — the marker of a program that was converted from set-UID-root without being taught the libcap API — and the bounding set masked away any bit the file asked for in fP, the kernel refuses to run the program at all rather than start it under-privileged. A capability-unaware program cannot detect that it is missing a privilege, so the kernel fails closed on its behalf (capabilities(7)). This is observable; the demonstration is in the failure-modes section below.
Two simplifications are worth memorising because they cover most real systems. First, when a process with nonzero UIDs execs an ordinary binary — no file capabilities, no setuid bit — F(permitted) and F(inheritable) are empty and fE is false, so the formula collapses to P'(permitted) = P'(effective) = P'(ambient). Without an ambient set, that is zero: all capabilities are lost. Second, for UID 0 the kernel emulates traditional UNIX. If the real or effective UID is 0, the file’s inheritable and permitted sets are notionally all ones; if the effective UID is 0 or fE is set, fE is notionally one. The formula then collapses to:
P'(permitted) = P(inheritable) | P(bounding)
P'(effective) = P'(permitted)
— root gets everything except what the bounding set forbids. In commoncap.c this is handle_privileged_root(), whose comment spells out the same arithmetic (pP' = (cap_bset & ~0) | (pI & ~0)), and the whole fixup is switched off by the SECBIT_NOROOT securebit. The line-by-line walk of cap_bprm_creds_from_file() — including the ptrace downgrade, the secureexec decision, and the interaction with [[no_new_privs and Privilege Escalation Control|no_new_privs]] — belongs to Capability Transitions Across execve.
History — a Withdrawn Standard and Twenty-Six Years of Patching
The name “POSIX capabilities” is a misnomer the kernel community keeps for continuity. The design descends from POSIX 1003.1e, a draft standard for security extensions covering capabilities, access control lists, mandatory access control, and auditing. That draft was never ratified — it was formally withdrawn; capabilities(7)’s STANDARDS section says flatly that “no standards govern capabilities, but the Linux capability implementation is based on the withdrawn POSIX.1e draft standard,” linking to an archived copy of draft 1003.1e-990310. The reference user-space library says the same thing from the other side: libcap’s own README describes itself as “a library for getting and setting POSIX.1e (formerly POSIX 6) draft 15 capabilities” (libcap README). Linux capabilities are therefore a de facto standard implemented against an abandoned document, which explains a great deal about the model’s rough edges — and why other systems that started from the same draft (FreeBSD’s Capsicum, Solaris privileges) diverged completely.
timeline title Linux capabilities — the shipped milestones (release dates from cdn.kernel.org) POSIX.1e era (to 1999) : the 1003.1e security-extensions draft is written, then withdrawn : libcap is built against draft 15 and still says so in its README Linux 2.2 (1999) : capabilities land as a per-thread attribute : no way to attach them to a file — a privileged parent must set them 2000 : the sendmail capabilities bug — a setuid-root program's setuid() call fails; fixed in Linux 2.2.16 Linux 2.6.24 (24 Jan 2008) : file capabilities — security.capability xattr, revisions 1 and 2 Linux 2.6.25 (17 Apr 2008) : bounding set becomes per-thread; CAP_MAC_OVERRIDE / CAP_MAC_ADMIN Linux 2.6.26 (13 Jul 2008) : securebits — SECBIT_NOROOT and friends Linux 2.6.33 (24 Feb 2010) : file capabilities become unconditional (CONFIG option removed) Linux 3.2 (2012) : /proc/sys/kernel/cap_last_cap appears : Kerrisk publishes "CAP_SYS_ADMIN - the new root" Linux 4.3 (2 Nov 2015) : the ambient set — inheritance finally works without file caps Linux 4.14 (12 Nov 2017) : v3 namespaced file capabilities (rootid in the xattr) Linux 5.8 (3 Aug 2020) : CAP_PERFMON and CAP_BPF carved out of CAP_SYS_ADMIN Linux 5.9 (12 Oct 2020) : CAP_CHECKPOINT_RESTORE carved out Linux 5.12 (26 Apr 2021) : CAP_SETFCAP required to map UID 0 in a new user namespace Linux 6.3 (24 Apr 2023) : kernel_cap_t becomes a single u64 internally Linux 6.12 LTS (18 Nov 2024) : still 41 capabilities — unchanged on master as of 2026-09-04
Twenty-six years of capability development. What it shows: the feature shipped incomplete in 1999 (no file attachment), took nine years to get file capabilities, and another seven to get a usable inheritance mechanism. The insight to take: every one of these milestones is a repair to a problem the original design created, and the two biggest — file capabilities in 2.6.24 and ambient capabilities in 4.3 — were both attempts to make the same use case work: “let an unprivileged program hand one privilege to a helper it executes.”
Two entries deserve unpacking.
Linux 2.2 shipped capabilities without file capabilities, which sounds like a detail and was in fact a decade-long hole: the only way to start a program with a specific capability set was to have a privileged parent construct it programmatically. capabilities(7) lists the three requirements for a “full implementation” — kernel checks against the effective set, system calls to read and write the sets, and a filesystem that can attach capabilities to an executable — and states plainly that “before Linux 2.6.24, only the first two of these requirements are met.”
The sendmail capabilities bug (2000) is the reason a chunk of the modern kernel exists, and its ghost is still in the source: the PR_SET_SECUREBITS handler in cap_task_prctl() carries the comment “doing anything requires privilege (go read about the ‘sendmail capabilities bug’).” The Sendmail Security Team advisory of 7 June 2000 states the impact without ambiguity: “There is a bug in the Linux kernel capability model for versions through 2.2.15 that allows local users to get root. Sendmail is one of the programs that can be attacked this way” (Bugtraq, 2000-06-07). The mechanism, per the upstream libcap project’s own account, is that an unprivileged process could lower inheritable-set bits, so that a set-user-ID-root program it subsequently executed came up holding only a subset of capabilities. Sendmail then did what every setuid program of the era did — call setuid(getuid()) to drop privilege — and that call failed for want of CAP_SETUID. Sendmail did not check the return value, because on a pre-capabilities UNIX it could not fail. It carried on as root and processed the attacker’s .forward file. The fix in Linux 2.2.16 removed the user’s ability to manipulate the inheritable set that way; sendmail 8.10.2 added a runtime probe and refuses to start on a vulnerable kernel (Sendmail advisory; libcap upstream notes).
The lesson generalises past sendmail, and it is the single most important thing to take from capability history: introducing capabilities turned a call that could not fail into a call that could. Every privilege-dropping sequence written before 1999 assumed setuid() succeeds. The kernel’s securebits machinery, the no_new_privs flag, and the paranoia in the ambient-set design are all downstream of that one class of bug.
Uncertain
Verify: the precise mechanism of the sendmail bug — specifically that the attacker lowered inheritable bits rather than all three sets. Reason: the authoritative sendmail advisory states the impact but not the mechanism; the mechanism above comes from the upstream
libcapproject’s retrospective page, which is primary forlibcap(the project README names it as the project’s home) but is a much later recollection of a 2000-era kernel bug, and secondary retellings elsewhere describe the attack as zeroing all three sets. The security consequence is identical either way. To resolve: read the Linux 2.2.16 patch tokernel/sys.c/include/linux/capability.hagainst 2.2.15.#uncertain
Why the Ambient Set Exists — the Clearest Way to Learn the Whole Model
If you understand why Linux 4.3 needed a fifth capability set, you understand the other four. The motivation is a single sentence from Andy Lutomirski’s request-for-comments posting: “Capability inheritance is basically useless.” (LWN 636533, 12 March 2015).
Here is the failure, straight from the formula. You hold CAP_NET_BIND_SERVICE and want a helper program you execve to hold it too. You put the bit in your inheritable set — that is what “inheritable” sounds like it is for. Then you exec /usr/bin/python3. The permitted term for inheritance is P(inheritable) & F(inheritable), and /usr/bin/python3 has no security.capability attribute at all, so F(inheritable) is zero. The AND is zero. As Lutomirski put it: “If you aren’t root and you execute an ordinary binary, fI is zero, so your capabilities have no effect whatsoever on pP’. This means that you can’t usefully execute a helper process or a shell command with elevated capabilities if you aren’t root.”
The theoretical workaround was to set fI to the full set on every non-setuid executable on the system. Lutomirski’s assessment: “No one does this because it’s a PITA and it isn’t even supported on most filesystems. If you try this, you’ll discover that every nonroot program ends up with secure exec rules, breaking many things.” (Setting fE triggers AT_SECURE, which disables LD_PRELOAD and friends.)
This is not a hypothetical. Jake Edge’s write-up of the debate records Serge Hallyn — one of the capability maintainers — lamenting that “it is still not possible to make ping use capabilities (rather than setuid) by default,” because some filesystems have no extended-attribute support and, at the time, cpio and older tar would silently drop the attribute during packaging (LWN 632520, 11 February 2015). Christoph Lameter, who started the thread, ran a user-space network stack needing raw network access and had been carrying an out-of-tree inheritance patch in production for six years. The same article notes that Nokia’s MeeGo-based N9 phone shipped using inheritable capabilities — the feature had real users working around it in the field.
The design argument is worth preserving because it shows what “adding a capability set” costs. Lameter’s first patch made inheritance a global sysfs setting: write capability numbers to a file, and they are inherited across every execve system-wide. That was rejected as too coarse. Hallyn proposed a per-thread “ambient inheritable” set analogous to the bounding set. Lutomirski attached two conditions: a bit must already be permitted before it can be raised in ambient, and he initially wanted PR_SET_NO_NEW_PRIVS to be a prerequisite — which Lameter argued “would make the patch pointless,” since his workload had to run setuid programs sometimes. The no_new_privs requirement was dropped; the permitted-and-inheritable precondition stayed and became the invariant. Casey Schaufler objected to the whole use case (“You’re getting into pretty sketchy territory using that kind of a programming model in a security enforcing environment”) and, on the broader question, offered the epitaph: “The POSIX scheme is workable, but given that it’s 20 years old and hasn’t developed real traction it’s hard to call it successful.”
What shipped in Linux 4.3 is the design in Lutomirski’s RFC, with three safety properties that are the reason it is not an escalation primitive:
- The invariant. “pA obeys the invariant that no bit can ever be set in pA if it is not set in both pP and pI. Dropping a bit from pP or pI drops that bit from pA.” The kernel enforces this in two places:
PR_CAP_AMBIENT_RAISEreturns-EPERMunless the bit is already raised in both permitted and inheritable, andcap_capset()re-masks the ambient set after every change —new->cap_ambient = cap_intersect(new->cap_ambient, cap_intersect(*permitted, *inheritable))(security/commoncap.c, v6.12). Programs that drop privileges the old way keep working. - Privileged files annihilate it.
P'(ambient) = (file is privileged) ? 0 : P(ambient). In the kernel:if (has_fcap || is_setid) cap_clear(new->cap_ambient);. “You cannot use pA to try to subvert a setuid, setgid, or file-capped program.” - UID transitions clear it. A
setresuidfrom root to non-root unconditionally clears the ambient set, because the pre-existingKEEPCAPS+setresuid+execveidiom was itself an effective privilege drop that programs relied on.
The payoff, in the author’s words: “If you are nonroot but you have a capability, you can add it to pA. If you do so, your children get that capability in pA, pP, and pE. For example, you can set pA = CAP_NET_BIND_SERVICE, and your children can automatically bind low-numbered ports. Hallelujah!”
That is verifiable in four lines. The run below is on the note’s host (kernel 7.1.8, libcap 2.78, 2026-09-04) inside an unprivileged user namespace, with SECBIT_NOROOT set via capsh --secbits=1 so that the UID-0 compatibility fixup does not mask the result. 0x400 is bit 10 — CAP_NET_BIND_SERVICE:
$ unshare -Ur sh -c '
> # A: the capability is in the INHERITABLE set, then we exec an ordinary binary
> capsh --secbits=1 --caps="cap_net_bind_service=eip" -- -c \
> "grep -E \"^Cap(Inh|Prm|Eff|Amb)\" /proc/self/status"
CapInh: 0000000000000400 <- the bit is inheritable ...
CapPrm: 0000000000000000 <- ... and it did not survive the exec
CapEff: 0000000000000000
CapAmb: 0000000000000000
> # B: identical, but also raised in the AMBIENT set
> capsh --secbits=1 --caps="cap_net_bind_service=eip" \
> --addamb=cap_net_bind_service -- -c \
> "grep -E \"^Cap(Inh|Prm|Eff|Amb)\" /proc/self/status"
CapInh: 0000000000000400
CapPrm: 0000000000000400 <- P'(permitted) |= P'(ambient)
CapEff: 0000000000000400 <- P'(effective) = P'(ambient), since fE is false
CapAmb: 0000000000000400 <- and it stays ambient for the next exec too
'Run A is the formula’s inheritance term evaluating to zero because /bin/bash has no fI. Run B is the | P'(ambient) term doing what “inheritable” was always assumed to do.
flowchart TB START["Thread holds CAP_NET_BIND_SERVICE<br/>and wants an exec'd helper to hold it"] START --> Q{"Route?"} Q -->|"put it in pI<br/>(the obvious choice)"| I1["execve(/usr/bin/python3)"] I1 --> I2["term = P(inh) & F(inh)<br/>F(inh) = 0 — no xattr on the file"] I2 --> I3["pP' = 0<br/>capability LOST"]:::bad Q -->|"stamp fI on every binary<br/>(the 2008 workaround)"| W1["setcap cap_net_bind_service+i on ...<br/>every executable?"] W1 --> W2["needs xattr support on every fs;<br/>tar/cpio drop the attribute;<br/>fE triggers AT_SECURE, killing LD_PRELOAD"] W2 --> W3["unworkable in practice<br/>(Hallyn: ping still can't do this)"]:::bad Q -->|"put it in pA<br/>(Linux 4.3+)"| A1["prctl(PR_CAP_AMBIENT_RAISE)<br/>requires the bit in pP AND pI"] A1 --> A2["execve(/usr/bin/python3)"] A2 --> A3["pA' = pA (file is not privileged)<br/>pP' |= pA' ; pE' = pA'"] A3 --> A4["capability KEPT"]:::good A2 --> A5["execve(/usr/bin/sudo) — setuid file"] A5 --> A6["pA' = 0 — ambient wiped<br/>no escalation path"]:::good classDef bad fill:#7a2020,color:#fff,stroke:#c66 classDef good fill:#1f5c2e,color:#fff,stroke:#6c9
The three routes to passing a capability through execve, and why only one works. What it shows: the inheritable set is dead on arrival for ordinary binaries because its transition term is ANDed with a file attribute that is almost never present; the file-attribute workaround fails on packaging and filesystem grounds; the ambient set bypasses the file entirely while being wiped the instant a set-UID or file-capped program is executed. The insight to take: “inheritable” does not mean inherited — it means inheritable if the destination file agrees. Ambient is the set that means what people expect inheritable to mean, and its safety comes entirely from the two red-line rules: it can never exceed permitted ∩ inheritable, and it evaporates on exec of anything privileged.
The ambient set is exposed to userspace through prctl(2): PR_CAP_AMBIENT_RAISE, PR_CAP_AMBIENT_LOWER, PR_CAP_AMBIENT_IS_SET, and PR_CAP_AMBIENT_CLEAR_ALL, all dispatched in cap_task_prctl(). A process can also be locked out of raising ambient bits at all by the SECBIT_NO_CAP_AMBIENT_RAISE securebit — the one securebit added specifically for this feature. Deeper coverage of the ambient set’s interaction with file capabilities is in File Capabilities and Ambient Capabilities.
File Capabilities on Disk — the security.capability Attribute, Byte by Byte
File capabilities are the replacement for the set-user-ID-root binary: instead of a program that becomes all of root, the executable carries a precise list of the privileges it needs. Since Linux 2.6.24 the kernel stores that list in an extended attribute named security.capability, written with setcap(8) and requiring CAP_SETFCAP (capabilities(7); setcap(8)). The semantics of the three file sets, and the reasoning behind the namespaced variant, are developed in File Capabilities and Ambient Capabilities; what this note adds is the wire format, drawn at byte accuracy and confirmed against a real attribute read off a real filesystem.
The attribute has three revisions, all defined in include/uapi/linux/capability.h (v6.12):
| Revision | Magic | Since | Size | Layout |
|---|---|---|---|---|
VFS_CAP_REVISION_1 | 0x01000000 | 2.6.24 | XATTR_CAPS_SZ_1 = 4*(1 + 2*1) = 12 bytes | magic + one (permitted, inheritable) pair — capabilities 0–31 only |
VFS_CAP_REVISION_2 | 0x02000000 | 2.6.25 | XATTR_CAPS_SZ_2 = 4*(1 + 2*2) = 20 bytes | magic + two pairs — the full 64-bit capability space |
VFS_CAP_REVISION_3 | 0x03000000 | 4.14 | XATTR_CAPS_SZ_3 = 4*(2 + 2*2) = 24 bytes | as v2, plus a trailing rootid naming the user namespace the caps belong to |
Every field is __le32 — explicitly little-endian regardless of host byte order, so a filesystem image carries its capabilities correctly to a big-endian machine. magic_etc packs two things: the top byte is the revision (VFS_CAP_REVISION_MASK is 0xFF000000, VFS_CAP_REVISION_SHIFT is 24) and bit 0 (VFS_CAP_FLAGS_EFFECTIVE, value 0x000001) is the file effective bit fE. Sizes are validated exactly: get_vfs_caps_from_disk() returns -EINVAL if the attribute’s length does not match its declared revision, which makes a truncated or forged attribute a hard failure rather than a partial read.
packet-beta 0-31: "magic_etc — top byte 0x03 = VFS_CAP_REVISION_3, bit 0 = fE (effective)" 32-63: "data[0].permitted — fP for capabilities 0..31" 64-95: "data[0].inheritable — fI for capabilities 0..31" 96-127: "data[1].permitted — fP for capabilities 32..63" 128-159: "data[1].inheritable — fI for capabilities 32..63" 160-191: "rootid — v3 only; UID that is root in the owning user namespace"
The security.capability extended attribute, version 3 (24 bytes = 192 bits), field by field. What it shows: the whole file-capability mechanism is six little-endian 32-bit words. Two words hold the permitted mask, two the inheritable mask (split low/high because the capability space outgrew 32 bits at 2.6.25), one word carries the revision and the single fE bit, and version 3 appends one more word identifying whose root the capabilities belong to. The insight to take: there is no effective mask on a file — fE is one bit of magic_etc, and this is the single most misunderstood thing about file capabilities. Truncate the last word and you have a valid v2 attribute; that is exactly what the kernel hands back to a reader inside the matching namespace.
That is not a reconstruction from headers. Here is a live attribute, produced by running setcap inside an unprivileged user namespace on this note’s host (uid 1000 mapped to 0) and then reading the raw bytes back from the initial namespace:
$ unshare -Ur sh -c 'setcap cap_net_bind_service=ep ./mycat' # uid 1000 -> 0 inside
$ getcap ./mycat
./mycat cap_net_bind_service=ep
$ getfattr -n security.capability --only-values ./mycat | xxd -g4
00000000: 01000003 00040000 00000000 00000000 ................
00000010: 00000000 e8030000 ........Decoding it against the diagram, remembering that every word is little-endian:
offset bytes value (host order) meaning
------ ----------- ------------------ ----------------------------------------------
0x00 01 00 00 03 0x03000001 VFS_CAP_REVISION_3 | VFS_CAP_FLAGS_EFFECTIVE
-> v3 attribute, fE = 1
0x04 00 04 00 00 0x00000400 fP low word: bit 10 = CAP_NET_BIND_SERVICE
0x08 00 00 00 00 0x00000000 fI low word: empty
0x0c 00 00 00 00 0x00000000 fP high word (caps 32..63): empty
0x10 00 00 00 00 0x00000000 fI high word: empty
0x14 e8 03 00 00 0x000003e8 = 1000 rootid: host UID 1000 is "root" for these caps
Twenty-four bytes, and every one of them is accounted for. Two things this demonstrates that prose alone cannot:
The v3 conversion is automatic and invisible. The command run was an ordinary setcap cap_net_bind_service=ep, which asks for a v2 attribute. The kernel rewrote it. cap_convert_nscap() in commoncap.c implements the rule: the writer must pass capable_wrt_inode_uidgid(idmap, inode, CAP_SETFCAP); then, if the request is v2-sized on a non-idmapped mount and the writer holds CAP_SETFCAP in the namespace that mounted the filesystem, the v2 attribute is stored as-is — otherwise the kernel allocates a struct vfs_ns_cap_data, stamps rootid with the writer’s namespace root translated into the filesystem’s namespace, and stores that. A process that is root only inside its own user namespace cannot mint host-wide privilege; it can only mint privilege scoped to namespaces where its own UID is root.
The translation runs in both directions. Reading the same file from inside a user namespace with the same mapping returns a 20-byte v2 attribute with the rootid stripped — 0x0100000200040000000000000000000000000000. capabilities(7) describes this as deliberate: the simplification “means that no changes are required to user-space tools (e.g. setcap(1) and getcap(1)) in order for those tools to be used to create and retrieve version 3 security.capability attributes.” A tool written in 2010 works unmodified against a 2017 format.
At execve time the reverse check is rootid_owns_currentns(), a five-line walk that decides whether these capabilities apply to you:
static bool rootid_owns_currentns(vfsuid_t rootvfsuid)
{
kroot = vfsuid_into_kuid(rootvfsuid);
for (ns = current_user_ns();; ns = ns->parent) {
if (from_kuid(ns, kroot) == 0)
return true;
if (ns == &init_user_ns)
break;
}
return false;
}Read it: take the kernel UID recorded in rootid (1000, above); walk from your own user namespace toward the initial one; if at any level that UID translates to 0, the capabilities are yours. Otherwise get_vfs_caps_from_disk() returns -ENODATA and the file is treated as having no capabilities at all — silently, with no error, which is a failure mode in its own right.
The design that shipped is not the one first proposed. LWN’s June 2017 write-up describes Stefan Berger’s approach of decorating the attribute name, storing the caps under security.capability@uid=1000, and records Casey Schaufler and James Bottomley objecting on the grounds that a UID is the wrong key when container UIDs are allocated dynamically (LWN 726816). What merged in 4.14 instead was Serge Hallyn’s v3-value approach, whose posting states the threat model exactly: “Root in a non-initial user ns cannot be trusted to write a traditional security.capability xattr. If it were allowed to do so, then any unprivileged user on the host could map his own uid to root in a private namespace, write the xattr, and execute the file with privilege on the host.” His stated goal is the practical one: “This allows a simple setxattr to work, allows tar/untar to work, and allows us to tar in one namespace and untar in another while preserving the capability, without risking leaking privilege into a parent namespace” (LWN 721396).
The Programming Interface — capget, capset, prctl, and libcap
Two system calls read and write a thread’s capability sets directly: capget(2) and capset(2). They are deliberately low-level, and glibc provides no wrappers, so a program must reach them through syscall(2) (capget(2)):
int syscall(SYS_capget, cap_user_header_t hdrp, cap_user_data_t datap);
int syscall(SYS_capset, cap_user_header_t hdrp, const cap_user_data_t datap);The structures, from the uapi header:
typedef struct __user_cap_header_struct {
__u32 version; /* must be _LINUX_CAPABILITY_VERSION_3 */
int pid; /* target thread (0 == self) */
} *cap_user_header_t;
struct __user_cap_data_struct {
__u32 effective;
__u32 permitted;
__u32 inheritable;
};Line by line. version selects the ABI revision and must be _LINUX_CAPABILITY_VERSION_3 (0x20080522) on any modern kernel; _VERSION_1 (0x19980330, one 32-bit word) and _VERSION_2 (0x20071026, marked “deprecated — use v3” in the header itself) are legacy. Because there are more than 32 capabilities, version 3 uses two 32-bit words per set (_LINUX_CAPABILITY_U32S_3 == 2), so userspace passes an array of two __user_cap_data_struct: index 0 carries capabilities 0–31 and index 1 carries 32–63. The header supplies the split arithmetic — CAP_TO_INDEX(x) is x >> 5 and CAP_TO_MASK(x) is 1U << (x & 31). pid names the target thread; capget may read any process, but for capset “the only permitted values for hdrp->pid are 0 or, equivalently, the value returned by gettid(2)” — you may only modify yourself. The kernel enforces it in SYSCALL_DEFINE2(capset, ...) in kernel/capability.c, whose header comment records the history: “The ability to [modify] any other process(es) has been deprecated and removed.”
The version field doubles as a capability-probe protocol. cap_validate_magic() switches on it: a known-but-legacy value logs a one-time warn_legacy_capability_use() / warn_deprecated_v2() message and proceeds; an unknown value causes the kernel to write its own supported version back into your header and return -EINVAL. Passing deliberate garbage and reading back the header is the documented way to ask a kernel what it speaks — surprising the first time it silently rewrites your struct.
capset enforces four monotonicity rules, and reading them in the kernel (cap_capset() in commoncap.c) is more precise than reading them in prose:
Rule (in cap_capset) | Effect | Why |
|---|---|---|
Without CAP_SETPCAP: new I ⊆ (old I ∪ old P) | You may only make inheritable what you already hold or already inherit | Prevents manufacturing an inheritable bit out of nothing |
| New I ⊆ (old I ∪ old X) | The bounding set caps the inheritable set too | Since 2.6.25 — closes the “keep it in I, gain it later via a file’s fI” bypass |
| New P ⊆ old P | A capability can never be added to the permitted set | The core non-escalation property; only execve may add |
| New E ⊆ new P | You cannot make effective what you do not hold | Definitional |
| Then: A ← A ∩ (new P ∩ new I) | Ambient bits auto-drop when their support disappears | Enforces the ambient invariant on every write |
Violations return -EPERM. The consequence is worth stating plainly: dropping a capability is meaningful. Once a bit leaves the permitted set, the only route back is an execve of a file that grants it — and if you also dropped it from the bounding set with prctl(PR_CAPBSET_DROP) (which needs CAP_SETPCAP, and is implemented as a bare cap_lower(new->cap_bset, cap) in cap_prctl_drop()), there is no route back at all, for this thread or any descendant.
libcap and the Command-Line Tools
Because the raw interface is awkward — no glibc wrapper, manual word splitting, version probing — essentially all real code uses libcap, the reference library maintained by Andrew G. Morgan at git.kernel.org, which capget(2) explicitly recommends. Its C interface centres on an opaque cap_t: cap_get_proc() reads the current thread’s sets, cap_set_flag() raises or lowers bits in a chosen set, cap_set_proc() writes the result back through capset.
libcap defines two text formats, both specified in cap_text_formats(7), and confusing them is a common source of wasted time.
| Format | Used by | Syntax | Example |
|---|---|---|---|
| Capability set (from the POSIX.1e draft) | setcap, getcap, capsh --caps=, cap_from_text(3) | comma-separated names (or all), then operator =, +, -, then flags e (effective), i (inheritable), p (permitted); clauses applied left to right | cap_net_bind_service=ep — clear everything, then raise this cap in effective and permitted |
| IAB tuple (a pure Linux extension) | pam_cap(8), captree(8), capsh --iab= | comma-separated capabilities, each prefixed: nothing or % = Inheritable, ! = Bounding (read it as blocked), ^ = Ambient (implies %) | !cap_chown,^cap_net_bind_service — block CAP_CHOWN in the bounding set; make CAP_NET_BIND_SERVICE inheritable and ambient |
Two traps in the first format. = resets the named capabilities in all three sets before applying the trailing flags, so all= (or bare =) is the empty set; + and - require an explicit capability list. And because e looks like a set, people write setcap 'cap_x=p' and then wonder why the program still gets EPERM — that is the file effective bit left at zero, meaning the capability lands in the process’s permitted set dormant and the program must raise it itself with capset.
The tools that ship with libcap:
setcap— write a file’s capabilities.setcap 'cap_net_bind_service=ep' /usr/bin/myserver. RequiresCAP_SETFCAP.getcap— read a file’s capabilities, in the set text format.getpcaps— read a running process’s sets, by PID.capsh— a capability-aware shell wrapper:--drop=removes bounding bits,--caps=sets P/E/I,--addamb=raises ambient bits,--secbits=sets securebits,--decode=turns a hex mask into names,--printdumps everything. Indispensable for testing, and the source of every experiment in this note.captree,pscap,netcap,filecap— survey tools for finding what on a system actually holds capabilities.
Reading Capabilities Out of /proc
/proc/PID/status is the ground truth for a running process, and /proc/PID/task/TID/status for an individual thread (capabilities are per-thread, and the process file shows only the main thread’s). Five fields matter, each a 16-hex-digit mask:
$ grep -E '^Cap|^NoNewPrivs|^Seccomp' /proc/self/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
$ capsh --decode=0x000001ffffffffff | tr ',' '\n' | wc -l
41That is an ordinary login shell on the note’s host: it holds nothing (CapPrm, CapEff, CapInh, CapAmb all zero) but its ceiling is untouched — CapBnd is 0x1ffffffffff, all 41 bits, which is CAP_FULL_SET (CAP_VALID_MASK is defined as BIT_ULL(CAP_LAST_CAP+1)-1). Decode any of these with capsh --decode=. A note on reading old documentation: before Linux 3.8 these fields showed nonexistent capabilities as enabled; since 3.8 everything above CAP_LAST_CAP reads as 0, which is why a modern full bounding set is 0x1ffffffffff and not 0xffffffffffffffff.
Failure Modes — “I ran setcap and it still gets EPERM”
Capabilities fail in ways that produce no log line, no error, and no clue. The list below is ordered by how often it wastes an afternoon, and the first two were reproduced on this note’s host rather than repeated from documentation.
1. The filesystem is mounted nosuid — file capabilities are silently ignored. This one cost real time during the writing of this note. The identical experiment run under /tmp gave CapPrm: 0000000000000000 with no error; run under /home it worked. The reason is one sentence in capabilities(7): “during the capability transitions described above, file capabilities may be ignored (treated as empty) for the same reasons that the set-user-ID and set-group-ID bits are ignored; see execve(2).” MS_NOSUID is chief among those reasons, and modern distributions mount /tmp, /dev/shm, and /run with it by default — Fedora 44’s /tmp line in /proc/self/mountinfo reads rw,nosuid,nodev. The same clause covers a kernel booted with no_file_caps. Diagnostic: findmnt -no OPTIONS -T /path/to/binary. If nosuid appears, getcap will happily show the capability and execve will just as happily ignore it.
2. The bounding set masked a bit the file asked for — and the exec fails outright. Not “starts unprivileged”: fails. This is the capability-dumb-binary safety check, and it is easy to hit inside a hardened container that dropped bounding bits. Reproduced:
$ getcap ./mycat
./mycat cap_net_bind_service=ep
$ capsh --secbits=1 --caps="" -- -c './mycat /proc/self/status | grep -E "^Cap(Prm|Eff)"'
CapPrm: 0000000000000400 <- file caps applied normally
CapEff: 0000000000000400
$ capsh --secbits=1 --drop=cap_net_bind_service --caps="" -- -c './mycat /proc/self/status'
/bin/bash: line 1: ./mycat: Operation not permittedbprm_caps_from_vfs_caps() returned -EPERM because caps->permitted had a bit that X & fP could not deliver, and fE was set. The kernel refuses to start a program that cannot tell it is under-privileged. Diagnostic: compare getcap output against CapBnd of the parent; capsh --decode= both.
3. The file effective bit is not set. setcap 'cap_net_raw=p' ./prog grants the capability dormant: P'(effective) = F(effective) ? P'(permitted) : P'(ambient), and with fE false and no ambient set, the new effective set is empty. The program holds the capability and cannot use it until it calls cap_set_proc() to raise it. This is correct and intentional — it is how a capability-aware program keeps privilege dormant except during the one syscall that needs it — but it is a trap for anyone who did not write the program. Diagnostic: getcap shows =p rather than =ep.
4. The attribute did not survive packaging or copying. Extended attributes are not copied by default. cp needs --preserve=xattr (or -a), rsync needs -X, tar needs --xattrs --xattrs-include='security.*', and container image layers built by tools that do not preserve security.* will drop file capabilities on the floor. This is not a historical footnote: it is precisely why, per Serge Hallyn in LWN 632520, ping still could not be shipped with capabilities instead of set-UID in 2015 — cpio and older tar lost the attribute. Diagnostic: getfattr -n security.capability ./binary on the deployed artefact, not the built one.
5. A v3 attribute whose rootid does not map into your namespace — silent no-op. get_vfs_caps_from_disk() returns -ENODATA when rootid_owns_currentns() is false, and -ENODATA is indistinguishable from “this file has no capabilities.” A binary stamped inside one container’s user namespace confers nothing in another container with a different UID mapping, and nothing on the host. Diagnostic: read the raw attribute from the initial namespace (getfattr -n security.capability --only-values f | xxd), decode the last word, and compare it against the uid_map of the namespace you are executing in.
6. Capabilities are per-thread, and the process you are looking at has many. capabilities(7)’s second paragraph says it outright: “Capabilities are a per-thread attribute.” capset(2) can only modify the calling thread. In a multithreaded runtime — a Go program, a JVM, anything with a thread pool — dropping capabilities on one thread leaves the others fully privileged, and /proc/PID/status shows only the main thread. Diagnostic: grep CapEff /proc/PID/task/*/status | sort -u; more than one distinct value means a partial drop.
7. Confusing a UID change with a privilege drop. Setting a non-root UID does not by itself guarantee capabilities are gone — the process may have used PR_SET_KEEPCAPS/SECBIT_KEEP_CAPS, may hold ambient capabilities, or may have exec’d a file-capped binary. Conversely, a UID-0 process with an empty permitted set has no privileges at all. Diagnostic: read CapEff, never Uid.
8. Version-field mistakes with capset. Passing _LINUX_CAPABILITY_VERSION_1 on a kernel with capabilities above bit 31 truncates the high word silently. Passing an unknown version returns -EINVAL and overwrites your header’s version field with the kernel’s — by design, but startling.
9. The capability granted is root in disguise. The most consequential failure is not mechanical. Roughly a third of the table above escalates trivially: CAP_SETUID is setuid(0); CAP_SYS_MODULE is arbitrary ring-0 code; CAP_DAC_OVERRIDE is write access to /etc/shadow; CAP_SYS_PTRACE is code injection into any root process; CAP_SETFCAP is “stamp cap_setuid=ep onto a file and run it.” Kerrisk’s verdict on the worst of them stands: “CAP_SYS_ADMIN has become the new root. If the goal of capabilities is to limit the power of privileged programs to be less than root, then once we give a program CAP_SYS_ADMIN the game is more or less over” (LWN 486306). At Linux 3.2 that article counted 451 of 1,167 capability checks (over a third) as CAP_SYS_ADMIN; restricting to non-driver, x86-only code it was still 167 of 552, about 30% — up from 23 of 147 (16%) when capabilities were introduced in Linux 2.2. See CAP_SYS_ADMIN and the Capability Granularity Problem.
flowchart TB S["setcap succeeded, but the program still gets EPERM"] --> Q1{"Does execve fail<br/>with EPERM outright?"} Q1 -->|"yes"| A1["Bounding set masked a bit in fP while fE is set<br/>-> capability-dumb safety check<br/>FIX: restore the bounding bit, or clear fE"] Q1 -->|"no, it runs"| Q2{"getcap shows the caps?"} Q2 -->|"no"| A2["Attribute lost in packaging/copy<br/>FIX: cp -a / rsync -X / tar --xattrs"] Q2 -->|"yes"| Q3{"findmnt shows nosuid<br/>on that mount?"} Q3 -->|"yes"| A3["File caps ignored, silently<br/>FIX: move the binary off /tmp, /run, /dev/shm"] Q3 -->|"no"| Q4{"CapPrm nonzero at runtime?"} Q4 -->|"no"| A4["v3 rootid does not map into this user namespace<br/>-> get_vfs_caps_from_disk returned -ENODATA<br/>FIX: re-stamp inside the right namespace"] Q4 -->|"yes, but CapEff is zero"| A5["fE bit not set — capability is dormant<br/>FIX: setcap '...=ep', or call cap_set_proc()"] Q4 -->|"yes, and CapEff has the bit"| Q5{"Multithreaded?"} Q5 -->|"yes"| A6["Check every /proc/PID/task/*/status<br/>capabilities are PER-THREAD"] Q5 -->|"no"| A7["Not a capability problem:<br/>check the LSM (SELinux/AppArmor denials),<br/>seccomp, and the inode uid/gid mapping"]
A diagnostic decision tree for the commonest capability failure. What it shows: six distinct root causes that all present identically as “the privileged operation returns EPERM,” separated by cheap tests you can run in order. The insight to take: every branch except the last is a place where the kernel deliberately fails silently — an ignored attribute, an unmapped rootid, a dormant effective bit. Capabilities have almost no diagnostics of their own, so the debugging technique is always the same: read getcap on the file, read CapPrm/CapEff/CapBnd on the process, and compare.
Alternatives and When to Choose Them
Capabilities are one layer in a stack of confinement mechanisms and are rarely the whole answer. The honest framing is that capabilities answer which privileged operations a process may perform, and every neighbouring mechanism answers a different question.
| Mechanism | The question it answers | Choose it when | Weakness relative to capabilities |
|---|---|---|---|
| set-user-ID-root binary | “Who does this program run as?” | Essentially never in new code — this is what capabilities replace | All-or-nothing. ping moving to cap_net_raw=ep is the canonical win |
| POSIX capabilities | “Which privileged operations may this process perform?” | You need one or two root-like powers and nothing else | No object dimension; ~a third of the bits are root-equivalent |
| seccomp-BPF | “Which system calls, with which argument values, may this process make?” | Shrinking kernel attack surface; sandboxing untrusted code | Cannot express “may bind port 443”; argument inspection cannot follow pointers |
| SELinux / AppArmor (mandatory access control) | “Which objects may this subject touch?” | You need per-object policy, or defence in depth over capabilities | Requires policy authoring and a labelled filesystem; capability checks are themselves LSM hooks, so an LSM can only subtract |
| Landlock | “Which parts of the filesystem and network may I restrict myself to?” | An unprivileged process wants to sandbox itself with no administrator involvement | Versioned ABI levels; coverage is still narrower than a full MAC |
| User Namespaces | “Privileged with respect to what?” | You want a full capability set that is meaningless outside a boundary — the basis of Rootless Containers | Scopes privilege rather than slicing it; only virtualised resources are covered |
| Remove the need for privilege | “Does the kernel still require a privilege here?” | Always check first | — |
That last row is not a joke, and ping is again the example. Rather than granting CAP_NET_RAW, the kernel gained an unprivileged ICMP datagram socket type gated by a sysctl. Documentation/networking/ip-sysctl.rst (v6.12) documents it: ping_group_range takes two integers and “restrict[s] ICMP_PROTO datagram sockets to users in the group range. The default is 1 0, meaning, that nobody (not even root) may create ping sockets.” Distributions ship a widened range, and on such a system ping needs neither set-UID nor a capability. The cheapest capability is the one you discover you do not need.
The layers compose, and a hardened daemon in 2026 typically uses four at once: a non-root UID, a minimal capability set (ideally empty), a seccomp allow-list, and an LSM profile — with no_new_privs set so none of it can be undone by an execve. See no_new_privs and Privilege Escalation Control and Landlock vs seccomp vs Namespaces.
Production Notes — What Real Systems Actually Grant
Container runtimes
Container runtimes are the highest-volume consumer of capabilities, and their default is the single most consequential capability policy in existence. Docker/moby drops the full set and grants back a fixed allow-list of 14 capabilities. That list is not a documentation artefact; it is a literal Go slice in oci/caps/defaults.go (moby v28.0.0), and containerd carries a byte-identical defaultUnixCaps() in pkg/oci/spec.go:
| Granted by default (14) | Why a container gets it | Notable |
|---|---|---|
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_FSETID | Package installers and entrypoint scripts manipulate file ownership and modes | CAP_DAC_OVERRIDE is root over the container filesystem’s contents |
CAP_SETUID, CAP_SETGID, CAP_SETPCAP, CAP_SETFCAP | su/gosu-style entrypoints drop to a service account | CAP_SETUID means an in-container process can become container-root at will |
CAP_KILL | Init-like entrypoints signal their children | |
CAP_NET_BIND_SERVICE | Serve on port 80/443 as a non-root user | The one everybody actually wants |
CAP_NET_RAW | ping inside the container | Enables ARP/DNS spoofing on the container network — the most-argued-about default |
CAP_SYS_CHROOT | chroot-based entrypoints | |
CAP_MKNOD | Create device nodes in the container | |
CAP_AUDIT_WRITE | Login-style programs write audit records |
Conspicuously absent: CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_SYS_MODULE, CAP_SYS_PTRACE, CAP_SYS_RAWIO, CAP_SYS_TIME. Operators adjust with --cap-add / --cap-drop, both of which accept names with or without the CAP_ prefix and the special value ALL (Docker run reference). The hardened idiom is --cap-drop=ALL --cap-add=NET_BIND_SERVICE, or simply --cap-drop=ALL if the service listens above 1024.
Below the runtime, the OCI runtime specification exposes all five sets as separate arrays under process.capabilities — effective, bounding, inheritable, permitted, ambient — alongside process.noNewPrivileges. The spec’s own example is instructive because it uses the sets asymmetrically:
"noNewPrivileges": true,
"capabilities": {
"bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"permitted": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"inheritable": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"effective": ["CAP_AUDIT_WRITE", "CAP_KILL"],
"ambient": ["CAP_NET_BIND_SERVICE"]
}Read against the transition formula: CAP_NET_BIND_SERVICE is ambient, so it survives into any helper the entrypoint executes; the other two are effective now but will vanish at the first execve of an ordinary binary. That is a deliberate split between “privileges this process needs” and “privileges its children need,” and it is only expressible because all five sets are separately addressable.
Kubernetes
Kubernetes surfaces the same controls through a pod’s securityContext.capabilities (see SecurityContext) and constrains them with the Pod Security Standards. The two enforcing levels differ in an easily-missed way:
| Level | Rule | Effective allow-list |
|---|---|---|
| Baseline | Adding capabilities beyond a fixed list is forbidden | 13: AUDIT_WRITE, CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, MKNOD, NET_BIND_SERVICE, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT |
| Restricted (v1.22+) | Containers must drop ALL, and may add back only NET_BIND_SERVICE. Linux-only policy since v1.25 | 1 |
The Baseline list is exactly Docker’s default 14 minus CAP_NET_RAW — Kubernetes concluded that ping-in-a-container is not worth handing every workload the ability to forge packets on the pod network. See Pod Security Standards.
systemd
For services outside containers, systemd exposes the sets declaratively (systemd.exec(5)):
| Directive | Set it touches | Notes |
|---|---|---|
CapabilityBoundingSet= | bounding (X) | Whitespace-separated names; a leading ~ inverts the list; the empty string resets to empty. This is the one-way ratchet |
AmbientCapabilities= | ambient (A) | Added in systemd v229. The documentation records a subtlety worth knowing: “adding capabilities to the ambient capability set adds them to the process’s inherited capability set” (the kernel invariant demands it), and “option keep-caps is automatically added to SecureBits= to retain the capabilities over the user change” |
SecureBits= | securebits | keep-caps, no-setuid-fixup, noroot and their -locked variants |
NoNewPrivileges= | the no_new_privs thread flag | See no_new_privs and Privilege Escalation Control |
AmbientCapabilities= plus User= is the modern idiom that finally retires set-UID-root for daemons: run as an unprivileged account from PID 1 onward, and hand the process exactly the one capability it needs, with no file attribute and no privileged parent step.
See Also
- Capability Sets and the Bounding Set — the five sets in depth, the
securebitsflags, and a worked bounding-set drop - Capability Transitions Across execve —
cap_bprm_creds_from_file()line by line, including the ptrace downgrade andsecureexec - File Capabilities and Ambient Capabilities — the file-side semantics and the ambient set’s interaction with them
- CAP_SYS_ADMIN and the Capability Granularity Problem — why one capability swallowed a third of all checks
- no_new_privs and Privilege Escalation Control — the flag that makes capability-dropping irreversible across
execve - User Namespaces — the reciprocal note: capability checks are namespace-relative, and this is the note that owns that axis
- UID and GID Mapping — the
uid_map/gid_mapmachinery a v3 file capability’srootidis resolved against - Process Credentials and struct cred — where
cap_effective,cap_permitted,cap_inheritable,cap_bsetandcap_ambientphysically live - Discretionary Access Control and setuid setgid and the Sticky Bit — the mode-bit layer
CAP_DAC_OVERRIDEoverrides and the mechanism capabilities replace - Extended Attributes and ACLs — the
security.*xattr namespace that carries file capabilities - The Linux Security Module Framework and LSM Hooks and the security_ Call Sites —
security_capable()is itself an LSM hook - Seccomp and seccomp-BPF, SELinux, AppArmor, Landlock — the complementary confinement layers
- SecurityContext and Pod Security Standards — how Kubernetes requests and constrains all of the above
- Linux Security MOC — the parent map (section B, “Splitting Root — POSIX Capabilities”)
- Linux Containers and Isolation MOC — containers drop most capabilities; this leaf is cross-linked there