
eBPF LSM based Mandatory Access Control and jailer
eBPF based Mandatory Access Control for Linux.
This project is a full rewrite of the closed source BpfJailer and is completely experimental. It leverages newer features like bpf arena that were not available when the internal BpfJailer was written. Issues are expected and are not eligible for bug bounty or considered security findings. Once properly evaluated it will replace the internal closed source version.
BpfJailer uses eBPF LSM programs to put processes into jails, called pods, each
bound to a role from a TOML policy. A pod is inherited across fork and
exec. Optional policy features:
kill and ptrace — target roles/pods this may signal or attach to.bpf — which roles' eBPF maps and programs a role may open, or whether it
may call bpf(2) at all.keyring — which roles' fs-verity keyrings a role may add certificates
to, or whether it may write keyrings at all.Denials and lifecycle events are written to pinned ring buffers. bpfjlog
prints the human-readable BPF diagnostics and structured events and follows
the ring buffers across a live policy replacement.
A binary can claim a role through the user.bpfj.policy.exec xattr and is
enrolled in it at exec time. Running processes can also be enrolled directly,
and an unprivileged process can enroll itself through bpfjsrv/bpfjclient.
| Directory | Binary | Purpose |
|---|---|---|
bpfj/ | The core library and BPF programs: jailer, enforcers, policy parser, libbpf C++ helpers. | |
ctl/ | bpfjctl | General purpose tool for attaching, reloading, inspecting and detaching the jailer, and for enrolling processes. |
cmd/ | bpfjcmd | bpfjctl with its arguments, and optionally its policy, compiled in. It ignores argv, so it can be statically linked and fs-verity signed as a single unit. |
srv/ | bpfjsrv | Socket activated server that enrolls unprivileged callers into roles that allow it. |
client/ | bpfjclient | Minimal client for bpfjsrv, with no libbpf or BPF-toolchain dependency. |
log/ | bpfjlog | Consumer for the pinned diagnostic and structured-event ring buffers. |
tests/ | bpfjtest | Test suite. |
CONFIG_BPF_LSM=y and bpf in
the lsm= boot parameter). BpfJailer is only tested on 6.16+, and older
kernels are not supported.bpftool, and a C++20 compiler.openssl, fsverity and setfattr, plus the static
archives listed in the Makefile (STATIC=1).Set LIBBPF_CFLAGS / LIBBPF_LIBS if pkg-config cannot find libbpf. Point
LIBARENA at the libarena checkout on every build:
Every make below also needs LIBARENA (see Requirements), set on the
command line or exported in the environment.
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
All output goes under build/. Set BUILD= to build somewhere else, for
example make BUILD=build-asan SANITIZE=address,undefined.
make test
The tests have to run as root, because each one creates a mount namespace and
mounts a bpffs. make test builds as the invoking user and runs only the test
binary under sudo.
Tests run serially by default because concurrent BPF LSM detach can panic
affected kernels. Use make test TEST_ARGS=Suite.Test for a focused case, and
only opt into -j N or BPFJTEST_JOBS=N inside a disposable VM.
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
The programs are pinned under /sys/fs/bpf/bpfj-pins by default. Use
--bpffs-path and --pin-dir to change this. They stay loaded until detach
runs.
bpfjctl wrap without --drop-cap and a non-root --uid leaves the command
able to remove itself from the jail. See bpfjctl wrap --help.
Run sudo build/bpfjlog while the jailer is attached to observe it. BPF
diagnostics are written to stderr and structured events to stdout. The logger
automatically reconnects when replace swaps in a new set of pinned maps.
replace loads a complete second jailer beside the active one, migrates pod
membership, variables and tracked resource ownership, then atomically swaps
the pin trees. Both trees remain attached during the handoff, forks and
enrollment are coordinated with the migration, and ownership changes are
journaled and replayed. Replacement fails closed if persisted layout versions
are incompatible or the state cannot be copied safely.
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"