基于 eBPF 的 Linux 强制访问控制。
本项目是对闭源 BpfJailer 的完全重写,且完全处于实验阶段。它利用了 bpf arena 等较新的特性,这些特性在编写内部 BpfJailer 时尚不可用。预计会出现问题,这些问题不符合漏洞赏金资格,也不被视为安全发现。经过适当评估后,它将取代内部闭源版本。
BpfJailer 使用 eBPF LSM 程序将进程放入称为 pod 的 jail 中,每个 pod 绑定到 TOML 策略中的一个角色。pod 会跨 fork 和 exec 继承。可选的策略特性:
kill 和 ptrace — 可以对其发送信号或附加的目标角色/pod。bpf — 某个角色可以打开哪些角色的 eBPF map 和程序,或者它是否可以调用 bpf(2)。keyring — 某个角色可以向哪些角色的 fs-verity keyring 添加证书,或者它是否可以写入 keyring。拒绝和生命周期事件被写入固定的环形缓冲区。bpfjlog 打印人类可读的 BPF 诊断信息和结构化事件,并在实时策略替换期间跟随环形缓冲区。
二进制文件可以通过 user.bpfj.policy.exec xattr 声明一个角色,并在 exec 时被注册到该角色中。运行中的进程也可以被直接注册,非特权进程可以通过 bpfjsrv/bpfjclient 自行注册。
| 目录 | 二进制文件 | 用途 |
|---|---|---|
bpfj/ | 核心库和 BPF 程序:jailer、enforcer、策略解析器、libbpf C++ 辅助工具。 | |
ctl/ | bpfjctl | 用于附加、重新加载、检查和分离 jailer 以及注册进程的通用工具。 |
cmd/ | bpfjcmd | 将参数以及可选的策略编译进其中的 bpfjctl。它忽略 argv,因此可以作为单个单元进行静态链接和 fs-verity 签名。 |
srv/ | bpfjsrv | 套接字激活的服务器,将非特权调用者注册到允许其注册的角色中。 |
client/ | bpfjclient | bpfjsrv 的最小客户端,不依赖 libbpf 或 BPF 工具链。 |
log/ | bpfjlog | 固定诊断和结构化事件环形缓冲区的消费者。 |
tests/ | bpfjtest | 测试套件。 |
CONFIG_BPF_LSM=y 且 lsm= 启动参数中包含 bpf)。BpfJailer 仅在 6.16+ 上测试过,不支持较旧的内核。bpftool 以及 C++20 编译器。clang 20 和 21 在从 arena 内存进行 __builtin_memcpy 时不会生成 addr_space_cast,因此验证器会拒绝它们构建的 jailer。CONFIG_BTRFS_FS=y 而非模块。BPF 程序从 vmlinux.h 读取 btrfs inode 类型,Makefile 从运行中内核的 BTF 生成该文件。protoc,Debian 和 Ubuntu 将其作为 libprotobuf-dev 和 protobuf-compiler 提供。openssl、fsverity 和 setfattr,以及 Makefile 中列出的静态归档文件(STATIC=1)。如果 pkg-config 找不到 libbpf,请设置 LIBBPF_CFLAGS / LIBBPF_LIBS。在每次构建时将 LIBARENA 指向 libarena 检出副本:
下面每个 make 命令也都需要 LIBARENA(参见“Requirements”),可在命令行中设置,或在环境中导出。
make # build/bpfjctl
make STATIC=1 # bpfjctl with no shared object dependencies
make client # build/bpfjclient, no BPF toolchain needed
make log # build/bpfjlog
make signing-key # generate a development signing key and certificate
make signed SIGNING_KEY=... SIGNING_CERT=... # static, fs-verity signed bpfjctl
make srv SIGNING_KEY=... SIGNING_CERT=... # static, signed bpfjsrv
make cmd SIGNING_KEY=... SIGNING_CERT=... \
CMD_ARGS="replace-compiled" CMD_POLICY=policy.toml CMD_ROLE=bpfjailer
make clean
所有输出都位于 build/ 下。设置 BUILD= 可将构建输出到其他位置,例如 make BUILD=build-asan SANITIZE=address,undefined。
make test
测试必须以 root 身份运行,因为每个测试都会创建一个挂载命名空间并挂载一个 bpffs。make test 以调用用户身份构建,并且仅在 sudo 下运行测试二进制文件。
测试默认串行运行,因为并发分离 BPF LSM 可能导致受影响的内核发生 panic。使用 make test TEST_ARGS=Suite.Test 运行聚焦的测试用例,并且仅在一次性 VM 中才选择启用 -j N 或 BPFJTEST_JOBS=N。
sudo bpfjctl check policy.toml # parse a policy and report what it holds
sudo bpfjctl attach policy.toml # load and pin the jailer
sudo bpfjctl replace policy.toml # reload without releasing jailed tasks
sudo bpfjctl wrap ROLE POD_ID -- CMD # run CMD in a new pod
sudo bpfjctl enroll ROLE POD_ID PID [NAME=VALUE...] # enroll with variables
sudo bpfjctl show PID # pods a process is in
sudo bpfjctl list # every pod and its processes
sudo bpfjctl detach # unpin and unload
程序默认被固定(pin)在 /sys/fs/bpf/bpfj-pins 下。使用
--bpffs-path 和 --pin-dir 可以更改此位置。它们会保持加载状态,直到运行 detach。
不带 --drop-cap 且使用非 root 的 --uid 运行 bpfjctl wrap 时,该命令仍能将自己从 jail 中移除。参见 bpfjctl wrap --help。
在 jailer 附加期间运行 sudo build/bpfjlog 以观察它。BPF 诊断信息写入 stderr,结构化事件写入 stdout。当 replace 换入一组新的固定 map 时,日志记录器会自动重新连接。
replace 会在活动 jailer 旁边加载一个完整的第二个 jailer,迁移 pod 成员关系、变量和受跟踪的资源所有权,然后原子地交换 pin 树。在交接期间两棵树都保持附加状态,fork 和注册与迁移协调进行,所有权变更会被记录到日志并重放。如果持久化的布局版本不兼容或状态无法安全复制,替换会以失败关闭(fail closed)方式终止。
base-role = "floor" # optional: enroll every process on the host
vars = ["vm_uuid"] # known variable names
[certs]
corp-ca = "MIIDXTCCAkWgAwIBAgIJAK..." # PEM or base64 DER certificate
[roles.floor]
any = true # open tracking-only base role
[roles.webserver]
enforce-binary-certs = ["corp-ca"] # execs must be signed by one of these
kill-roles = ["floor"] # may signal its own pod, plus these roles
ptrace-pod = true # its own pod only
proc-roles = ["floor"] # may open proc files for these roles
bpf-pod = true # only BPF objects from its own pod
lkm-any = false # deny module and kexec loading
mq-sysv-pod = true # only SysV queues from its own pod
mq-posix-pod = true # only POSIX queues from its own pod
shm-sysv-pod = true # only SysV SHM from its own pod
shm-posix-pod = true # only POSIX SHM from its own pod
keyring-own = true # only its own role's keyring
[[roles.webserver.mq-posix-pattern]]
name = "/service-${vm_uuid}-*"
allow = true
[[roles.webserver.shm-posix-pattern]]
name = "/service-${vm_uuid}-*"
allow = true
[[roles.webserver.exec-paths]]
path = "/usr/bin/webserver"
allow = true
permissions = ["exec"]
[[roles.webserver.exec-paths]]
path = "/usr/lib"
allow = true
permissions = ["shared-object"]
[[roles.webserver.paths]] # cached path policy
path = "/"
allow = false
[[roles.webserver.paths]]
path = "/usr"
allow = true
access = "read-only"
[[roles.webserver.paths]]
path = "/etc"
allow = true
access = "read-only"
[[roles.webserver.paths]]
path = "/srv/web"
allow = true
access = "read-write"
[[roles.webserver.unix-bind]] # pathname bind rule
path = "/run/webserver"
allow = true
[[roles.webserver.unix-connect]]
name = "@control-${vm_uuid}"
allow = true
[[roles.webserver.unix-dgram]]
path = "/dev/log"
allow = true
[[roles.webserver.mount]] # destination and permitted filesystem types
path = "/srv/data"
allow = true
filesystems = ["ext4", "xfs"]
[[roles.webserver.mount]]
path = "/run/webserver"
allow = true
filesystems = ["any"] # every filesystem type at this destination
[[roles.webserver.umount]]
path = "/"
allow = false
[[roles.webserver.umount]]
path = "/srv/data"
allow = true
[roles.sandbox]
unpriv-enroll = true # every unspecified operation remains denied
override-stacked = true # answers alone, ignoring roles stacked below
当某个角色没有对应选项时,大多数操作门都会被拒绝。*-pod 选项允许来自同一 pod 的资源,*-roles 会添加指定的所有者角色,而 *-any 则完全开放该操作。keyring-own 是角色作用域内的对应选项,因为 fs-verity keyring 属于角色而非 pod。enroll-roles 指定了 bpfjsrv 唯一可以添加的角色;没有它,通过 bpfjsrv 进行的注册会被拒绝。Unix 路径名、挂载和卸载操作在其选项缺失或没有路径匹配时会被拒绝。抽象 Unix 套接字名称仍然是需显式启用的过滤器,因此未配置或未匹配的抽象名称会被允许。
完全开放的 proc 选项是 proc-any,遵循与其他所有权族相同的 *-any 顺序。
any = true 会开放所有没有更具体选项的操作。这对于仅用于归因的 pod 很有用。诸如 bpf-pod、kill-roles、paths 或 enforce-binary-certs 之类的作用域选项会针对该操作覆盖 any。lkm-any、fs-any、verity-any、mount-any 和 umount-any 是特定于操作的完全开放形式。
持有多个角色的进程只有在所有角色都同意时才被允许执行某个操作。角色按从新到旧的顺序被查询,而 override-stacked 角色会代表其下的角色作答。kill 和 ptrace 的目标侧会忽略 override:目标持有的每个角色都必须被列出。完整语义记录在 bpfj/policy/Policy.h 中。
vars 是一个允许列表。如果注册时设置的变量不在策略中列出,则会被拒绝;而如果完全没有 vars,则任何 pod 都不会携带任何变量。replace 会按名称将每个 pod 的变量一并迁移,如果新策略不再列出某个 pod 正在携带的变量,则会失败。
各种策略选项接受路径和 glob。这些选项都遵循一个通用匹配器。变量用 ${NAME} 表示,并支持通配符 ? 和 *。这使得可以匹配作用于特定 pod 内变量的选项,例如让两个容器拥有彼此不同的文件系统权限和隔离规则。目前所有文件系统操作都在 systemd 的挂载命名空间中执行,以防止挂载操作影响匹配器。在该命名空间中无法解析的文件会被允许。选择匹配命名空间的功能即将推出。
有关完整的选项矩阵、匹配语义和替换行为,请参阅 POLICY.md。