Kiểu Tetragon: Từ Quan Sát Đến Cưỡng Chế Bằng bpf_send_signal
Bài 14 (LSM) và 15 (seccomp) cưỡng chế ở các điểm an ninh chuyên dụng. Nhưng cách hệ Kubernetes làm an ninh runtime phổ biến nhất — Tetragon (dự án của Cilium) — lại đi đường khác: nó gắn vào chính các hook quan sát mà Part II dùng (kprobe, tracepoint), rồi từ đó hành động. Bài này mổ cơ chế biến quan sát thành cưỡng chế, và tự dựng lại nó.
Tetragon: công cụ quan sát + một nút "hành động"
Tetragon về bản chất là một công cụ quan sát kprobe/tracepoint (như execsnoop Bài 9) cộng thêm khả năng cưỡng chế. Theo tài liệu Tetragon, nó enforce bằng hai cách, cả hai đều là helper eBPF chạy thẳng trong nhân, không cần chuyển sang userspace:
bpf_send_signal()— gửi một signal (thườngSIGKILL) cho tiến trình vừa khớp chính sách, giết nó đồng bộ ngay trong nhân.bpf_override_return()— ghi đè giá trị trả về của hàm/syscall (vd épopenattrả-EPERM), chặn thao tác mà không giết tiến trình.
Điểm tinh tế: chương trình eBPF so khớp sự kiện với chính sách trong nhân; nếu khớp thì gọi helper — không có vòng đi userspace nào trong đường cưỡng chế. Đây là điều khiến nó nhanh và khó lách.
Dựng lại cơ chế SIGKILL
Ta dựng đúng cơ chế (1): một tracepoint trên sched_process_exec (như Bài 9) nhưng thay vì in sự kiện, nó giết tiến trình nếu đó là một binary bị cấm. Hẹp và an toàn: chỉ giết tiến trình exec đúng /tmp/forbidden-bin.
SEC("tracepoint/sched/sched_process_exec")
int kill_forbidden(struct trace_event_raw_sched_process_exec *ctx)
{
char fn[24] = {};
unsigned off = ctx->__data_loc_filename & 0xFFFF;
bpf_probe_read_kernel_str(fn, sizeof(fn), (void *)ctx + off); // tên file exec
const char want[] = "/tmp/forbidden-bin";
for (int i = 0; i < sizeof(want) - 1; i++)
if (fn[i] != want[i])
return 0; // không phải mục tiêu -> cho chạy
bpf_send_signal(9); // SIGKILL chính tiến trình này
return 0;
}
Nửa đầu giống hệt một công cụ quan sát — đọc tên file đang exec từ field động của tracepoint (Bài 9). Khác biệt duy nhất là dòng cuối: thay vì bpf_ringbuf_submit để báo cáo, nó gọi bpf_send_signal(9) — gửi SIGKILL cho tiến trình hiện tại (tiến trình vừa exec). Quan sát biến thành cưỡng chế chỉ bằng một helper.
Chạy: binary cấm bị giết, binary thường chạy
Gắn bằng bpftool (autoattach), rồi thử:
sudo bpftool prog loadall tetra_kill.bpf.o /sys/fs/bpf/tetra autoattach
cp /bin/sleep /tmp/forbidden-bin
/bin/sleep 0.2 # binary thường
/tmp/forbidden-bin 5 # binary cấm
844: tracepoint name kill_forbidden ... <- gắn vào exec
-- normal /bin/sleep 0.2 --
sleep OK (exit 0) <- chạy bình thường
-- /tmp/forbidden-bin 5 --
Killed
exit=137 (137 = 128 + SIGKILL) <- bị giết NGAY khi exec
/bin/sleep chạy xong bình thường. /tmp/forbidden-bin (cùng là sleep, chỉ khác tên) bị Killed ngay lập tức, vỏ shell báo exit=137 — đúng 128 + 9 (SIGKILL). Tiến trình chết ngay tại thời điểm exec, trước khi chạy được lệnh nào. Chính sách "cấm binary này" được cưỡng chế hoàn toàn trong nhân, từ một tracepoint quan sát.
Một sự thật quan trọng: SIGKILL không phải lúc nào cũng đủ
Tài liệu Tetragon nêu rõ một điểm dễ bị bỏ qua: gửi SIGKILL đồng bộ dừng tiến trình, nhưng không phải lúc nào cũng ngăn được thao tác đang diễn ra. Ví dụ SIGKILL bắn trong một write() không đảm bảo dữ liệu chưa kịp ghi xuống file — syscall có thể đã làm xong phần việc trước khi tiến trình chết. Vì thế để chắc chắn chặn thao tác, phải kết hợp: dùng bpf_override_return() ép syscall trả lỗi (thao tác không thực hiện) và bpf_send_signal() để giết tiến trình. Cưỡng chế ở exec (như demo) thì SIGKILL là đủ — tiến trình chết trước khi làm gì; nhưng cưỡng chế ở một syscall ghi/sửa thì phải override.
(Cụm này có CONFIG_BPF_KPROBE_OVERRIDE=y và CONFIG_FUNCTION_ERROR_INJECTION=y, nên bpf_override_return dùng được — nhưng nó chỉ gắn được vào hàm có đánh dấu ALLOW_ERROR_INJECTION, một danh sách hẹp các điểm an toàn để ghi đè.)
Vì sao đường này không cần reboot
Khác Bài 14: demo này gắn vào tracepoint, không phải hook LSM, và cưỡng chế bằng signal — nên nó chạy không cần bpf trong danh sách LSM, không cần reboot. Đó cũng là lý do Tetragon chọn đường này: nó hoạt động trên kernel phổ thông không bật BPF LSM, chỉ cần kprobe/tracepoint + bpf_send_signal (có từ kernel 5.3). Đánh đổi: nó cưỡng chế sau khi sự kiện đã bắt đầu (giết tiến trình đang/đã exec), khác LSM chặn trước khi thao tác xảy ra. Mỗi cơ chế một chỗ đứng.
🧹 Dọn dẹp
sudo rm -rf /sys/fs/bpf/tetra # bỏ pin -> gỡ chương trình
rm -f /tmp/forbidden-bin
Gỡ xong bpftool prog show không còn kill_forbidden; node về 140 chương trình. Mã nguồn (tetra_kill.bpf.c, lệnh build/attach) ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 16-tetragon-style.
Tổng kết
Tetragon làm an ninh runtime bằng cách gắn vào hook quan sát (kprobe/tracepoint) rồi cưỡng chế qua hai helper eBPF chạy thẳng trong nhân: bpf_send_signal() gửi SIGKILL giết tiến trình đồng bộ, và bpf_override_return() ghi đè giá trị trả về syscall để chặn thao tác. Ta dựng lại cơ chế SIGKILL: một tracepoint sched_process_exec (y như công cụ quan sát Bài 9) nhưng dòng cuối gọi bpf_send_signal(9) thay vì báo cáo — binary cấm /tmp/forbidden-bin bị giết ngay tại exec (exit 137 = 128+SIGKILL), binary thường chạy bình thường. Sự thật từ tài liệu: SIGKILL đồng bộ không phải lúc nào cũng ngăn được thao tác đang dở (vd write có thể đã ghi), nên muốn chắc phải kết hợp bpf_override_return. Đường này gắn vào tracepoint + signal nên không cần BPF LSM hay reboot (khác Bài 14) — chạy trên kernel phổ thông, đánh đổi là cưỡng chế sau khi sự kiện bắt đầu thay vì chặn trước.
Part V khép lại — ba cơ chế cưỡng chế: LSM BPF (chặn trước, ngữ nghĩa, cần bpf LSM), seccomp (lọc syscall thô, mọi container), kiểu Tetragon (kprobe/tracepoint + signal/override, không cần LSM). Part VI quay lại quan sát nhưng ở tầng sâu nhất: observability hiệu năng — profiling bằng perf_event, đo độ trễ tầng nhân, off-CPU, và mổ cách Hubble dựng bức tranh luồng mạng toàn cụm từ các sự kiện eBPF.