
eBPF LSM 기반 강제 접근 제어 및 jailer
Linux용 eBPF 기반 강제 접근 제어(Mandatory Access Control).
이 프로젝트는 클로즈드 소스 BpfJailer의 완전한 재작성 버전이며 완전히 실험적입니다. 내부 BpfJailer가 작성되었을 당시에는 사용할 수 없었던 bpf arena와 같은 최신 기능을 활용합니다. 문제가 발생할 것으로 예상되며 버그 바운티 대상이 아니고 보안 취약점으로 간주되지도 않습니다. 적절히 평가된 후에는 내부 클로즈드 소스 버전을 대체할 것입니다.
BpfJailer는 eBPF LSM 프로그램을 사용하여 프로세스를 pod라고 불리는 jail에
가두며, 각 pod는 TOML 정책의 role에 바인딩됩니다. pod는 fork와 exec를
통해 상속됩니다. 선택적 정책 기능:
kill과 ptrace — 시그널을 보내거나 attach할 수 있는 대상
role/pod.bpf — 어떤 role의 eBPF 맵과 프로그램을 열 수 있는지, 또는
bpf(2)를 아예 호출할 수 있는지.keyring — 어떤 role의 fs-verity keyring에 인증서를 추가할 수
있는지, 또는 keyring에 아예 쓸 수 있는지.거부 및 수명주기 이벤트는 pinned ring buffer에 기록됩니다. bpfjlog는
사람이 읽을 수 있는 BPF 진단과 구조화된 이벤트를 출력하며, 라이브 정책
교체 전반에 걸쳐 ring buffer를 따라갑니다.
바이너리는 user.bpfj.policy.exec xattr을 통해 role을 주장할 수 있으며
exec 시점에 해당 role에 등록됩니다. 실행 중인 프로세스도 직접 등록할 수
있고, 비특권 프로세스는 bpfjsrv/bpfjclient를 통해 스스로를 등록할 수
있습니다.
| 디렉터리 | 바이너리 | 용도 |
|---|---|---|
bpfj/ | 핵심 라이브러리와 BPF 프로그램: jailer, enforcer, 정책 파서, libbpf C++ 헬퍼. | |
ctl/ | bpfjctl | jailer를 attach, reload, inspect, detach하고 프로세스를 등록하기 위한 범용 도구. |
cmd/ | bpfjcmd | 인수와 선택적으로 정책이 컴파일된 bpfjctl. argv를 무시하므로 정적으로 링크되고 단일 단위로 fs-verity 서명될 수 있습니다. |
srv/ | bpfjsrv | 이를 허용하는 role로 비특권 호출자를 등록하는 소켓 활성화 서버. |
client/ | bpfjclient | libbpf나 BPF 툴체인 의존성이 없는 bpfjsrv용 최소 클라이언트. |
log/ | bpfjlog | pinned 진단 및 구조화된 이벤트 ring buffer용 소비자. |
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(요구 사항 참조)도 필요하며, 명령줄에서
설정하거나 환경에 export하세요.
make # build/bpfjctl
make STATIC=1 # 공유 객체 의존성이 없는 bpfjctl
make client # build/bpfjclient, BPF 툴체인 불필요
make log # build/bpfjlog
make signing-key # 개발용 서명 키와 인증서 생성
make signed SIGNING_KEY=... SIGNING_CERT=... # 정적, fs-verity 서명된 bpfjctl
make srv SIGNING_KEY=... SIGNING_CERT=... # 정적, 서명된 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
테스트는 각각 마운트 네임스페이스를 생성하고 bpffs를 마운트하기 때문에
root로 실행해야 합니다. make test는 호출한 사용자로 빌드하고 sudo
아래에서 테스트 바이너리만 실행합니다.
동시 BPF LSM detach가 영향을 받는 커널을 패닉시킬 수 있으므로 테스트는
기본적으로 직렬로 실행됩니다. 집중된 케이스에는
make test TEST_ARGS=Suite.Test를 사용하고, 일회용 VM 내에서만 -j N
또는 BPFJTEST_JOBS=N을 선택적으로 사용하세요.
sudo bpfjctl check policy.toml # 정책을 파싱하고 내용을 보고
sudo bpfjctl attach policy.toml # jailer를 로드하고 pin
sudo bpfjctl replace policy.toml # jailed task를 해제하지 않고 reload
sudo bpfjctl wrap ROLE USER_ID -- CMD # 새 pod에서 CMD 실행
sudo bpfjctl enroll ROLE USER_ID PID [NAME=VALUE...] # 변수와 함께 등록
sudo bpfjctl show PID # 프로세스가 속한 pod
sudo bpfjctl list # 모든 pod와 그 프로세스
sudo bpfjctl detach # unpin 및 unload
프로그램은 기본적으로 /sys/fs/bpf/bpfj-pins 아래에 pin됩니다. 이를
변경하려면 --bpffs-path와 --pin-dir을 사용하세요. detach가 실행될
때까지 로드된 상태로 유지됩니다.
--drop-cap 없이 비root --uid로 bpfjctl wrap을 실행하면 명령이
스스로 jail에서 빠져나올 수 있습니다. bpfjctl wrap --help를 참조하세요.
jailer가 attach된 상태에서 sudo build/bpfjlog를 실행하여 관찰하세요.
BPF 진단은 stderr에, 구조화된 이벤트는 stdout에 기록됩니다. 로거는
replace가 새로운 pinned 맵 집합을 교체할 때 자동으로 재연결합니다.
replace는 활성 jailer 옆에 완전한 두 번째 jailer를 로드하고, pod
멤버십, 변수, 추적된 리소스 소유권을 마이그레이션한 다음 pin 트리를
원자적으로 교체합니다. 핸드오프 동안 두 트리 모두 attach된 상태로
유지되고, fork와 등록은 마이그레이션과 조정되며, 소유권 변경은
저널링되고 재생됩니다. 영속화된 레이아웃 버전이 호환되지 않거나 상태를
안전하게 복사할 수 없으면 교체는 fail closed됩니다.
base-role = "floor" # 선택 사항: 호스트의 모든 프로세스 등록
vars = ["vm_uuid"] # 알려진 변수 이름
[certs]
corp-ca = "MIIDXTCCAkWgAwIBAgIJAK..." # PEM 또는 base64 DER 인증서
[roles.floor]
any = true # 추적 전용 기본 role 열기
[roles.webserver]
enforce-binary-certs = ["corp-ca"] # exec는 이 중 하나로 서명되어야 함
kill-roles = ["floor"] # 자체 pod와 이 role들에 시그널 가능
ptrace-pod = true # 자체 pod만
proc-roles = ["floor"] # 이 role들에 대한 proc 파일 열기 가능
bpf-pod = true # 자체 pod의 BPF 객체만
lkm-any = false # 모듈 및 kexec 로딩 거부
mq-sysv-pod = true # 자체 pod의 SysV 큐만
mq-posix-pod = true # 자체 pod의 POSIX 큐만
shm-sysv-pod = true # 자체 pod의 SysV SHM만
shm-posix-pod = true # 자체 pod의 POSIX SHM만
keyring-own = true # 자체 role의 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]] # 캐시된 경로 정책
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]] # 경로명 bind 규칙
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]] # 목적지 및 허용된 파일시스템 유형
path = "/srv/data"
allow = true
filesystems = ["ext4", "xfs"]
[[roles.webserver.mount]]
path = "/run/webserver"
allow = true
filesystems = ["any"] # 이 목적지의 모든 파일시스템 유형
[[roles.webserver.umount]]
path = "/"
allow = false
[[roles.webserver.umount]]
path = "/srv/data"
allow = true
[roles.sandbox]
unpriv-enroll = true # 명시되지 않은 모든 작업은 거부됨
override-stacked = true # 아래에 스택된 role을 무시하고 단독으로 응답
대부분의 작업 게이트는 role에 해당 옵션이 없으면 거부됩니다. *-pod
옵션은 동일한 pod의 리소스를 허용하고, *-roles는 명명된 소유자 role을
추가하며, *-any는 해당 작업을 완전히 엽니다. keyring-own은 fs-verity
keyring이 pod가 아닌 role에 속하기 때문에 role 범위의 대응 옵션입니다.
enroll-roles는 bpfjsrv가 추가할 수 있는 유일한 role을 지정하며, 이것이
없으면 bpfjsrv를 통한 등록은 거부됩니다. Unix 경로명, mount, umount
작업은 해당 옵션이 없거나 일치하는 경로가 없으면 거부됩니다. 추상 Unix
소켓 이름은 opt-in 필터로 남아 있으므로, 구성되지 않았거나 일치하지 않는
추상 이름은 허용됩니다.
완전히 열린 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는 작업별 완전 개방 형식입니다.
여러 role을 보유한 프로세스는 모든 role이 동의할 때만 작업이
허용됩니다. Role은 최신 순으로 조회되며, override-stacked role은 그
아래의 role을 대신하여 응답합니다. kill과 ptrace의 대상 측은
override를 무시합니다. 대상이 보유한 모든 role이 나열되어야 합니다.
전체 의미론은 bpfj/policy/Policy.h에 문서화되어 있습니다.
vars는 허용 목록입니다. 정책에 나열되지 않은 변수를 설정하는 등록은
거부되며, vars가 전혀 없으면 어떤 pod도 변수를 가지지 않습니다.
replace는 각 pod의 변수를 이름으로 전달하며, 새 정책이 pod가 보유한
변수를 더 이상 나열하지 않으면 실패합니다.