CAP_SYS_ADMIN and the Capability Granularity Problem

POSIX capabilities exist to break the monolithic power of UID 0 into roughly forty independently grantable bits, so that a program needing only one privilege does not need all of them. CAP_SYS_ADMIN defeats that purpose. It is the single most overloaded capability in the kernel — a grab-bag gating mount, pivot_root, sethostname, swap, quota control, many ioctls, namespace operations, and historically BPF and performance monitoring — so sprawling that the capabilities(7) manual page itself calls it “the new root” and warns developers: “Don’t choose CAP_SYS_ADMIN if you can possibly avoid it!” (capabilities(7)). Granting it is so close to granting full root that doing so silently undoes the least-privilege gains the whole capability model was built to deliver. This note explains why the kernel ended up with one capability holding a third of all privilege checks, what concretely it gates, and the remediation trend of the 2020s: carving discrete privileges back out of it into CAP_BPF, CAP_PERFMON (both Linux 5.8), and CAP_CHECKPOINT_RESTORE (Linux 5.9).

This note assumes familiarity with the capability model itself — what the per-thread permitted, effective, inheritable, bounding, and ambient sets are, and how a thread acquires capabilities across execve — which is covered in POSIX Capabilities and Capability Sets and the Bounding Set. Here the focus is narrower: the pathology of one specific capability and the design lesson it teaches.

Versions are pinned to the 6.12 long-term-support kernel (released 2024-11-17); the split capabilities CAP_BPF/CAP_PERFMON/CAP_CHECKPOINT_RESTORE were introduced in 5.8 and 5.9 respectively and are long-present in 6.12. Capability numeric values quoted below are from the v6.12 include/uapi/linux/capability.h.

Mental Model — One Capability, Most of Root

The capability model’s promise is decomposition: instead of “you are root, you bypass every check” versus “you are not root, you are bound by every check,” each privileged operation is gated by a narrow, named bit. Binding a port below 1024 needs only CAP_NET_BIND_SERVICE (bit 10); overriding file-permission checks needs CAP_DAC_OVERRIDE (bit 1); changing process priority beyond the normal range needs CAP_SYS_NICE (bit 23). A correctly capability-aware daemon holds the two or three bits it needs and nothing else, so a bug in it cannot, for example, load a kernel module.

CAP_SYS_ADMIN (bit 21) breaks the promise from the other direction. Rather than gating one coherent class of operation, it accreted into a catch-all: when a kernel developer added a new privileged operation and could not find an existing capability that fit, the path of least resistance was to gate it on CAP_SYS_ADMIN. Over decades of such decisions it became the union of dozens of unrelated powers — and because several of those powers (loading code into the kernel via BPF, forging process credentials, controlling devices, manipulating namespaces) are individually enough to escalate to full root, holding CAP_SYS_ADMIN is practically equivalent to holding UID 0.

flowchart TB
  ROOT["UID 0 (legacy)<br/>bypasses every check"]
  ROOT -->|"decomposed into"| CAPS["~40 capabilities<br/>(least-privilege ideal)"]
  CAPS --> NARROW["Narrow caps:<br/>CAP_NET_BIND_SERVICE,<br/>CAP_DAC_OVERRIDE,<br/>CAP_SYS_NICE ..."]
  CAPS --> CSA["CAP_SYS_ADMIN<br/>(the dumping ground)"]
  CSA --> O1["mount / umount / pivot_root"]
  CSA --> O2["sethostname / setdomainname"]
  CSA --> O3["swapon / swapoff / quotactl"]
  CSA --> O4["setns / namespace ops"]
  CSA --> O5["many ioctls, keyctl,<br/>credential forging, ..."]
  CSA -. "carved back out (2020+)" .-> SPLIT["CAP_BPF (5.8)<br/>CAP_PERFMON (5.8)<br/>CAP_CHECKPOINT_RESTORE (5.9)"]
  style CSA fill:#fdd
  style SPLIT fill:#dfd

How CAP_SYS_ADMIN subverts the capability model. What it shows: the model decomposes root into narrow bits, but CAP_SYS_ADMIN re-aggregated a huge, unrelated set of powers — so granting it returns most of root’s reach in a single bit. The insight to take: because several operations under CAP_SYS_ADMIN are individually root-equivalent, granting it is near-indistinguishable from granting UID 0; the green node is the corrective trend of pulling discrete privileges back out into purpose-built capabilities.

