seccomp-bpf: BPF Cổ Điển Lọc Syscall Trong Mọi Container
Bài 14 cưỡng chế bằng eBPF hiện đại (LSM). Nhưng có một cơ chế lọc dựa trên BPF cũ hơn nhiều và phổ biến hơn nhiều — nó đang chạy trong gần như mọi container bạn từng dùng: seccomp-bpf. Và nó dùng BPF cổ điển (cBPF), không phải eBPF. Bài này làm rõ khác biệt đó, soi seccomp thật trên cụm, rồi tự viết một filter.
cBPF và eBPF: hai thế hệ
"BPF" ban đầu (1992) là Berkeley Packet Filter — một máy ảo nhỏ để lọc gói cho tcpdump. Đó là cBPF (classic BPF): 2 thanh ghi (A, X), tập lệnh tí hon, không map, không helper, không vòng lặp. eBPF (Bài 1) là bản mở rộng năm 2014: 11 thanh ghi 64-bit, maps, helper, verifier mạnh — gần như một thứ khác hẳn.
Điều nhiều người không để ý: seccomp vẫn dùng cBPF, không phải eBPF. Khi cài một seccomp filter, ta nạp một mảng lệnh cBPF (struct sock_filter). Nhân dịch cBPF sang eBPF bên trong để chạy, nhưng API mà userspace thấy là cBPF thuần. Đây là lý do seccomp đáng nằm trong series eBPF: nó là họ hàng cổ của eBPF, và là ứng dụng BPF được triển khai rộng nhất hành tinh.
seccomp thật trên cụm: ai bị giới hạn, ai không
/proc/<pid>/status cho biết một tiến trình có seccomp không: trường Seccomp (0=tắt, 1=strict, 2=filter) và Seccomp_filters (số filter đang gắn). Quét toàn node:
pid 1161 pause filters=1 <- sandbox container của pod
pid 1290 aws-ebs-csi-dri filters=1
pid 1429 cilium-operator filters=1
pid 2645 csi-provisioner filters=1
pid 450 systemd-resolve filters=28 <- service systemd tự siết
pid 123 systemd-journal filters=17
Đọc ra bức tranh thật. Các container thường — pause (container sandbox của mỗi pod), csi-*, cilium-operator — chạy với Seccomp: 2, filters=1: đó là profile seccomp mặc định mà containerd áp khi khởi động container. Nhưng kiểm các pod privileged:
cilium-agent (pid 1857): Seccomp: 0 <- KHÔNG giới hạn
kubelet (pid 817): Seccomp: 0
containerd (pid 592): Seccomp: 0
Seccomp: 0 — không seccomp. Đây là điểm hay đính chính: không phải mọi container đều có seccomp. Pod privileged / hostPID (như cilium-agent, kubelet) chạy unconfined; và Kubernetes mặc định không áp seccomp lên workload trừ khi đặt securityContext.seccompProfile: RuntimeDefault (hoặc bật feature gate SeccompDefault). Containerd thì áp profile mặc định cho container không-privileged — nên pause và CSI sidecar có filters=1.
Còn systemd-resolve với 28 filter minh họa một tính chất cốt lõi: seccomp filter chồng lên nhau và không gỡ được. Mỗi dòng SystemCallFilter= trong unit file thêm một filter; đặt rồi thì còn đó tới khi tiến trình chết. Đây là chủ đích thiết kế — một sandbox tự nới lỏng được thì vô nghĩa.
Tự viết một filter cBPF
Một filter seccomp là chương trình cBPF chạy trên struct seccomp_data (số syscall nr, kiến trúc arch, các args), trả về một verdict. Viết filter chặn mkdir/mkdirat bằng EPERM, cho qua mọi syscall khác:
struct sock_filter filter[] = {
// A = arch; nếu không phải x86_64 thì cho qua (phòng thủ kiến trúc)
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// A = nr (số syscall)
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mkdir, 2, 0), // trúng -> nhảy tới RET ERRNO
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mkdirat, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), // syscall khác: cho
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
};
struct sock_fprog prog = { .len = 8, .filter = filter };
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // bắt buộc cho tiến trình thường
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); // cài filter
Đây là cBPF trần: BPF_STMT/BPF_JUMP là macro dựng từng lệnh; thanh ghi A nạp một field của seccomp_data bằng BPF_LD|BPF_ABS; các bước nhảy có offset tương đối (số lệnh nhảy qua khi đúng/sai). Không map, không helper — khác hẳn eBPF. Verdict: SECCOMP_RET_ALLOW cho syscall chạy, SECCOMP_RET_ERRNO | EPERM làm syscall trả -EPERM mà không thực thi (các verdict khác: KILL_PROCESS, TRAP gửi SIGSYS, USER_NOTIF...). PR_SET_NO_NEW_PRIVS là điều kiện bắt buộc để tiến trình không-đặc-quyền cài được seccomp.
Chạy: mkdir bị chặn, printf vẫn chạy
filter has 8 cBPF instructions
before filter: mkdir /tmp/sc-a -> OK
seccomp filter installed (mode 2)
after filter: mkdir /tmp/sc-b -> Operation not permitted
after filter: this printf still works (write syscall allowed)
Trước khi cài, mkdir chạy bình thường. Sau khi cài filter, đúng mkdir nhận Operation not permitted (EPERM filter trả), trong khi printf — vốn gọi syscall write, được SECCOMP_RET_ALLOW — vẫn in ra bình thường. Lọc chính xác ở mức từng syscall: chặn cái này, cho cái kia. Đây đúng là cơ chế một profile seccomp container dùng để chặn các syscall nguy hiểm (mount, kexec_load, ptrace...) mà vẫn cho ứng dụng chạy.
seccomp so với LSM BPF
Hai cơ chế cưỡng chế, khác tầng:
| seccomp-bpf | LSM BPF (Bài 14) | |
|---|---|---|
| BPF | cBPF (cổ điển) | eBPF |
| Lọc theo | số syscall + args thô | thao tác ngữ nghĩa (mở file này, mount...) |
| Thấy được | seccomp_data (nr, arch, args) |
đối tượng nhân thật (struct file *...) |
| Phạm vi | mỗi tiến trình, kế thừa qua fork | toàn hệ thống |
| Gỡ | không (chồng tới khi chết) | có (gỡ link) |
seccomp nhanh và đơn giản nhưng "mù" — nó thấy số syscall và đối số thô, không dereference được con trỏ (không biết openat đang mở file nào). LSM BPF thấy đối tượng nhân đã phân giải. Chúng bổ sung nhau: seccomp thu hẹp bề mặt syscall, LSM BPF cưỡng chế chính sách ngữ nghĩa.
🧹 Dọn dẹp
Không có gì để dọn ở mức hệ thống: filter seccomp chỉ sống trong tiến trình demo, tiến trình thoát là filter biến mất (seccomp gắn theo tiến trình, không phải toàn cục). Phần quét /proc/*/status chỉ đọc, không đụng gì. Node vẫn 140 chương trình eBPF. Mã nguồn (seccomp_demo.c) ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 15-seccomp-bpf.
Tổng kết
seccomp-bpf lọc syscall bằng BPF cổ điển (cBPF) — bản BPF nguyên thủy mà tcpdump dùng, không phải eBPF (nhân tự dịch cBPF→eBPF để chạy, nhưng API là cBPF). Nó là ứng dụng BPF phổ biến nhất: containerd áp một profile mặc định lên container thường (pause, CSI sidecar trên cụm có Seccomp: 2), systemd siết service của nó (systemd-resolve chồng 28 filter). Nhưng không phải mọi container đều có — pod privileged như cilium-agent/kubelet chạy Seccomp: 0, và Kubernetes mặc định không áp seccomp trừ khi đặt RuntimeDefault. Một filter là chương trình cBPF trên struct seccomp_data (nr/arch/args), cài bằng prctl(PR_SET_SECCOMP, ...) sau PR_SET_NO_NEW_PRIVS, trả verdict (ALLOW/ERRNO/KILL...). Ta viết 8 lệnh cBPF chặn mkdir bằng EPERM — chặn đúng nó, printf (write) vẫn chạy. Filter kế thừa qua fork/exec và không gỡ được (chủ đích: sandbox không tự nới lỏng). So với LSM BPF: seccomp lọc syscall thô và mù con trỏ; LSM thấy đối tượng nhân — hai tầng bổ sung nhau.
Bài 16 khép Part V bằng cách hệ Kubernetes thực sự làm an ninh runtime: Tetragon — quan sát bằng kprobe/tracepoint rồi cưỡng chế (kill tiến trình, chặn) ngay trong nhân, và ta sẽ mổ cơ chế nó dùng để biến quan sát thành hành động.