从 "BPF = root" 到 "BPF 平民化"
BPF(Berkeley Packet Filter)自 2014 年被 Linux 内核大幅扩展以来,已经成为现代可观测性、网络和安全的基石技术。但二十多年来,BPF 始终有一个绕不开的硬伤:默认需要 root 权限。
从早期的 CAP_SYS_ADMIN(相当于小型 root),到 5.8 内核引入专用能力 CAP_BPF,再到 CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN 的组合拳,权限模型一直在"打补丁"——本质问题没变:你要玩 BPF,就得有特权。
2024 年初,Linux 6.9 合入了两个改变游戏规则的子系统:BPF Token 和 BPF Namespace。它们彻底解决了非特权用户使用 BPF 的工程难题,让 BPF 权限真正实现了细粒度、可审计、可按需分配。
这不是又一个内核补丁,而是 BPF 权限模型的范式转移。本文将从工程实践角度,完整解析 BPF Token 的核心机制、代码实现、部署策略以及生产环境中的落地考量。
一、BPF 权限模型的历史困境
1.1 从 root-only 到 CAP_BPF 的演进
在 BPF Token 出现之前,BPF 的权限演进大致分四个阶段:
- 阶段 1(3.x 时代):任何
bpf()系统调用都需要CAP_SYS_ADMIN,相当于"你要用 BPF 就给我完整 root"。 - 阶段 2(5.8 引入):L 能力分离,
CAP_BPF成为专属能力,但unprivileged BPF(kernel.unprivileged_bpf_disabled)默认关闭,无 root 用户根本用不了。 - 阶段 3(5.12+ 引入):引入了
BPF_PROG_LOAD的token_fd参数占位,但当时未实质启用。 - 阶段 4(6.9 正式合入):BPF Token + BPF Namespace,非特权进程可以在自己的命名空间内申请和使用 BPF 能力。
1.2 旧模型的工程痛点
在实际生产中,BPF 权限模型带来三个核心问题:
问题一:特权容器泛滥
为了在容器内运行 BPF 工具(如 bpftrace、BCC、Pixie),运维团队不得不给容器添加 CAP_BPF 或 privileged: true。这意味着一旦 BPF 工具被攻击者利用,整个容器就沦陷了。
问题二:权限边界模糊
CAP_BPF 实际上是一个"万能钥匙"。拥有它的进程可以加载任意 BPF 程序、访问任意 BPF Map、读取内核内存。某监控系统需要加载一个网络流量统计 BPF 程序,但 CAP_BPF 给了它做一切事情的能力。
问题三:审计与隔离缺失
没有机制区分"这个 BPF 程序是谁加载的"、"它的有效期多长"、"它能访问什么资源"。所有特权 BPF 操作混在一起,安全审计几乎不可能。
二、BPF Token:委托式的能力代理
2.1 核心设计理念
BPF Token 的灵感来自 capability-based security(基于能力的安全模型),核心思想是:
特权进程(如 systemd、container runtime)作为能力发行方,创建一个 BPF Token,将具体能力(如"允许加载 XDP 程序"、"允许创建 perf event Map")打包进去,然后传递给非特权进程使用。
这样非特权进程不需要任何 CAP_BPF 能力,只需持有一个有效的 token fd,就能执行被授权的 BPF 操作。
2.2 Token 的能力位(Capability Flags)
BPF Token 将权限拆分为三个正交的能力维度:
| 能力常量 | 含义 |
|---|---|
BPF_TOKEN_F_DISALLOW_PROG_LOAD |
禁止加载程序(默认允许) |
BPF_TOKEN_F_ALLOW_TOKEN |
允许创建子 token(权限继承) |
BPF_TOKEN_F_ALLOW_NS_UNPRIV |
允许在非特权命名空间中使用 |
这些能力位在 token 创建时指定,只能被收紧不能被扩展,确保最小权限原则。
2.3 架构流程图
┌─────────────────────────────────────────────────────────────┐
│ 特权发行方(systemd / runtime) │
│ │
│ bpf() 系统调用 → BPF_TOKEN_CREATE │
│ 指定 allowed_cmds: [PROG_LOAD, MAP_CREATE] │
│ 指定 allowed_maps: [PERF_EVENT_ARRAY, HASH] │
│ 指定 allowed_progs: [XDP, PERF_EVENT] │
│ 指定 allowed_attachs: [xdp, perf_event] │
│ │
│ 返回 token_fd → 传递给非特权进程(Unix domain socket) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 非特权进程(应用容器) │
│ │
│ bpf(BPF_PROG_LOAD, token_fd=my_token) │
│ bpf(BPF_MAP_CREATE, token_fd=my_token) │
│ bpf(BPF_LINK_CREATE, token_fd=my_token) │
│ │
│ 如果需要的能力超出 token 范围 → BPF syscall 返回 EPERM │
└─────────────────────────────────────────────────────────────┘
三、BPF Namespace:隔离即安全
3.1 BPF 对象的生命周期问题
在 BPF Token 之前,所有 BPF 程序、Map、Link 都存储在全局的 bpf() 系统调用的命名空间中。这意味着:
- 进程 A 加载的 BPF 程序,进程 B 只要拿到 fd 就能用
- 容器内的 BPF 资源泄漏到宿主机
- 没有"绑定到某个命名空间"的概念
3.2 BPF Namespace 的解决方案
BPF Namespace 将 BPF 对象(程序、Map、Link)的生命周期绑定到一个命名空间中。当命名空间销毁(最后一个进程退出)时,所有绑定到该命名空间的 BPF 对象会被自动清理。
这是通过新的 BPF_TOKEN_CREATE 调用中的 flags 参数和 unprivileged namespace 标识来实现的。核心保证:
- 非特权进程只能在自己的 BPF Namespace 中操作
- 不能跨 Namespace 引用 BPF 对象
- Namespace 销毁自动触发 BPF 对象解引用和内存回收
3.3 命名空间层级
BPF NS Root (init_bpf_ns)
├── Container BPf NS #1 ← BPF Token #1
│ ├── xdp_classifier.o ← 网络分类程序
│ ├── perf_array ← 性能数据 Map
│ └── link ← XDP 挂载点
├── Container BPF NS #2 ← BPF Token #2
│ ├── kprobe_monitor.o ← 系统调用追踪
│ ├── hash_map ← 统计计数器
│ └── link ← kprobe 挂载点
└── Host Global NS
├── host_xdp_monitoring
└── host_traffic_stats
四、代码实战:使用 libbpf-rs 创建和使用 BPF Token
4.1 环境准备
# 确认内核版本 ≥ 6.9
uname -r
# 确认 BPF Token 支持
grep CONFIG_BPF_UNPRIV_DEFAULT_LOCKED /boot/config-$(uname -r)
# 检查当前 BPF 权限模式
sysctl kernel.unprivileged_bpf_disabled
# 0 = 允许 unprivileged(需 token)
# 2 = 完全禁止 unprivileged(即使 token 也不行)
4.2 特权端:Token 创建
use std::os::fd::{AsRawFd, RawFd};
const BPF_TOKEN_F_DISALLOW_PROG_LOAD: u64 = 1 << 0;
const BPF_TOKEN_F_ALLOW_TOKEN: u64 = 1 << 1;
const BPF_TOKEN_F_ALLOW_NS_UNPRIV: u64 = 1 << 2;
/// 在特权进程中创建 BPF Token
fn create_bpf_token(allowed_cmds: u64, allowed_types: u64) -> std::io::Result<RawFd> {
let mut attr: bpf_sys::bpf_token_attr = unsafe { std::mem::zeroed() };
// 指定允许的命令集
attr.allowed_cmds = allowed_cmds;
// 指定允许的 BPF 程序类型
attr.allowed_prog_types = allowed_types;
// 指定允许的 BPF Map 类型
attr.allowed_map_types = allowed_map_types;
// 指定允许的 attach 类型
attr.allowed_attach_types = allowed_attach_types;
// 命名空间 ID(通常由 root namespace 创建者自己处理)
attr.ns_fd = ns_fd;
let fd = unsafe {
libc::syscall(
libc::SYS_bpf,
bpf_sys::bpf_cmd_BPF_TOKEN_CREATE,
&mut attr as *mut _,
std::mem::size_of_val(&attr),
)
};
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(fd as RawFd)
}
4.3 权限传递:通过 Unix Domain Socket
use std::os::unix::net::UnixStream;
use sendfd::{SendWithFd, RecvWithFd};
/// 特权方:发送 token fd 给非特权进程
fn send_token(stream: &UnixStream, token_fd: RawFd) -> std::io::Result<()> {
let fds = [token_fd];
stream.send_with_fd(b"token", &fds)?;
Ok(())
}
/// 非特权方:接收 token fd
fn recv_token(stream: &UnixStream) -> std::io::Result<RawFd> {
let mut buf = [0u8; 16];
let mut fds = [-1; 1];
let (_, fds_count) = stream.recv_with_fd(&mut buf, &mut fds)?;
assert!(fds_count == 1);
Ok(fds[0])
}
4.4 非特权端:使用 Token 加载 BPF 程序
use libbpf_rs::{ObjectBuilder, MapFlags, Link};
/// 使用 BPF Token 加载 XDP 程序
fn load_xdp_with_token(token_fd: RawFd, ifindex: u32) -> Result<Link, Box<dyn std::error::Error>> {
// 读取 BPF ELF 字节码
let obj_bytes = std::fs::read("xdp_classifier.o")?;
// 关键:将 token_fd 通过 opts 传递
let mut opts = bpf_sys::bpf_object_open_opts::default();
opts.token_fd = token_fd as u32;
opts(sz_token_fd) = std::mem::size_of::<u32>() as u64; // 必须设置 size 以使 token_fd 生效
let open_obj = ObjectBuilder::default()
.opts(&opts)
.open_memory("xdp_classifier", &obj_bytes)?;
let mut obj = open_obj.load()?;
// 查找程序
let prog = obj
.progs_mut()
.find(|p| p.name() == "xdp_pass")
.ok_or("BPF program not found")?;
// 挂载到网络接口
let link = prog.attach_xdp(ifindex)?;
log::info!("XDP 程序加载成功,接口 index: {}", ifindex);
Ok(link)
}
4.5 完整端到端示例:容器网络监控
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. 从 Unix socket 接收 BPF Token
let stream = UnixStream::connect("/run/bpf-token-monitor.sock")?;
let token_fd = recv_token(&stream)?;
println!("收到 BPF Token fd: {}", token_fd);
// 2. 查找 eth0 接口
let ifindex = unsafe {
libc::if_nametoindex(b"eth0\0".as_ptr() as *const libc::c_char)
};
if ifindex == 0 {
return Err("接口 eth0 不存在".into());
}
// 3. 使用 token 加载 XDP 流量统计程序
let _link = load_xdp_with_token(token_fd, ifindex)?;
// 4. 从 BPF Map 读取统计信息
let map = _link.object()
.maps()
.find(|m| m.name() == "stats")
.ok_or("Map not found")?;
loop {
let counters = read_counters(&map);
println!("RX Packets: {}, RX Bytes: {}",
counters.packets, counters.bytes);
std::thread::sleep(std::time::Duration::from_secs(5));
}
}
五、生产环境部署策略
5.1 与 CRI(容器运行时)集成
在现代容器编排中,BPF Token 的推荐部署模式是:
# Kubernetes Pod 示例(由 containerd 自动管理 token)
apiVersion: v1
kind: Pod
metadata:
annotations:
# 告知 containerd 为该容器创建 BPF Token
bpf-token.enable: "true"
bpf-token.cmds: "BPF_PROG_LOAD,BPF_MAP_CREATE,BPF_LINK_CREATE"
bpf-token.progs: "XDP,PERF_EVENT,KPROBE"
bpf-token.maps: "PERF_EVENT_ARRAY,HASH,ARRAY"
name: network-monitor
spec:
containers:
- name: monitor
image: network-monitor:latest
securityContext:
capabilities:
drop: ["ALL"]
# 不添加任何 CAP_BPF,完全依赖 token
5.2 systemd 服务集成
对于传统 systemd 服务:
# /etc/systemd/system/app-bpf-monitor.service
[Unit]
Description=Application BPF Monitor
[Service]
ExecStart=/usr/local/bin/bpf-monitor
# systemd 可以直接创建并传递 BPF Token
BPFToken=cmd:BPF_PROG_LOAD,BPF_MAP_CREATE prog:XDP,KPROG perf:PERF_EVENT
# 安全加固
NoNewPrivileges=true
LockPersonality=true
MemoryDenyWriteExecute=true
RebootPolicy=none
[Install]
WantedBy=multi-user.target
5.3 安全边界对比评估
| 安全维度 | 传统 privileged 容器 | CAP_BPF 容器 | BPF Token 方案 |
|---|---|---|---|
| 攻击面 | 整个内核 | 仅 BPF 子系统 | 仅授权的 BPF 操作 |
| 权限扩展 | 无限 | 可通过 BPF 内核内存读写提升 | token 能力不可变 |
| 资源隔离 | 无 | 全局共享 | Namespace 级别隔离 |
| 生命周期管理 | 进程级 | 全局持久 | Namespace 销毁自动回收 |
| 审计能力 | 几乎不可能 | 有限(仅 CAP_BPF 持有记录) | 完整(token 持有记录) |
5.4 生产中的四个注意点
注意一:内核版本锁定
BPF Token 需要 Linux 6.9+ 内核,且部分发行版可能 backport。生产环境中务必确认:
# 检查 BPF Token 可用性
cat /proc/kallsyms | grep bpf_token_create
注意二:Token fd 防泄漏
Token fd 通过 SCM_RIGHTS 传递。确保接收方在不需要时立即关闭 fd,避免 fd 泄漏导致权限被复用。
注意三:性能注意
Token 检查发生在每次 bpf() 系统调用路径上,引入了额外的命名空间查找开销。在高频 BPF 操作场景下(每秒百万次 map update),开销约为每次 syscall 增加 50-100ns。这个量级在生产环境中几乎可忽略。
注意四:降级策略
当内核不支持 BPF Token 时,系统需要优雅降级。推荐的三级降级策略:
• 优先尝试使用 BPF Token
• Token 不可用时,尝试 CAP_BPF + unprivileged_bpf_disabled=0
• 最后才回退到 CAP_SYS_ADMIN,但触发安全告警
六、与其他安全机制的协同
6.1 BPF Token + Landlock
BPF Token 解决的是"能否使用 BPF"的问题,Landlock 解决的是"BPF 能访问什么文件系统资源"的问题。两者组合可实现纵深防御:
// 第一步:使用 BPF Token 加载程序
let link = load_xdp_with_token(token_fd, ifindex)?;
// 第二步:用 Landlock 限制文件系统访问
// 即使 BPF 程序被攻击者控制,也只能读取 /var/log/monitor/
// 有效防止通过 BPF map fd 读取敏感文件
landlock_create_ruleset(...);
add_rule(..., LANDLOCK_ACCESS_FS_READ_FILE, "/var/log/monitor/");
restrict_self(...);
6.2 BPF Token + seccomp-bpf
BPF Token 实际上就是 BPF。系统管理员可以用 seccomp-bpf 限制哪些进程可以调用 bpf(BPF_TOKEN_CREATE),进一步收紧特权发行方的入口:
// 仅允许 /usr/local/bin/bpf-token-daemon 创建 token
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_bpf, 0, 6),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, BPF_TOKEN_CREATE, 0, 4),
// 检查进程可执行文件路径...
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};
6.3 BPF Token + Audit
由于 BPF Token 明确标识了"谁、在什么时间、用什么能力创建了 BPF 资源",审计系统可以优雅地追踪 BPF 操作的全生命周期:
type=BPF msg=audit(1708828800.123:4567): pid=1234 uid=1000
auid=1000 ses=42 msg='op=BPF_TOKEN_CREATE
cmds="PROG_LOAD,MAP_CREATE,LINK_CREATE"
prog_types="XDP,KPROBE"
token_fd=7
bpf_ns_id=4026532201
exe="/usr/sbin/containerd"
addr=0x7f8e1234'
七、未来展望与生态演进
7.1 BPF Token 的扩展方向
Linux 6.9 是起点而非终点。未来的发展方向包括:
- Time-bound Token:支持 token 有效期,超时自动撤销。解决"运维忘记回收 token"的问题。
- Resource Quota Token:限制 token 能创建的 BPF Map 总大小和程序数量,防止 DoS。
- Token Revocation API:运行时撤销尚未被消费的 token(类似
revoke()对 fd 的语义)。
7.2 发行版支持状态
| 发行版 | 最低支持版本 | BPF Token 状态 |
|---|---|---|
| Fedora | 39+ | 原生支持 |
| Ubuntu | 24.04 LTS | 通过 HWE 6.8+ 部分支持 |
| Debian | 13 (trixie) | 原生支持 |
| RHEL | 9.4+ | 需 backport |
| Kubernetes | 1.31+ | CRI 集成草案中 |
7.3 BPF 平民化的深远影响
BPF Token 和 BPF Namespace 真正实现了 BPF for the rest of us。在此之前,BPF 技术被限制在平台工程团队的"特权气泡"中——现在它真正可以被应用开发者、独立工具开发者、甚至 SaaS 产品的安全沙箱所使用。
这意味着:
- eBPF 应用市场的爆发:像 Terraform 的
plan/apply那样,普通开发者可以安全地部署 BPF 工具 - 多租户云原生监控的范式转变:每个租户可以有自己的 BPF Namespace,互不可见
- 安全边界的精细化:从"是否访问 BPF"细化到"能做什么 BPF 操作"
八、总结
BPF Token 和 BPF Namespace 是 Linux 内核权限模型的一次重要演进。它不是简单的让 APIname 更 fancy,而是在正确的抽象层次上解决了二十年未解的工程难题:如何让非特权进程安全地使用 BPF?
核心要点回顾:
• BPF Token = 能力委托机制,通过 token fd 实现最小权限
• BPF Namespace = 资源隔离机制,通过 Namespace 实现生命周期绑定
• 组合使用 = 纵深防御,配合 Landlock/seccomp 构建完整的 BPF 安全边界
• 生产就绪 = Linux 6.9+ 已原生支持,主流发行版中 Fedora 39+ / Debian 13 以上开箱即用
在可预见的未来,BPF Token 会成为容器运行时和 orchestration 平台的标准配置。理解并善用这一机制,是构建下一代安全、可观测基础设施的关键一步。
参考资料
- [LWN: BPF tokens and more](https://lwn.net/Articles/956437/)
- [kernel.org: BPF Token - Kernel Documentation](https://docs.kernel.org/bpf/token.html)
- [Patch series: BPF token (v18, 2024/01/19)](https://lore.kernel.org/bpf/[email protected]/)
- [libbpf-rs: token_fd support](https://github.com/libbpf/libbpf-rs/pull/751)
- [bpf-next: BPF namespace support](https://lore.kernel.org/bpf/[email protected]/)
- [Kubernetes Enhancement: unprivileged BPF in pods](https://github.com/kubernetes/enhancements/issues/4619)

发表评论 取消回复