Why This Happened — The Path-of-Least-Resistance Failure

The clearest account of how this came to be is Michael Kerrisk’s 2012 LWN analysis, bluntly titled “CAP_SYS_ADMIN: the new root” (Kerrisk 2012). Counting capability uses across the Linux 3.2 source, he found that of 1,167 capability checks in C files, 451 — rather more than a third — were CAP_SYS_ADMIN (Kerrisk 2012). Even after excluding driver code and non-x86 architectures, it still accounted for roughly 30% of the remaining checks. No other capability comes close.

The cause is a coordination failure inherent in the distributed kernel-development model. Kerrisk’s diagnosis: “when random kernel developers are faced with the question ‘What capability should I use?’ the answer often goes ‘I’m not sure… maybe CAP_SYS_ADMIN?’” (Kerrisk 2012). A developer adding a privileged operation must pick a gate. There is no central authority assigning capabilities to features, no required review for the choice, and the existing capability names are vague enough that none obviously fits a novel operation. So, “lacking sufficient information for a decision, the developer chooses CAP_SYS_ADMIN for their new feature” (Kerrisk 2012). Each such choice is locally reasonable and globally corrosive — the capability accretes one more unrelated power, and the next developer sees an even larger precedent for “miscellaneous admin stuff goes here.”

The security consequence is that the capability becomes self-defeating. Kerrisk notes that with CAP_SYS_ADMIN “there are several avenues of attack by which a rogue process could gain all of the other capabilities” (Kerrisk 2012). A process that can load a kernel module, forge credentials, or write to kernel memory via a debugging interface can trivially grant itself any other capability — so a “least-privilege” grant of just CAP_SYS_ADMIN is no restriction at all against a determined adversary inside that process.

The capabilities(7) manual page now bakes this lesson into its “Notes to kernel developers” section, in language worth quoting in full because it is the official statement of the problem:

“Don’t choose CAP_SYS_ADMIN if you can possibly avoid it! A vast proportion of existing capability checks are associated with this capability (see the partial list above). It can plausibly be called ‘the new root’, since on the one hand, it confers a wide range of powers, and on the other hand, its broad scope means that this is the capability that is required by many privileged programs. Don’t make the problem worse.” (capabilities(7))

What CAP_SYS_ADMIN Actually Gates

The capabilities(7) list of operations CAP_SYS_ADMIN permits is long and visibly heterogeneous. Drawing from the man page (capabilities(7)), the major categories are:

  • Filesystem mounting and pivoting. mount(2), umount(2), and pivot_root(2). This single fact is why container runtimes that set up their own root filesystem traditionally needed CAP_SYS_ADMIN, and why a process holding it can mount a filesystem image it controls and execute setuid binaries from it.
  • System identity. sethostname(2) and setdomainname(2) — changing the machine’s hostname and NIS domain.
  • Swap and quotas. swapon(2), swapoff(2), and quotactl(2) — enabling/disabling swap areas and administering disk quotas.
  • Namespaces. setns(2) to join an existing namespace, and broad authority over namespace operations — directly relevant to containers.
  • System V IPC. IPC_SET and IPC_RMID operations on System V IPC objects regardless of ownership.
  • Resource and credential manipulation. Overriding the RLIMIT_NPROC resource limit; forging process credentials passed in SCM_CREDENTIALS over UNIX sockets; exceeding the per-process open-file-descriptor limit.
  • Extended attributes. Setting trusted and security extended attributes.
  • A pile of ioctls and miscellaneous syscalls. Block-device ioctls, filesystem ioctls, the /dev/random ioctl, the TIOCSTI terminal-injection ioctl, madvise(2)’s MADV_HWPOISON, ioprio_set(2) for the idle/realtime I/O classes, fanotify_init(2), several keyctl(2) operations, perf_event_open(2), certain ptrace(2) operations, privileged syslog(2) operations, device-driver administration, and device control groups.
  • Historically: BPF and performance monitoring and checkpoint/restore — now split out (see below), though CAP_SYS_ADMIN retains them for backward compatibility.

