The Problem: CAP_BPF Was Never Enough (or It Was Always Too Much)
If you’ve deployed eBPF tooling in a containerized environment, you’ve hit this wall. Loading a BPF program requires CAP_BPF. Attaching a tracing program needs CAP_PERFMON on top of that. Networking hooks want CAP_NET_ADMIN. Some operations still demand full CAP_SYS_ADMIN.
And here’s the uncomfortable part: these capabilities are system-wide. There is no such thing as “CAP_BPF, but only inside this container.” A process holding CAP_BPF + CAP_PERFMON can attach a kprobe and read arbitrary kernel memory via bpf_probe_read_kernel() — memory belonging to every workload on that host. Kernel state simply isn’t namespaced, so BPF’s view of it can’t be either.
This is why granting CAP_BPF + CAP_PERFMON inside a user namespace doesn’t work — the kernel deliberately checks these capabilities against the initial user namespace. Being “root inside a container” means nothing to the BPF subsystem, by design.
Thus resulting in two possibilities:
Grant broad capabilities to any container that needs eBPF (observability agents, network sidecars, security sensors) — effectively giving it host-level tracing power and weakening isolation.
Ban eBPF in unprivileged contexts entirely — blocking an entire class of legitimate, useful tooling.
Neither is acceptable in a multi-tenant world. This is the gap BPF token was built to close.
What Is a BPF Token?
BPF token, authored by Andrii Nakryiko and merged in Linux 6.9, is an explicit delegation mechanism. Instead of trying to namespace capabilities (fundamentally unsafe for BPF), it lets a privileged, trusted process — a container runtime, systemd, or a platform daemon — mint a precisely scoped permission grant and hand it to an unprivileged process.
The token itself is just a file descriptor, derived from a specially configured BPF filesystem (bpffs) instance. When the unprivileged process makes bpf() syscalls, it passes the token FD alongside the command, and the kernel performs its permission checks against the token’s delegated permission set instead of (only) the caller’s capabilities.
One phrase from the original patch series captures the philosophy: trust is the key here. BPF token is not unconditional unprivileged eBPF. It’s a mechanism for a privileged principal to say: “I trust this specific workload to perform these specific BPF operations, and nothing more.”
The Four Axes of Delegation
The scoping happens at bpffs mount time, via four mount options — each a bitmask over enums straight out of include/uapi/linux/bpf.h:
If a bit isn’t set, the operation is denied — token or no token. The axes compose: allowing prog_load in delegate_cmds but only socket_filter in delegate_progs means the workload can load socket filters and only socket filters. You can hand a container the ability to run XDP programs with array maps while denying it kprobes, perf events, and everything else that would let it snoop on the host.
A configured delegating mount looks like this:
none on /sys/fs/bpf type bpf (rw,relatime,uid=1000000,gid=1000000,
delegate_cmds=map_create:prog_load,
delegate_maps=ringbuf,
delegate_progs=socket_filter,
delegate_attachs=cgroup_inet_ingress)The special value any delegates everything on that axis — useful for development, dangerous as a default.
How It Works: The Delegation Flow
The critical design constraint: a token must be created inside a non-init user namespace, from a bpffs instance bound to that same namespace. The host’s regular /sys/fs/bpf will never produce a usable token. This binding is what prevents a leaked token FD from being replayed outside its intended container boundary.
The full handshake between a container runtime (privileged) and workload (unprivileged) looks like this:
The unprivileged side opens the filesystem context; the privileged side configures the delegation options and instantiates it; the mount travels back over a Unix domain socket. Only then can the workload derive a token with the BPF_TOKEN_CREATE command:
union bpf_attr attr = {
.token_create = {
.flags = 0,
.bpffs_fd = bpffs_mount_fd, /* fd of the delegated bpffs */
},
};
int token_fd = syscall(__NR_bpf, BPF_TOKEN_CREATE, &attr, sizeof(attr));Note that BPF_TOKEN_CREATE still checks the caller for CAP_BPF — but in the caller’s own user namespace, which any userns “root” trivially has. The real gate is the delegation configuration the privileged side baked into the mount.
Sounds intricate? It is — deliberately so. But in practice you almost never write this plumbing yourself. Steps 1–5 are the container runtime’s job (LXD, Incus, and systemd conventions handle this), and step 6–7 are handled transparently by libbpf.
Using Tokens with libbpf: Nearly Transparent
For applications built on libbpf — which is most of the ecosystem — token adoption can be as small as a one-line change, or zero lines:
Option 1 — Explicit path in code:
LIBBPF_OPTS(bpf_object_open_opts, opts,
.bpf_token_path = "/sys/fs/bpf", /* delegated bpffs mount */
);
struct bpf_object *obj = bpf_object__open_file("prog.bpf.o", &opts);
/* libbpf derives the token and passes it to map_create,
btf_load, prog_load automatically */Option 2 — Environment variable, no code change:
LIBBPF_BPF_TOKEN_PATH=/sys/fs/bpf ./my_ebpf_appOption 3 — Implicit default: if /sys/fs/bpf itself is a delegated bpffs in the current user namespace, recent libbpf detects this and uses it automatically.
libbpf’s feature-probing internals were also reworked to be token-aware, so capability detection inside a delegated environment reflects what the token actually permits.
Trying It Today: LXD in Four Commands
The fastest way to experience the end-to-end flow is LXD (≥ 6.2 tooling on a ≥ 6.9 kernel):
lxc launch ubuntu:noble bpf-experiments
lxc config set bpf-experiments limits.kernel.memlock=unlimited
lxc config set bpf-experiments security.delegate_bpf=true
lxc config set bpf-experiments security.delegate_bpf.prog_types=socket_filter
lxc config set bpf-experiments security.delegate_bpf.cmd_types=prog_load:map_create
lxc config set bpf-experiments security.delegate_bpf.map_types=ringbufLXD mounts a pre-configured delegating bpffs inside the container; a libbpf-based app inside just works. Incus exposes the same capability through security.bpffs.delegate_* options. To figure out exactly which types your workload needs, strace -e bpf against the application is your friend — every program type, map type, and attach type shows up in the syscall trace, and you delegate exactly that set.
Why Use BPF Tokens: The Use Cases
1. eBPF-powered agents in Kubernetes and containers. Observability agents, CNI components, and security sensors currently run as privileged DaemonSets almost everywhere. Tokens enable a model where the runtime delegates only the program/map/attach types each agent actually needs. Cilium has an open proposal to consume tokens (checking LIBBPF_BPF_TOKEN_PATH, falling back gracefully on older kernels), which signals where the ecosystem is heading.
2. Multi-tenant platforms. A cloud provider or internal platform can let tenants run their own packet filters or socket-level programs without ever exposing host tracing primitives. The delegation axes are the tenancy boundary.
3. CI/CD pipelines. Testing eBPF programs in CI traditionally meant privileged runners — a supply-chain risk. With tokens, a build job gets exactly the ability to load and exercise the program types under test, nothing more.
4. Safer distro and desktop tooling. There’s active work (a FOSDEM 2026 talk covered this) on distributions adopting tokens so that tracing and observability tools can run without setuid binaries or blanket capability grants.
5. Sandboxed AI agents and untrusted automation. If you’re building sandboxes where semi-trusted code runs — AI agent runtimes, plugin systems, per-customer microVMs — tokens give the supervisor a clean way to grant the sandboxed workload self-observability (say, ringbuf maps and socket filters for its own traffic) while structurally denying kprobes and kernel-memory reads. The permission set is declared once at mount time by the trusted layer, and the kernel enforces it on every syscall; the workload inside can’t negotiate its way past it.
The Security Model: What Tokens Are Not
It’s worth being precise, because “unprivileged eBPF” is a loaded phrase:
Not unconditional unprivileged BPF. Someone privileged must explicitly opt in per-mount, per-axis. If there is No delegating mount then no token, no access.
Namespace-bound. A token FD is tied to the user namespace where it was created and is useless outside it.
LSM-visible. The token patch series added dedicated LSM hooks (and SELinux support) around token creation and use, so system-wide security policy can observe and veto delegation. If you work with LSM-BPF, this is a genuinely interesting enforcement point: policy on who may mint and wield tokens, not just who holds capabilities.
Still trust-based. A workload that can load even a socket filter consumes kernel resources and interacts with the verifier. Delegate the minimum set, and treat
delegate_attachs=anyonly for development and not in production configs.
One operational footnote: memlock. BPF maps consume locked memory, and unprivileged contexts often have tight RLIMIT_MEMLOCK defaults — hence the limits.kernel.memlock=unlimited line in the LXD recipe. Forgetting this is may result in map_create returning -EPERM even if token is fine.
Where Things Stand
Kernel: merged in 6.9 (2024); the initial commands covered
BPF_MAP_CREATE,BPF_BTF_LOAD,BPF_PROG_LOAD, and token support has been threaded consistently through the verifier and helper-availability logic. Follow-up work on finer-grained delegation continues on the lists.libbpf: full high-level support —
bpf_token_path,LIBBPF_BPF_TOKEN_PATH, implicit default-path detection.Runtimes: LXD (
security.delegate_bpf.*) and Incus (security.bpffs.delegate_*) ship first-class support.Ecosystem: systemd-level conventions for exposing delegatable bpffs instances were part of the design discussion from day one; major consumers like Cilium are actively scoping adoption.
Closing Thoughts
BPF token is one of those infrastructure features that’s invisible when it works: an FD, a few mount options, a permission check rerouted. But it dissolves a constraint that has shaped eBPF deployment architecture for a decade — the assumption that anything touching BPF must be (nearly) root.
For eBPF — a subsystem whose power is inherently global — the second question was always the right one to ask. If you ship eBPF software, wiring in token support now (it’s often one environment variable) means your users get least-privilege deployment the moment their platform enables delegation. And if you run the platform, the four delegate_* axes are the sharpest isolation knob the BPF subsystem has ever given you.



