基于 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 编译器。openssl、fsverity 和 setfattr,以及 Makefile 中列出的静态归档文件(STATIC=1)。如果 pkg-config 找不到 libbpf,请设置 LIBBPF_CFLAGS / LIBBPF_LIBS。在每次构建时将 LIBARENA 指向 libarena 检出:
下面的每个 make 也都需要 LIBARENA(见要求),在命令行上设置或在环境中导出。
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 USER_ID -- CMD # run CMD in a new pod
sudo bpfjctl enroll ROLE USER_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
这些程序默认固定在 /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 成员关系、变量和跟踪的资源所有权,然后原子地交换固定树。在交接期间两棵树都保持附加状态,fork 和注册会与迁移协调,所有权变更会被记录日志并重放。如果持久化布局版本不兼容或状态无法安全复制,替换会以失败关闭方式失败。
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。
examples/signed-attach:一个签名的 bpfjcmd,它是主机上唯一被允许更新 BpfJailer 自身 BPF 程序的二进制文件。examples/unpriv-enroll:一个非 root 进程通过 bpfjsrv 将自己关入 jail。