Read that list and the pathology is obvious: there is no coherent “system administration” concept that unifies “set the hostname,” “inject characters into a terminal,” “load a BPF program,” “forge credentials over a socket,” and “enable swap.” It is simply where unrelated privileged operations were filed. Each is independently dangerous; several are independently root-equivalent.

A concrete worked example of the root-equivalence: TIOCSTI is an ioctl that pushes characters into a terminal’s input queue as though typed. A process holding CAP_SYS_ADMIN and sharing a controlling terminal with a root shell can inject a command into that shell — instant privilege escalation, gated by the same bit you might have granted a process merely so it could mount(2) a tmpfs. (The independent danger of TIOCSTI was severe enough that the kernel later gained a dev.tty.legacy_tiocsti sysctl to disable it entirely.)

The Remediation Trend — Carving Privileges Back Out

The corrective response, beginning in earnest around 2020, is to split discrete privileges out of CAP_SYS_ADMIN into purpose-built capabilities. Three such splits are now stable and present in the 6.12 kernel. Their numeric values in include/uapi/linux/capability.h (v6.12) place them at the very end of the capability range, reflecting that they are the newest additions:

CapabilityValueAdded inCarves out
CAP_PERFMON38Linux 5.8Performance monitoring (perf_event_open(2), BPF operations with performance implications)
CAP_BPF39Linux 5.8Privileged BPF operations (creating maps, loading programs, BTF)
CAP_CHECKPOINT_RESTORE40Linux 5.9Checkpoint/restore (set ns_last_pid, clone3() set_tid, read other processes’ map_files symlinks)

CAP_LAST_CAP is 40 in v6.12, i.e. CAP_CHECKPOINT_RESTORE is currently the highest-numbered capability.

Uncertain

Verify: that CAP_BPF (value 39) and CAP_PERFMON (value 38) were added in Linux 5.8, and CAP_CHECKPOINT_RESTORE (value 40) in Linux 5.9. Reason: the numeric values are confirmed against the v6.12 capability.h header (38/39/40, CAP_LAST_CAP=40), and the “since Linux 5.8 / 5.9” version claims are confirmed against capabilities(7) on man7.org, but the exact merge commit was not inspected. To resolve: check the git history of include/uapi/linux/capability.h for the commits adding these constants, or the merge windows of the 5.8 and 5.9 release notes.

The capabilities(7) manual page states the rationale for each split in nearly identical wording, which is itself telling — they are three instances of one corrective pattern. For CAP_BPF: “This capability was added in Linux 5.8 to separate out BPF functionality from the overloaded CAP_SYS_ADMIN capability.” For CAP_PERFMON: “This capability was added in Linux 5.8 to separate out performance monitoring functionality from the overloaded CAP_SYS_ADMIN capability.” For CAP_CHECKPOINT_RESTORE: “This capability was added in Linux 5.9 to separate out checkpoint/restore functionality from the overloaded CAP_SYS_ADMIN capability.” (capabilities(7)). The recurring phrase “the overloaded CAP_SYS_ADMIN capability” is the man page acknowledging the design defect in its own normative text.

Crucially, the splits are backward-compatible: CAP_SYS_ADMIN still grants the BPF and performance-monitoring powers it always did. The v6.12 header notes this directly, commenting that CAP_SYS_ADMIN “Allow[s] everything under CAP_BPF and CAP_PERFMON for backward compatibility.” This is the only way to introduce the split without breaking every existing program that relied on CAP_SYS_ADMIN to call bpf(2) or perf_event_open(2). The benefit is forward: a new program that needs only to load BPF programs can be granted CAP_BPF alone — a far narrower grant than CAP_SYS_ADMIN — and a sandbox can drop CAP_SYS_ADMIN from its bounding set while keeping CAP_BPF available, which was impossible when the two were fused.

There is a subtlety the BPF split exposes: BPF’s privilege model is not binary even after the split. Some BPF operations remain gated on CAP_SYS_ADMIN specifically because they are genuinely dangerous — bpf_probe_write_user (writing to arbitrary user memory) and iterating all system-wide loaded programs and maps still require CAP_SYS_ADMIN, not merely CAP_BPF (per the v6.12 capability.h commentary). CAP_PERFMON relaxes the BPF verifier further than CAP_BPF alone (allowing pointer-to-integer conversions and bpf_probe_read of kernel memory), reflecting a deliberate gradation: the more dangerous the read/write reach, the higher the capability required. So the split is not a clean partition but a layeringCAP_BPF for ordinary privileged BPF, CAP_PERFMON for observation-class reads, CAP_SYS_ADMIN still required for the truly arbitrary-memory operations.

