Nguyên lý hoạt động của eBPF và JIT Compiler trong Kernel
Để hiểu cách eBPF bảo vệ database, bạn cần nắm cơ chế biên dịch mã trung gian (Intermediate Representation) thành mã máy (Machine Code) chạy trực tiếp trong kernel mà không cần tải module.
Quy trình hoạt động gồm 3 bước: Viết chương trình bằng C/Assembly, biên dịch thành Bytecode, và Kernel kiểm tra (Verifier) rồi biên dịch JIT (Just-In-Time) thành mã x86_64 thực thi.
1. Kiểm tra hỗ trợ eBPF trên Kernel hiện tại
Trước tiên, hãy xác minh kernel của server có bật tính năng eBPF và JIT compiler hay không.
Thực hiện lệnh kiểm tra file cấu hình kernel để đảm bảo các flag cần thiết được enable.
grep CONFIG_BPF /boot/config-$(uname -r)
Kết quả mong đợi: Bạn thấy các dòng CONFIG_BPF=y và CONFIG_BPF_JIT=y. Nếu thấy n hoặc không có dòng này, eBPF sẽ không hoạt động được.
2. Kiểm tra trạng thái JIT Compiler đang chạy
Để xem JIT compiler đang hoạt động, chúng ta kiểm tra file debugfs của kernel.
Thực hiện lệnh đọc file trạng thái của BPF JIT.
cat /sys/kernel/debug/bpf/jit_enable 2>/dev/null || echo "Debugfs not mounted, trying sysctl"
Nếu debugfs chưa mount, hãy mount và kiểm tra lại bằng lệnh sau.
mount -t debugfs none /sys/kernel/debug && cat /sys/kernel/debug/bpf/jit_enable
Kết quả mong đợi: Giá trị trả về là 1 (Bật). Nếu là 0, JIT compiler bị tắt, hiệu năng sẽ giảm mạnh.
3. Xác minh chương trình eBPF chạy qua JIT
Sử dụng công cụ bpftrace để chạy một script mẫu và quan sát việc JIT compiler biên dịch.
Thực hiện lệnh chạy script đếm số lần syscall open xảy ra.
bpftrace -e 'tracepoint:syscalls:sys_enter_open { printf("Open syscall: %s\n", comm); }' -d 2>&1 | grep -i "jit"
Kết quả mong đợi: Trong output log, bạn thấy dòng thông báo JIT enabled hoặc JIT compilation successful. Điều này chứng tỏ kernel đã biên dịch bytecode thành mã máy x86 để chạy.
Các loại eBPF programs phù hợp cho bảo mật Database
Không phải loại eBPF program nào cũng dùng để chặn tấn công. Đối với Database (MySQL, PostgreSQL, MongoDB), chúng ta tập trung vào các loại truy cập vào syscall và mạng.
1. Kprobes và Tracepoints cho Syscall Monitoring
Dùng để can thiệp vào các điểm gọi hàm (kernel functions) hoặc điểm đánh dấu (tracepoints) khi ứng dụng database gọi syscall.
Loại này phù hợp để chặn các hành vi như: Đọc file config nhạy cảm, thay đổi quyền file, hoặc gọi execve trái phép từ tiến trình database.
bpftrace -e 'kprobe:sys_open { printf("Process: %s opened: %s\n", comm, str(args->filename)); }'
Kết quả mong đợi: Khi bạn chạy lệnh cat /etc/mysql/my.cnf, bạn sẽ thấy log in thời gian thực hiển thị tên tiến trình và file được mở.
2. TC (Traffic Control) cho Network Filtering
Dùng để xử lý gói tin mạng (packet) trước khi nó vào hoặc ra khỏi network stack của kernel.
Đây là cơ chế mạnh nhất để chặn SQL Injection hoặc DDoS ở tầng L3/L4 trước khi gói tin đến ứng dụng database.
ip link set dev eth0 type bpf obj /path/to/filter.o sec ingress
Kết quả mong đợi: Các gói tin không thỏa mãn rule trong file filter.o sẽ bị drop ngay lập tức, không đến được socket của database.
3. Cgroup_skb cho Access Control
Dùng để gắn chương trình eBPF vào cgroup của tiến trình database, kiểm soát quyền truy cập mạng của riêng tiến trình đó.
Phù hợp để cô lập database: Cho phép database chỉ kết nối với App Server, chặn kết nối từ Internet trực tiếp.
bpftrace -e 'cgroup_skb:ingress { if (pid == 1234) { print("DB traffic blocked"); exit(1); } }'
Kết quả mong đợi: Nếu tiến trình ID 1234 (database) cố gửi gói tin, nó sẽ bị chặn và log thông báo "DB traffic blocked".
Cấu trúc luồng dữ liệu của Linux Audit subsystem
Audit subsystem là cơ chế ghi log bảo mật mặc định của Linux. Hiểu luồng dữ liệu giúp bạn biết điểm nào cần can thiệp để tăng tốc độ ghi log hoặc bổ sung logic chặn.
1. Luồng dữ liệu từ Syscall đến Audit Log
Luồng hoạt động chuẩn: User Space (App) -> Kernel Syscall -> Audit Hook (Kernel) -> Audit Buffer -> auditd (Daemon) -> File Log.
Để kiểm tra luồng này đang hoạt động, hãy tạo một audit rule để theo dõi truy cập vào thư mục dữ liệu database.
auditctl -w /var/lib/mysql -p wa -k mysql_data_access
Kết quả mong đợi: Không có lỗi, rule được thêm vào danh sách. Khi bạn chạy lệnh touch /var/lib/mysql/test.log, auditd sẽ ghi log.
2. Xác minh luồng ghi log của Audit
Thực hiện thao tác kích hoạt audit rule và xem log đầu ra để đảm bảo dữ liệu đi đúng hướng.
touch /var/lib/mysql/test_write && ausearch -k mysql_data_access
Kết quả mong đợi: Bạn thấy một dòng log chứa type=SYSCALL và key=mysql_data_access, chứng tỏ audit hook đã bắt được sự kiện và đưa vào buffer cho auditd xử lý.
3. Kiểm tra trạng thái Audit Buffer
Quan sát buffer nội bộ của kernel để xem audit đang giữ bao nhiêu sự kiện trước khi dump ra file.
auditctl -s
Kết quả mong đợi: Output hiển thị trạng thái auditd is running và space left in buffer. Nếu buffer đầy, các sự kiện mới có thể bị bỏ sót (overflow).
So sánh ưu nhược điểm giữa eBPF và Audit rules truyền thống
Để chọn giải pháp bảo vệ database tối ưu, bạn cần phân tích sự khác biệt về hiệu năng và khả năng can thiệp giữa hai công nghệ này.
1. Bảng so sánh kỹ thuật
- Auditd: Chỉ ghi log (Logging). Không thể chặn (Block) ngay trong kernel. Overhead cao do phải copy dữ liệu từ kernel sang user-space daemon.
- eBPF: Có thể ghi log VÀ chặn (Block/Drop) ngay trong kernel. Overhead cực thấp (microsecond) nhờ JIT compilation chạy trực tiếp trên CPU.
2. Thử nghiệm đo độ trễ (Latency)
Chúng ta sẽ so sánh thời gian phản hồi khi Auditd ghi log so với eBPF ghi log đơn giản.
Bước 1: Tạo 1000 file rỗng và đo thời gian khi Auditd đang bật.
time for i in {1..1000}; do touch /tmp/audit_test_$i; done
Bước 2: Tắt Audit rule và chạy lại với script eBPF đơn giản (nếu có bpftrace).
auditctl -D && time for i in {1..1000}; do touch /tmp/ebpf_test_$i; done
Kết quả mong đợi: Thời gian thực thi ở bước 2 thường nhanh hơn đáng kể (giảm 20-50ms tùy phần cứng), chứng tỏ overhead của auditd daemon lớn hơn eBPF.
3. Khả năng can thiệp chủ động
Thử nghiệm khả năng chặn hành vi: Auditd chỉ báo lỗi, eBPF có thể kill tiến trình.
Thử với Auditd: Chạy lệnh xóa file quan trọng, chỉ thấy log sau khi file đã mất.
rm /var/lib/mysql/important_table.ibd 2>/dev/null && ausearch -f important_table.ibd
Thử với eBPF (mô phỏng): Dùng bpftrace để in log và giả lập việc ngăn chặn (trong thực tế cần write eBPF C program để return -1).
bpftrace -e 'tracepoint:syscalls:sys_enter_unlink { if (args->filename == "/var/lib/mysql/important_table.ibd") { print("BLOCKED: Delete attempt"); exit(1); } }'
Kết quả mong đợi: Auditd chỉ ghi log sau khi xóa. Script eBPF in dòng BLOCKED trước khi lệnh thực thi thành công (trong môi trường production, eBPF có thể drop packet hoặc kill process ngay).
Cách eBPF tương tác với Syscall và Network Stack
Đây là phần cốt lõi để hiểu cách eBPF bảo vệ database: nó gắn vào các điểm hook cụ thể trong kernel stack.
1. Tương tác với Syscall (Kprobes/Tracepoints)
eBPF can thiệp vào syscall bằng cách gắn hook vào điểm vào (sys_enter) và điểm ra (sys_exit) của hàm syscall trong kernel.
Cơ chế: Khi ứng dụng database gọi read(), kernel gọi sys_read. eBPF hook vào điểm này để đọc tham số (file descriptor, buffer size) và quyết định cho phép hay không.
bpftrace -e 'tracepoint:syscalls:sys_enter_read { printf("PID: %d, FD: %d, Bytes: %d\n", pid, args->fd, args->count); }'
Kết quả mong đợi: Khi database đọc dữ liệu, bạn thấy log chi tiết về Process ID, File Descriptor và số byte đọc ngay tại thời điểm syscall được gọi.
2. Tương tác với Network Stack (XDP/TC)
eBPF can thiệp vào network stack ở hai điểm: XDP (Early packet processing) và TC (Traffic Control).
Cơ chế: Gói tin từ mạng đến NIC -> XDP (eBPF chạy ở đây, quyết định Drop/Accept) -> Kernel Network Stack -> Socket -> Application. Nếu eBPF ở XDP trả về XDP_DROP, gói tin không bao giờ đến socket của database.
Để xem eBPF đang load ở đâu, kiểm tra các interface mạng.
ip -d link show dev eth0 | grep -i "bpf"
Kết quả mong đợi: Nếu đã load program, bạn thấy dòng bpf (XDP) hoặc bpf (ingress/egress) trong thông tin của interface eth0.
3. Kiểm tra tương tác thực tế với gói tin SQL
Giả lập việc eBPF bắt gói tin TCP port 3306 (MySQL) và in nội dung header.
Dùng bpftrace để hook vào tracepoint mạng.
bpftrace -e 'kprobe:tcp_v4_rcv { if (args->sk->sk_family == 2 && args->sk->sk_num == 3306) { print("MySQL Packet received"); } }'
Kết quả mong đợi: Khi bạn thực hiện kết nối vào MySQL (ví dụ mysql -u root -p), script sẽ in dòng MySQL Packet received, chứng tỏ eBPF đã tương tác thành công với network stack để bắt gói tin.
Điều hướng series:
Mục lục: Series: Triển khai Database chống tấn công với Linux eBPF và Kernel Audit
« Phần 1: Chuẩn bị môi trường và kiến trúc hệ thống
Phần 3: Cấu hình Auditd để giám sát truy cập database »