class: center, middle, title-slide # BPF Tokens in Linux Distributions ## A Path to Safe User-Space eBPF .meta[ **Daniel Mellado** Red Hat — Fedora eBPF SIG ] .event[ FOSDEM 2026 · eBPF Devroom Brussels, February 2026 ] .logos[
] ??? **Wait for room to settle.** "Hi everyone! We're here to talk about BPF Tokens — a new kernel feature that lets us delegate eBPF privileges safely, without sudo or over-privileged containers." --- # About This Talk .left-column[ ### The Journey 1. **The Privilege Problem** 2. **What Are BPF Tokens?** 3. **The Anatomy of Delegation** 4. **Demo: eBPF in Rust with Aya** 5. **Live Walkthrough** 6. **Why Distributions Care** ] .right-column[ .token-box[ ### Goal Explain how **BPF Tokens** (merged in Kernel 6.7) allow us to delegate eBPF power safely — without `sudo` or over-privileged containers. ] .quote-box[ eBPF is the "superpower" of modern Linux, but it has a massive security tax. Tokens change that. ] ] .clearfix[] ??? **Hook:** "eBPF is the 'superpower' of modern Linux — tracing, networking, security. But it has a massive security tax: you basically need root to use it." "Today we'll show how BPF Tokens, merged in Kernel 6.7, change that equation." --- class: section-slide, center, middle # The Privilege Problem ## eBPF Has Been a "Root-Only" Club .section-number[1] ??? **Pause briefly.** --- # The eBPF Privilege Barrier ### Historically, eBPF requires serious capabilities: | Capability | Required For | |------------|--------------| | .cap-badge[CAP_SYS_ADMIN] | Legacy catch-all (dangerous!) | | .cap-badge[CAP_BPF] | Loading programs, creating maps | | .cap-badge[CAP_PERFMON] | Perf events, kprobes, uprobes | | .cap-badge[CAP_NET_ADMIN] | XDP, TC, socket programs | .warning-box[ **The Risk:** Giving a container `CAP_SYS_ADMIN` is effectively giving it **root over the host**. ] ??? "When eBPF was first introduced, it required CAP_SYS_ADMIN — way too many privileges just to load a tracing program." "Since kernel 5.8, we have CAP_BPF for more granular control. Network programs need CAP_BPF + CAP_NET_ADMIN. Tracing needs CAP_BPF + CAP_PERFMON." "But here's the catch: even if a process has CAP_BPF inside a user namespace, it still can't load eBPF programs. eBPF can see sensitive kernel behavior — so the kernel doesn't trust unprivileged users, period." --- # The Result: Friction Everywhere .card-grid[ .card[ ### 🔧 Complex Workarounds `setuid` binaries, capability wrappers, privilege escalation dances ] .card[ ### 🐳 Over-Privileged Containers `--privileged` sidecars in Kubernetes just to get observability ] .card[ ### 👩💻 Developer Friction Cannot run `bpftrace` as a normal user — always need `sudo` ] .card[ ### 🍲 Capability Soup Even with `CAP_BPF`, you often need `CAP_PERFMON` + `CAP_NET_ADMIN`... ] ] .challenge[ **"Rootless" is the future of cloud-native.** eBPF's privilege model is stuck in the past. ] ??? **Walk through the cards:** "Complex workarounds with setuid binaries. Over-privileged sidecars in Kubernetes. Developers who can't run bpftrace without sudo." "Even with CAP_BPF, you often need CAP_PERFMON AND CAP_NET_ADMIN. It's a capability soup that's hard to manage." **Key point:** "'Rootless' is the future of cloud-native. eBPF's privilege model is stuck in the past." --- class: section-slide, center, middle # What Are BPF Tokens? ## Delegation Without Root .section-number[2] ??? **Pause briefly.** --- # BPF Tokens: The Core Idea .quote-box[ A **BPF Token** is a kernel object that represents a **delegation of privileges**, tied to a specific `bpffs` instance. ] ### Key Properties: - **Mechanism:** Token is a file descriptor, not a string or capability - **Granularity:** Enable *specific* program types, not "all eBPF" - **Security Model:** Leverages existing VFS permissions .flow-diagram.centered[ .flow-box[Admin mounts bpffs] .flow-arrow[→] .flow-box.token[Token created] .flow-arrow[→] .flow-box[User opens token] .flow-arrow[→] .flow-box.highlight[Limited BPF access] ] ??? **Key definition:** "BPF Token is a mechanism for delegating BPF subsystem functionalities from a privileged process in init-namespace to an unprivileged process in a user-namespace." "The kernel won't let unprivileged users load eBPF. But if a privileged process explicitly delegates certain privileges? That's acceptable. This is what tokens enable." "It's a file descriptor — bound to the user namespace that created it. Can't be used outside that namespace." --- # The Token Flow
Admin
mounts
bpffs
with delegation options
Unprivileged process
opens a token from that mount
Token FD
is passed to the kernel during
sys_bpf()
Kernel
checks: "Does this token allow this operation?"
.solution[ The token isn't a "get out of jail free" card — it's a **restricted pass** to specific BPF operations. ] ??? **The full procedure (simplified for slides):** "Reality is more complex. The unprivileged process creates a user namespace, creates a mount namespace, gets a bpffs FD via fsopen(), sends it to the privileged process over a Unix socket." "The privileged process configures delegation options with fsconfig(), creates the mount with fsmount(), sends it back." "Then the unprivileged process creates a BPF Token FD with BPF_TOKEN_CREATE and uses it in bpf() calls." **Key insight:** "Good news: container runtimes handle steps 1-5. Loader libraries like libbpf handle step 6 transparently. Your app just uses the token." --- class: section-slide, center, middle # The Anatomy of Delegation ## Configuring What's Allowed .section-number[3] ??? **Pause briefly.** --- # Mount-Time Configuration ### Tokens are configured during the `mount` command: ```bash mount -t bpf none /sys/fs/bpf/token_area \ -o delegate_cmds=btf_load:map_create:prog_load \ -o delegate_maps=array:hash \ -o delegate_progs=kprobe:tracepoint ``` | Option | Controls | |--------|----------| | `delegate_cmds` | Allowed BPF syscall commands | | `delegate_maps` | Restricted list of map types | | `delegate_progs` | Restricted list of program types | | `delegate_attachs` | Allowed attachment types | ??? **Walk through the options:** "Four delegation options available via fsconfig():" "delegate_cmds — which bpf() syscall commands are allowed (prog_load, map_create, btf_load...)" "delegate_maps — which map types can be created (array, hash, ringbuf...)" "delegate_progs — which program types can be loaded (kprobe, tracepoint, xdp, tc...)" "delegate_attachs — which attach types are permitted" "This is where distro maintainers come in. We define sensible defaults for common use cases." --- # Example: Tracing-Only Token ### A token for developers who need tracing but nothing else: ```bash # Admin creates restricted mount mount -t bpf none /sys/fs/bpf/dev-tracing \ -o delegate_cmds=btf_load:map_create:prog_load \ -o delegate_progs=kprobe:tracepoint:perf_event \ -o delegate_maps=array:hash:ringbuf # Set permissions for developers group chmod 750 /sys/fs/bpf/dev-tracing chgrp developers /sys/fs/bpf/dev-tracing ``` .token-box[ **Result:** Developers can run `kprobe` programs but **cannot** load `tc` or `xdp` programs that could mess with networking. ] ??? "Here's a practical example. Admin creates a mount that only allows kprobe and tracepoint programs." "Set permissions for the developers group. Now they can trace, but they can't mess with networking." --- class: section-slide, center, middle # Demo: eBPF in Rust with Aya ## Tokens Meet Modern Tooling .section-number[4] ??? **Pause briefly.** --- # Why Aya? .left-55[ ### Pure-Rust eBPF Development - No dependency on `libbpf` or `libc` - Memory-safe userspace code - First-class token support via `EbpfLoader` - Growing ecosystem ```rust use aya::{EbpfLoader, BpfToken}; use std::fs::File; // Open the token created by Admin let token_file = File::open( "/sys/fs/bpf/token_area/token" )?; let token = BpfToken::from_file(token_file)?; ``` ] .right-45[ .rust-box[ ### Aya Highlights - Pure Rust, no C dependencies - Async-first design - Excellent error messages - Native token integration ]
.info-box[ **`.set_token(token)`** is the key — it handles passing the FD into the BPF attribute union before the syscall. ] ] .clearfix[] ??? "Aya is pure-Rust eBPF development. No dependency on libbpf or libc. Memory-safe userspace code." "And it has first-class token support via EbpfLoader." --- # Loading a Program with Tokens ```rust use aya::{EbpfLoader, BpfToken}; use std::fs::File; fn main() -> Result<(), Box
> { let token_path = "/sys/fs/bpf/token_area/token"; let mut loader = EbpfLoader::new(); // Check if token exists, if so, apply it if std::path::Path::new(token_path).exists() { let token_file = File::open(token_path)?; let token = BpfToken::from_file(token_file)?; loader.set_token(token); println!("Using BPF Token for delegation..."); } // Load the eBPF program - NO SUDO REQUIRED! let mut bpf = loader.load_file("my_kprobe.o")?; println!("Program loaded successfully!"); Ok(()) } ``` ??? **Walk through the code:** "Open the token file. Create a BpfToken from it. Call set_token on the loader. Load your program." "The key is `.set_token(token)` — it handles passing the file descriptor into the BPF attribute union before the syscall." "No sudo required!" --- class: section-slide, center, middle # Live Walkthrough ## Setting the Guardrails .section-number[5] ??? **Pause briefly.** --- # Step 1: Failure Without Token .demo-step.failure[ .label[❌ FAILURE] ```bash $ ./my-aya-tracer Error: EPERM (Operation not permitted) ``` ] **Why?** User lacks `CAP_BPF` and no token is available. ??? **Show the Rust demo output (without token mount):** "First, run the demo without any token mount set up. We get EPERM — operation not permitted. This is expected — unprivileged users can't do BPF operations." "Notice the kernel check: 6.18 supports tokens. But no delegated mount exists yet." --- # Step 2: Admin Creates Token .demo-step.success[ .label[🔧 ADMIN SETUP] ```bash # Create delegated mount with restricted permissions $ sudo mount -t bpf none /sys/fs/bpf/token_area \ -o delegate_cmds=btf_load:map_create:prog_load \ -o delegate_progs=kprobe:tracepoint # Grant access to specific user $ sudo chown developer:developer /sys/fs/bpf/token_area ``` ] ??? **Run the setup script as root:** ``` sudo mount -t bpf bpf /sys/fs/bpf/token_demo \ -o delegate_cmds=map_create -o delegate_cmds=prog_load \ -o delegate_progs=kprobe -o delegate_progs=tracepoint \ -o delegate_maps=array -o delegate_maps=hash sudo chown $USER:$USER /sys/fs/bpf/token_demo ``` "Admin creates the mount with specific delegation options. Only kprobe, tracepoint programs. Only array, hash maps. Then sets ownership so our user can access it." --- # Step 3: Success With Token .demo-step.success[ .label[✅ SUCCESS] ```bash $ ./my-aya-tracer Using BPF Token for delegation... Program loaded successfully! Attaching kprobe to do_sys_open... firefox opened /home/user/.config/... ``` ] **Same binary, same user** — but now with token access! ??? **Run the Rust demo again — now it detects the delegated mount:** "Run the demo again. Now we see the 🔑 emoji next to our mount — it detected the delegation options!" "The demo shows: UID 1000, not root. Delegated mount found. Current user can access." **Important caveat:** "In our simple demo, token creation still fails because the FULL workflow requires user namespace coordination. Container runtimes handle that. But the demo proves the mount is set up correctly." --- # Step 4: Enforcement Still Works .demo-step.failure[ .label[🚫 ENFORCEMENT] ```bash # Try to load a TC program (not in delegate_progs) $ ./my-aya-tc-filter Error: EPERM - program type not allowed by token ``` ] .token-box[ **The "Aha!" moment:** The token isn't unlimited access. It's a *restricted pass* that only allows what was explicitly delegated. ] ??? **Now run the container demo — this is the real payoff:** ``` ./container-demo.sh ``` "The container runs WITHOUT --privileged. Look at the output — inside the container, we can see the bpffs mount with all its delegation options preserved." "This is the key: Podman mounted the delegated bpffs into the container. With proper libbpf/Aya token support, the container could now load kprobe programs — no root needed!" **Why enforcement still matters:** "If someone tried to load a TC or XDP program, they'd get EPERM. The token only allows what was delegated. Security and usability coexisting." --- class: section-slide, center, middle # Why Distributions Care ## Distros as Policy Coordinators .section-number[6] ??? **Pause briefly.** --- # The Distro Role ### Distributions are the "Policy Coordinators" .card-grid.three-col[ .card[ ### 🐳 Container Runtimes Podman can mount a delegated `bpffs` into rootless containers ] .card[ ### ⚙️ System Services systemd can provide tokens to specific units ] .card[ ### 🔒 Security Guardrails Combine tokens with BPF LSM to sign/verify bytecode ] ] .info-box[ **Vision:** A `/sys/fs/bpf/user` mount available by default for developers, managed by a daemon like `logind`. ] ??? "Distributions are the 'Policy Coordinators.' We sit between the privileged container runtime and the unprivileged application." "The privileged process — like Podman or containerd — handles the complex fsopen/fsconfig/fsmount dance. It creates the delegated bpffs and passes it into the container." "The unprivileged app, using libbpf or Aya, auto-detects the token and uses it transparently. The app developer doesn't need to know the details." "We can also combine tokens with BPF LSM to sign and verify bytecode — defense in depth." --- # Fedora Integration Ideas .left-column[ ### Systemd Units ```ini [Service] # Token provider for tracing ExecStart=/usr/bin/mount -t bpf ... ExecStart=/usr/bin/chmod 750 ... ``` ### Group-Based Access ```bash # Add to 'bpf' group for tracing $ sudo usermod -aG bpf developer ``` ] .right-column[ ### Podman Integration ```bash # Mount token into container podman run \ -v /run/bpf/tracing:/sys/fs/bpf \ my-observability-tool ``` ### SELinux Policies ```te allow bpf_t bpf_token_t:bpf { prog_load map_create }; ``` ] .clearfix[] ??? "Systemd units for token providers. Group-based access — add to 'bpf' group to get tracing. Podman integration for containers. SELinux policies." "Vision: a /sys/fs/bpf/user mount available by default, managed by logind." --- # The Fedora eBPF SIG .left-column[ ### What We're Working On - Token-aware packaging guidelines - Default token provider services - SELinux policy updates - Container runtime integration - Documentation for admins ### Get Involved [fedoraproject.org/wiki/SIGs/eBPF](https://fedoraproject.org/wiki/SIGs/eBPF) ] .right-column[ .token-box[ ### The Shift **From:** "Trust the user with root" **To:** "Trust the policy via tokens" ]
.solution[ BPF Tokens are how we make eBPF a **first-class citizen** in distributions — secure, usable, and standard. ] ] .clearfix[] ??? **Emphasize the shift:** "We're moving from 'Trust the user with root' to 'Trust the policy via tokens.'" "This is how we make eBPF a first-class citizen in distributions." --- # Conclusion & Next Steps ### BPF Tokens change the game: - ✅ Remove the "Root Requirement" for many eBPF tasks - ✅ Enable fine-grained, per-namespace delegation - ✅ Work with modern tooling (Aya, libbpf) - ✅ Fit the "rootless" cloud-native future ### Your Next Steps: 1. **Experiment:** `mount -t bpf` on Kernel 6.7+ 2. **Explore:** Check out the Fedora eBPF SIG 3. **Adopt:** Port your tools to use libbpf or Aya token APIs ??? **Summarize:** "BPF Tokens remove the root requirement. They enable fine-grained delegation. They work with modern tooling. They fit the rootless cloud-native future." **Call to action:** "Experiment with mount -t bpf on Kernel 6.7+. Check out the Fedora eBPF SIG. Port your tools to use token APIs." --- # Resources .left-column[ ### Kernel & Libraries - **BPF Token Docs** [docs.kernel.org/bpf/token.html](https://docs.kernel.org/bpf/token.html) - **Token Patch Series (LWN)** [lwn.net/Articles/942529/](https://lwn.net/Articles/942529/) - **libbpf** [github.com/libbpf/libbpf](https://github.com/libbpf/libbpf) - **Aya (Rust eBPF)** [github.com/aya-rs/aya](https://github.com/aya-rs/aya) ] .right-column[ ### Fedora & Security - **Fedora eBPF SIG** [fedoraproject.org/wiki/SIGs/eBPF](https://fedoraproject.org/wiki/SIGs/eBPF) - **bpftool** [github.com/libbpf/bpftool](https://github.com/libbpf/bpftool) - **BPF LSM** [docs.kernel.org/bpf/bpf_lsm.html](https://docs.kernel.org/bpf/bpf_lsm.html) - **Podman** [github.com/containers/podman](https://github.com/containers/podman) ] .clearfix[] ??? "All these links will be in the slides." --- class: center, middle # Q&A .large[ **Daniel Mellado** ]
.contact[ 🐦 @dmellado 🐙 github.com/danielmellado ]
.small[ Slides: `github.com/danielmellado/fosdem26-bpf-tokens` ] ??? **Open for questions.** **Common questions:** - "Which kernel version?" → 6.7 or later. Fedora 40+ has it. - "Does this work with SELinux?" → Yes, but you need updated policies. We're working on standard policies for Fedora. - "Can I use tokens with bpftrace?" → libbpf has token support. bpftrace integration is in progress. - "libbpf vs Aya?" → Both support tokens. Aya is pure Rust, libbpf is the reference C implementation. libbpf handles token creation transparently. - "Can tokens be revoked?" → Yes — close the file descriptor, or umount the bpffs. - "What if someone copies the token file?" → The token FD is bound to the user namespace that created it. Can't be used outside that namespace. - "Do I need to implement the full fsopen/fsconfig dance?" → No! Container runtimes like Podman handle the privileged side. Loaders like libbpf auto-detect tokens. Your app just works. - "Why not just use capabilities in user namespaces?" → eBPF can see sensitive kernel behavior. The kernel explicitly doesn't trust CAP_BPF inside user namespaces. Tokens provide explicit delegation from a trusted init-namespace process. --- class: final-slide # Thank You! .tagline[ From "Trust the user with root" to "Trust the policy via tokens" ] .contact[
.quote-box[ BPF Tokens make eBPF a first-class citizen in distributions — secure, usable, and standard. ]
**FOSDEM 2026** · Distributions Devroom · Brussels ] ??? **Leave this slide up during Q&A.** **Key quotes to remember:** 1. "The kernel won't let unprivileged users load eBPF. But if a privileged process delegates? That's acceptable." 2. "The token FD is bound to the user namespace that created it — can't escape." 3. "Container runtimes handle the complex setup. Loaders like libbpf make tokens transparent. Apps just work." 4. "We're moving from 'Trust the user with root' to 'Trust the policy via tokens.'"