Failure Modes and Common Misunderstandings

“I dropped everything except CAP_SYS_ADMIN, so my container is hardened.” This is the central misunderstanding. A process retaining CAP_SYS_ADMIN can mount filesystems, manipulate namespaces, forge credentials, and (via several ioctls) reach root-equivalent power; dropping the other 39 capabilities buys almost nothing while CAP_SYS_ADMIN remains. The single most valuable capability to drop from a container is CAP_SYS_ADMIN. Container hardening guides and the Docker/Kubernetes default capability profiles deliberately exclude it from the default grant for exactly this reason — see Linux Containers and Isolation MOC and the orchestration-side SecurityContext.

CAP_BPF is safe to grant freely now that it’s split out.” Less dangerous than CAP_SYS_ADMIN, yes; safe, no. CAP_BPF still lets a process load programs into the kernel, and BPF program loading has historically been a rich source of local-privilege-escalation CVEs through verifier bugs. The split narrows the intended reach but does not make BPF loading a benign privilege.

Confusing the bounding set with the effective set. Dropping CAP_SYS_ADMIN from a thread’s bounding set is the durable, irreversible reduction — it prevents the capability from ever being regained across execve, even by executing a setuid-root binary or a file with file capabilities. Merely clearing it from the effective set is reversible if the thread still holds it as permitted. For genuine confinement, drop it from the bounding set (and, with [[no_new_privs and Privilege Escalation Control|no_new_privs]], from any execve-based re-acquisition path). The mechanics are in Capability Sets and the Bounding Set.

Alternatives and When to Choose Them

When you are the one granting a privilege to a process, the hierarchy is: grant the narrowest capability that gates the operation you need. If you need to bind a low port, grant CAP_NET_BIND_SERVICE, never CAP_SYS_ADMIN. If you need to load BPF programs, grant CAP_BPF (and CAP_PERFMON only if you need the relaxed verifier reads), never CAP_SYS_ADMIN. If you need raw socket access, CAP_NET_RAW. Reach for CAP_SYS_ADMIN only when the specific operation genuinely has no narrower gate — and treat that as a sign the program is effectively running as root.

When you are the one designing a new privileged kernel operation, the man page’s advice is the rule: create a narrow capability or reuse a tightly-matching existing one; do not file it under CAP_SYS_ADMIN. The CAP_BPF/CAP_PERFMON/CAP_CHECKPOINT_RESTORE splits are the worked examples of doing this retroactively.

Where capabilities are too coarse even after the splits, the answer is a different mechanism entirely. Seccomp filters the syscall surface irrespective of capabilities — it can deny mount(2) to a process that nonetheless holds CAP_SYS_ADMIN. Landlock and the LSM-based MACs (SELinux, AppArmor) constrain which objects an operation may touch, a dimension capabilities cannot express at all. A genuinely hardened service combines all three: drop CAP_SYS_ADMIN, add a seccomp allowlist, confine with a MAC profile.

Production Notes

The practical fallout of CAP_SYS_ADMIN’s overloading is most visible in containers. Running a container with --privileged (Docker) or privileged: true (Kubernetes securityContext) grants the full capability set including CAP_SYS_ADMIN, which is why “privileged container” is treated as roughly equivalent to “root on the host” in threat models. The default (non-privileged) capability set deliberately omits CAP_SYS_ADMIN, which is also why certain operations — mounting filesystems inside the container, some pivot_root-based tooling — fail by default and force the user to either add the capability back (re-opening the hole) or restructure to avoid needing it.

The split capabilities are now used in production to thread this needle. A workload that needs to load BPF programs (an observability agent, a CNI plugin) can be granted CAP_BPF and CAP_PERFMON instead of CAP_SYS_ADMIN, dramatically shrinking its blast radius while still functioning — this is precisely the forward-looking benefit the splits were designed to enable. The historical fusion meant such agents had to run with CAP_SYS_ADMIN, i.e. effectively as root; the split is what made least-privilege BPF tooling possible at all.

See Also