CAP_BPF and BPF Privilege Model
Before Linux 5.8, loading almost any extended Berkeley Packet Filter (eBPF) program required
CAP_SYS_ADMIN— the single most powerful Linux capability, a near-equivalent of root that grants mounting filesystems, loading kernel modules, callingreboot(), and dozens of other unrelated god-powers. A networking daemon that only wanted to attach an XDP program had to run with the keys to the entire kingdom. Linux 5.8 (August 2020) fixed this by carving BPF’s privileges out ofCAP_SYS_ADMINinto a small set of fine-grained capabilities:CAP_BPF(the baseline — load most program types, create maps, load BTF), plusCAP_PERFMONfor tracing/observability programs that read kernel memory, andCAP_NET_ADMINfor networking programs that steer packets (capabilities(7); CAP_BPF patch series, Starovoitov 2020). The design follows the principle of least privilege: a BPF-using daemon should hold only the capabilities its specific program type needs, andCAP_SYS_ADMINremains a backward-compatible superset that can still do everything. A handful of system-wide introspection commands still demand fullCAP_SYS_ADMINbecause they bypass per-object confinement.
Kernel version
Every capability check, line number, and helper definition here is read from the Linux 6.12 LTS source tree (released 2024-11-17), and verified identical in 6.18 LTS (released 2025-11-30). The capability-checking logic in
kernel/bpf/syscall.c,include/linux/capability.h, andkernel/bpf/token.cis byte-for-byte the same across both LTS branches as of this writing. Treat behaviour as “as of 6.12/6.18 LTS” — do not assume mainline 7.x is unchanged.
Mental Model: A Privilege Ladder, Not a Switch
The old model was a light switch: either you had CAP_SYS_ADMIN (effectively root) and could do all of BPF, or you had unprivileged BPF (a tiny, locked-down corner — see Unprivileged BPF and Its Restrictions). The new model is a ladder. CAP_BPF is the bottom rung: it unlocks the generic machinery of BPF — creating maps, loading the verifier’s advanced features, loading BTF type information — but deliberately stops short of the two things that make BPF dangerous to the rest of the system. Reading arbitrary kernel memory (what tracing programs do) requires climbing to CAP_PERFMON. Redirecting, dropping, or rewriting network packets (what XDP and tc programs do) requires CAP_NET_ADMIN. And CAP_SYS_ADMIN sits at the top as a backward-compatible superset that implies all of them.
flowchart TB subgraph TOP["CAP_SYS_ADMIN (backward-compatible superset)"] direction TB NOTE["implies CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN<br/>+ system-wide-by-global-ID introspection"] end TOP --> TRACE["CAP_BPF + CAP_PERFMON<br/>tracing: kprobe, tracepoint, fentry,<br/>perf_event, LSM, bpf_probe_read"] TOP --> NET["CAP_BPF + CAP_NET_ADMIN<br/>networking: XDP, tc/SCHED_CLS,<br/>sockmap, devmap, xskmap"] TRACE --> BASE["CAP_BPF (baseline)<br/>create most maps, load BTF,<br/>advanced verifier features,<br/>load non-priv prog types"] NET --> BASE BASE --> UNPRIV["No capability<br/>(only if sysctl allows):<br/>SOCKET_FILTER + CGROUP_SKB only"]
The BPF privilege ladder. What it shows: CAP_BPF is the foundation that every privileged BPF use builds on; tracing and networking each add exactly one more capability on top of it; CAP_SYS_ADMIN implies the whole ladder for backward compatibility. The insight to take: the split lets a daemon request precisely the rung it needs — an XDP loader takes CAP_BPF + CAP_NET_ADMIN and cannot read kernel memory; a tracing agent takes CAP_BPF + CAP_PERFMON and cannot reprogram the NIC. Neither needs the dozens of unrelated powers bundled into CAP_SYS_ADMIN.
The History: Splitting BPF Out of CAP_SYS_ADMIN
Linux capabilities are a partitioning of root’s omnipotence into ~40 distinct bits (CAP_NET_ADMIN, CAP_SYS_MODULE, CAP_DAC_OVERRIDE, and so on), each carried in a thread’s capability sets and checked individually by the kernel. The intent is least privilege: a program that only needs to bind low ports should hold only CAP_NET_BIND_SERVICE, not all of root. In practice, CAP_SYS_ADMIN became a dumping ground — over time so many unrelated operations were gated on it that holding it is essentially equivalent to being root. BPF program loading was one of those operations.
This was a real operational problem. eBPF’s whole value proposition is running sandboxed code safely in the kernel, yet loading that sandboxed code required the least-sandboxed capability in the system. A Cilium agent, a bpftrace session, or a custom XDP load-balancer all had to run as CAP_SYS_ADMIN (or root), which is a far larger attack surface than the task actually warrants.
Alexei Starovoitov’s CAP_BPF patch series (v7, May 2020, merged in Linux 5.8) addressed this by introducing new capabilities and re-gating BPF operations on them (LWN: “capability: introduce CAP_BPF and CAP_TRACING”; patch series cover letter). The capabilities(7) man page records the result plainly: CAP_BPF “was added in Linux 5.8 to separate out BPF functionality from the overloaded CAP_SYS_ADMIN capability,” and identically for CAP_PERFMON and performance monitoring (capabilities(7)).
Naming history
The original patch series proposed
CAP_TRACINGfor the tracing/perf privilege. Before merge it was unified with the perf events subsystem’s needs and renamedCAP_PERFMON(performance monitoring), which also gatesperf_event_open(2). Secondary sources written against the early patches still sayCAP_TRACING; the capability that actually shipped in 5.8 and exists in 6.12/6.18 isCAP_PERFMON(capabilities(7)).
Crucially, the split preserved backward compatibility: every operation newly gated on CAP_BPF/CAP_PERFMON/CAP_NET_ADMIN is also permitted to a holder of CAP_SYS_ADMIN. Existing software that ran BPF as root kept working unchanged; only software that wanted to drop privilege had to learn the new model.
How the Superset Logic Actually Works
The backward-compatibility “CAP_SYS_ADMIN implies everything” rule is not handwaving — it is two short inline helpers in include/linux/capability.h. As of 6.12 (and identical in 6.18):
static inline bool perfmon_capable(void)
{
return capable(CAP_PERFMON) || capable(CAP_SYS_ADMIN);
}
static inline bool bpf_capable(void)
{
return capable(CAP_BPF) || capable(CAP_SYS_ADMIN);
}Line by line: bpf_capable() returns true if the current thread holds either CAP_BPF or CAP_SYS_ADMIN. perfmon_capable() is the same pattern for CAP_PERFMON. So a process with CAP_SYS_ADMIN automatically satisfies every bpf_capable() and perfmon_capable() check without holding the narrower bits — that is the backward-compatibility guarantee, encoded in an ||.
The same superset rule extends to CAP_NET_ADMIN and to BPF token-based delegation through bpf_token_capable() in kernel/bpf/token.c:
static bool bpf_ns_capable(struct user_namespace *ns, int cap)
{
return ns_capable(ns, cap) || (cap != CAP_SYS_ADMIN && ns_capable(ns, CAP_SYS_ADMIN));
}
bool bpf_token_capable(const struct bpf_token *token, int cap)
{
struct user_namespace *userns;
/* BPF token allows ns_capable() level of capabilities */
userns = token ? token->userns : &init_user_ns;
if (!bpf_ns_capable(userns, cap))
return false;
if (token && security_bpf_token_capable(token, cap) < 0)
return false;
return true;
}bpf_ns_capable() checks whether the thread has capability cap in user namespace ns, or — and this is the superset clause — has CAP_SYS_ADMIN in that namespace, unless the requested capability is CAP_SYS_ADMIN (the cap != CAP_SYS_ADMIN guard prevents an infinite “SYS_ADMIN implies SYS_ADMIN” tautology, which is harmless but pointless). So in every BPF capability check, asking “do you have CAP_BPF?” is really asking “do you have CAP_BPF or CAP_SYS_ADMIN?” The bpf_token_capable() wrapper additionally consults the token’s user namespace (for delegated permissions — see BPF Token and Privilege Delegation) and gives the Linux Security Module a veto via security_bpf_token_capable().
What CAP_BPF Alone Grants
CAP_BPF is the baseline privilege — enough to use BPF’s generic machinery, but deliberately not enough to do anything that reaches into the rest of the kernel. From the patch series and the 6.12 source, holding CAP_BPF (and nothing else) lets a process:
- Create most map types. In
bpf_map_create()(kernel/bpf/syscall.c), a large set of map types —BPF_MAP_TYPE_HASH,ARRAY,PERCPU_*,LRU_HASH,LPM_TRIE,RINGBUF,STACK_TRACE,BLOOM_FILTER,STRUCT_OPS,ARENA, and more — are gated behindbpf_token_capable(token, CAP_BPF). (A few “unprivileged” map types likeBPF_MAP_TYPE_ARRAYandHASHneed no capability beyond passing theunprivileged_bpf_disabledgate; a few — sockmap, devmap, xskmap — needCAP_NET_ADMIN.) - Load BTF type information — the
BPF_BTF_LOADcommand checksbpf_token_capable(token, CAP_BPF)before accepting a BTF blob. BTF is the foundation of CO-RE portability; see BTF (BPF Type Format). - Load non-tracing, non-networking program types through
BPF_PROG_LOAD. - Retrieve translated and JITed program code. In
bpf_prog_get_info_by_fd(), the disassembly fields (jited_prog_len,xlated_prog_len, JITed ksyms) are zeroed out unlessbpf_capable()is true — so reading back a program’s compiled form is aCAP_BPFprivilege, not available to fully-unprivileged callers.
The CAP_BPF / CAP_PERFMON boundary moved after the 2020 split
The original 2020 patch series advertised a list of verifier relaxations under
CAP_BPF: indirect variable-offset stack access, bounded loops, BPF-to-BPF calls, pointer-to-integer conversions, andbpf_spin_lock(). That boundary has since shifted. As of 6.12, the two relaxations that leak information gate onCAP_PERFMON, notCAP_BPF: ininclude/linux/bpf.h,bpf_allow_ptr_leaks()(pointer-to-integer conversions / pointer leaks) andbpf_allow_uninit_stack()(reading uninitialized/indirect stack) both returnbpf_token_capable(token, CAP_PERFMON). This note follows the 6.12 source, where those two areCAP_PERFMONprivileges (see below). Treat the 2020 patch’s feature list as historical; verify any specific relaxation against the running kernel’sverifier.c/bpf.hrather than the mailing-list description, since the line between the two capabilities has been adjusted over releases.
The cover letter’s framing is that “CAP_BPF is the safest from security point of view and harmless on its own” (patch series). The reasoning: a process with only CAP_BPF can create maps and load generic programs, but two such processes are isolated from each other unless they explicitly share file descriptors — one cannot reach into another’s maps, and neither can read arbitrary kernel memory or touch the network.
What Needs CAP_PERFMON: Tracing and Reading Kernel Memory
Tracing program types are the ones that read kernel memory — the entire point of kprobe, tracepoint, fentry/fexit, perf_event, and BPF-LSM programs is to observe kernel state. That is exactly the power that can leak secrets, so it is gated above the CAP_BPF baseline on CAP_PERFMON. The check lives in bpf_prog_load():
if (is_perfmon_prog_type(type) && !bpf_token_capable(token, CAP_PERFMON))
goto put_token;is_perfmon_prog_type() returns true for BPF_PROG_TYPE_KPROBE, TRACEPOINT, PERF_EVENT, RAW_TRACEPOINT, RAW_TRACEPOINT_WRITABLE, TRACING (which covers fentry/fexit), LSM, STRUCT_OPS, and EXT. To load any of these, a process needs CAP_BPF (to clear the generic load gate) and CAP_PERFMON (to clear this one). CAP_PERFMON also independently gates perf_event_open(2) outside of BPF.
CAP_PERFMON is also what unlocks the dangerous helper functions. The patch series spells it out: with CAP_PERFMON, “bpf_probe_read to read arbitrary kernel memory is allowed” and “bpf_trace_printk to print kernel memory is allowed,” along with pointer-to-integer conversions inside programs. bpf_probe_read (and its typed successors bpf_probe_read_kernel/bpf_probe_read_user) is the helper that dereferences an arbitrary address — see BPF Helper Functions. Without CAP_PERFMON, the verifier refuses to hand a program these capabilities even if it somehow loaded.
There is a deeper, subtler consequence: CAP_PERFMON also relaxes the verifier’s Spectre hardening. In include/linux/bpf.h, several internal predicates gate on CAP_PERFMON:
static inline bool bpf_allow_ptr_leaks(const struct bpf_token *token)
{
return bpf_token_capable(token, CAP_PERFMON);
}
static inline bool bpf_allow_uninit_stack(const struct bpf_token *token)
{
return bpf_token_capable(token, CAP_PERFMON);
}
static inline bool bpf_bypass_spec_v1(const struct bpf_token *token)
{
return cpu_mitigations_off() || bpf_token_capable(token, CAP_PERFMON);
}
static inline bool bpf_bypass_spec_v4(const struct bpf_token *token)
{
return cpu_mitigations_off() || bpf_token_capable(token, CAP_PERFMON);
}A CAP_PERFMON-capable loader is allowed to leak pointers (bpf_allow_ptr_leaks — this is the pointer-to-integer-conversion relaxation), allowed to read uninitialized stack (bpf_allow_uninit_stack), and bypasses the verifier’s Spectre v1 (bounds-check bypass) and Spectre v4 (store-to-load forwarding) speculative-execution mitigations. These are exactly the verifier relaxations the 2020 patch originally listed under CAP_BPF; in 6.12 they live under CAP_PERFMON. The rationale is trust: a process that already holds CAP_PERFMON can legitimately read kernel memory through tracing anyway, so the speculative side channels add no new exposure for it — and removing the mitigations recovers verifier performance and acceptance. For unprivileged loaders the mitigations stay on. This is the precise seam where the capability model meets BPF and Spectre Hardening.
What Needs CAP_NET_ADMIN: Steering Packets
Networking program types — those that drop, redirect, mirror, or rewrite packets — are gated on CAP_NET_ADMIN, the long-standing capability for network configuration (it already gated routing tables, interface flags, and traffic control). The check, again in bpf_prog_load():
if (is_net_admin_prog_type(type) && !bpf_token_capable(token, CAP_NET_ADMIN))
goto put_token;is_net_admin_prog_type() returns true for BPF_PROG_TYPE_SCHED_CLS and SCHED_ACT (the tc classifier/action types — see tc and cls_bpf (Traffic Control Hooks)), XDP (see XDP (eXpress Data Path)), the lightweight-tunnel LWT_* types, SK_SKB, SK_MSG, FLOW_DISSECTOR, the cgroup socket types (CGROUP_SOCK, CGROUP_SOCK_ADDR, CGROUP_SOCKOPT, CGROUP_DEVICE, CGROUP_SYSCTL), SOCK_OPS, and NETFILTER. On the map side, BPF_MAP_TYPE_SOCKMAP, SOCKHASH, DEVMAP, DEVMAP_HASH, and XSKMAP likewise require CAP_NET_ADMIN at creation. So an XDP load-balancer needs CAP_BPF (generic load) plus CAP_NET_ADMIN (packet steering) and, importantly, no CAP_PERFMON — it has no business reading kernel memory.
CGROUP_SKB is the special case
BPF_PROG_TYPE_CGROUP_SKBis treated as unprivileged inis_net_admin_prog_type()(the source comment reads “always unpriv”). It is exempt from theCAP_NET_ADMINrequirement and, together withSOCKET_FILTER, is one of the only two program types loadable with no BPF capability at all (when theunprivileged_bpf_disabledsysctl permits). The reason: a cgroup-SKB program is confined to sockets within a cgroup the loader already controls, so it cannot affect traffic it does not own. See Unprivileged BPF and Its Restrictions.
Why Some Operations Still Demand Full CAP_SYS_ADMIN
The split did not push everything down to fine-grained capabilities. A specific class of commands in kernel/bpf/syscall.c still checks capable(CAP_SYS_ADMIN) directly, with no CAP_BPF fallback. As of 6.12 these are the global-ID and system-wide-introspection commands:
BPF_OBJ_GET_NEXT_ID(iterate every prog/map/link/BTF ID in the system),BPF_PROG_GET_FD_BY_ID,BPF_MAP_GET_FD_BY_ID,BPF_BTF_GET_FD_BY_ID,BPF_LINK_GET_FD_BY_ID(obtain a file descriptor for any object by its global ID),BPF_TASK_FD_QUERY(introspect which BPF program is attached to a given task’s perf-event fd),BPF_ENABLE_STATS(turn on system-wide BPF run-time statistics).
Each begins with the same guard, for example:
if (CHECK_ATTR(BPF_PROG_GET_FD_BY_ID))
return -EINVAL;
if (!capable(CAP_SYS_ADMIN))
return -EPERM;The reason is confinement. The entire fine-grained model rests on an assumption: a CAP_BPF process can only touch the objects it created or was explicitly handed an fd for. Object lifetime and access are mediated by file descriptors (see BPF Object Pinning and Lifetime); you reach a map only by holding its fd. The *_GET_FD_BY_ID family deliberately breaks that — it lets a caller mint a fresh fd for any object in the system by its global integer ID, regardless of who created it. That is system-wide reach, not per-object reach, so it cannot be granted to the narrow CAP_BPF rung without collapsing the confinement guarantee. Likewise, BPF_OBJ_GET_NEXT_ID enumerates everyone’s objects and BPF_ENABLE_STATS is a global toggle with system-wide effect. These are administrator/observer powers (used by bpftool to list and inspect everything on the box), so they correctly remain on CAP_SYS_ADMIN. Writing to the unprivileged_bpf_disabled and bpf_stats_enabled sysctls is similarly CAP_SYS_ADMIN-only (see Unprivileged BPF and Its Restrictions).
Configuration: Granting BPF Capabilities in Practice
A systemd service that loads an XDP program no longer needs User=root. It can drop to an unprivileged user and request exactly the rungs it needs via AmbientCapabilities:
[Service]
ExecStart=/usr/local/bin/xdp-loadbalancer
User=xdplb
# XDP needs the generic-load + packet-steering rungs, nothing more:
AmbientCapabilities=CAP_BPF CAP_NET_ADMIN
# Lock down everything else:
NoNewPrivileges=true
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_XDPLine by line: User=xdplb runs as an unprivileged account; AmbientCapabilities=CAP_BPF CAP_NET_ADMIN grants only the two rungs an XDP loader needs (generic BPF load plus network-admin packet steering) — and not CAP_PERFMON, so even if the binary were compromised it could not load a tracing program to read kernel memory; NoNewPrivileges=true prevents regaining privilege via setuid. A bpftrace/observability agent would instead request AmbientCapabilities=CAP_BPF CAP_PERFMON and no CAP_NET_ADMIN.
To inspect what a running process holds, getpcaps <pid> (from libcap) prints its capability sets; the effective set must contain the relevant bit for the BPF check to pass. Note that capabilities are checked at load/attach time, not at attach-target time — a daemon can load and pin a program while privileged, then drop all capabilities, and the program keeps running.
Failure Modes and Common Misunderstandings
“CAP_BPF lets me do all of BPF.” No. CAP_BPF alone cannot load a kprobe (needs CAP_PERFMON), cannot load an XDP program (needs CAP_NET_ADMIN), and cannot call BPF_PROG_GET_FD_BY_ID (needs CAP_SYS_ADMIN). The single most common error is granting CAP_BPF to a tracing agent and getting -EPERM on BPF_PROG_LOAD because CAP_PERFMON is missing.
-EPERM ambiguity. A bpf() syscall failing with EPERM does not say which capability was missing — the kernel returns the same errno whether you lacked CAP_BPF, CAP_PERFMON, CAP_NET_ADMIN, or CAP_SYS_ADMIN, or were blocked by unprivileged_bpf_disabled. Diagnosis means matching the failing command/program type against the gates above. libbpf’s verbose output and strace -e bpf help localize which command failed.
Capabilities vs. the unprivileged_bpf_disabled sysctl are different gates. Even with unprivileged_bpf_disabled=0 (unprivileged BPF allowed), the per-program-type capability checks still run — the sysctl only governs the fully-unprivileged window. And conversely, holding CAP_BPF bypasses the sysctl entirely (the gate is sysctl_unprivileged_bpf_disabled && !bpf_cap). These two mechanisms are explained in Unprivileged BPF and Its Restrictions; do not conflate them.
User namespaces don’t grant BPF by themselves. Holding CAP_BPF inside an unprivileged user namespace is not, by default, accepted by the core bpf() checks against the init namespace — which is the whole reason BPF Token and Privilege Delegation exists. A CAP_SYS_ADMIN (or CAP_BPF) capability in a child user namespace lets you do user-namespace-scoped things, but loading a program that affects the host needs either host capabilities or a delegated BPF token.
Alternatives and When to Choose Them
- Run as root /
CAP_SYS_ADMIN. Simplest, works everywhere, maximal attack surface. Acceptable for a one-offbpftoolinvocation by an admin; wrong for a long-running daemon. The whole point ofCAP_BPFis to retire this pattern for services. - Fine-grained capabilities (
CAP_BPF+ the one you need). The recommended model for any daemon: least privilege, smaller blast radius if compromised. Costs a little setup (ambient capabilities, knowing which rung each program type needs). - BPF tokens. When the loader itself should be unprivileged and a separate privileged manager grants it scoped BPF rights — typically a container runtime delegating to a workload in a user namespace. See BPF Token and Privilege Delegation. Tokens are 6.9+; not all tooling supports them yet.
- Unprivileged BPF. Effectively dead as a deployment strategy: disabled by default on modern distros and limited to two program types even when on. See Unprivileged BPF and Its Restrictions.
Production Notes
Cilium, Katran, Falco, Pixie, and most production eBPF deployments target the fine-grained model where the platform allows it, though many still ship as privileged for compatibility with older kernels (pre-5.8 has no CAP_BPF) and because dropping to exact capabilities requires every step of the load/attach path to be capability-clean — a single helper or map type that needs a capability the daemon dropped causes a late -EPERM. The practical migration pattern is: start privileged, instrument every bpf() call, then iteratively narrow the capability set until the program loads, attaches, and runs with only CAP_BPF plus its one program-class capability.
A subtle gotcha from the kernel’s own design: because CAP_PERFMON relaxes Spectre hardening and unlocks bpf_probe_read, granting it is genuinely powerful — a CAP_BPF + CAP_PERFMON agent can read essentially any kernel memory through tracing. The patch author’s own framing — that nobody + CAP_BPF + CAP_PERFMON is “safer than typical setup with userid=root” — is a comparison to the old root-everything baseline, not a claim that the capability is harmless. Treat CAP_PERFMON as “can read all kernel memory” and scope it accordingly.
See Also
- Unprivileged BPF and Its Restrictions — the
unprivileged_bpf_disabledsysctl, the two unprivileged program types, and the Spectre-driven lockdown — the other half of the privilege story - BPF Token and Privilege Delegation — delegating scoped BPF rights to unprivileged processes in user namespaces (6.9+)
- BPF and Spectre Hardening — why
CAP_PERFMONrelaxes the verifier’s speculative-execution mitigations - The bpf() Syscall — the multiplexed syscall whose commands these capability checks gate
- eBPF Verifier — the safety engine whose advanced features
CAP_BPFunlocks - BPF JIT Compiler — turns verified programs into native code after the capability checks pass
- BPF Helper Functions —
bpf_probe_readand the kernel-memory helpers gated onCAP_PERFMON - BPF Program Types — the full taxonomy that determines which capability each program needs
- Linux eBPF MOC — parent map of content
- Linux Security MOC — the broader Linux capability and access-control model