Phân tích và Giảm thiểu Overhead của eBPF Program
Trong môi trường Database có tải cao (High Load), mỗi microsecond delay từ eBPF program đều có thể gây ra bottleneck cho I/O. Nguyên nhân chính thường là do logic xử lý trong program quá phức tạp hoặc số lượng map quá lớn.
Bước 1: Đo lường thời gian thực thi (latency) của eBPF program đang chạy.
Chúng ta sử dụng bpftrace để hook vào điểm entry của program và đo thời gian.
bpftrace -e 'tracepoint:syscalls:sys_enter_write { printf("%s: %lldns\n", comm, nsecs); }'
Kết quả mong đợi: Danh sách các tiến trình (comm) kèm thời gian thực thi (ns). Nếu thấy các giá trị > 500ns thường xuyên, program cần tối ưu.
Bước 2: Kiểm tra số lần chạy (hit count) và thời gian trung bình.
Để có cái nhìn tổng quan về overhead, ta dùng bpftool để inspect các map lưu số liệu thống kê.
bpftool map list | grep -E "perf|stats" | xargs -I {} bpftool map dump {}
Kết quả mong đợi: Xem số lượng key-value trong map. Nếu map bị tràn hoặc kích thước quá lớn (ví dụ > 100MB), kernel sẽ tốn CPU để quản lý memory.
Bước 3: Tối ưu code C của eBPF.
Nguyên tắc vàng: Tránh gọi hàm helper tốn kém (như `bpf_probe_read_user` liên tục) và tránh logic branching (if/else) sâu trong hot path.
Thay đổi logic trong file nguồn (ví dụ: `db_guard.c`) để sử dụng `bpf_map_lookup_elem` thay vì `bpf_probe_read` khi có thể.
// Thay thế đoạn code cũ
// u64 *val = bpf_map_lookup_elem(&my_map, &key);
// if (val) {
// if (*val > THRESHOLD) bpf_printk("BLOCKED");
// }
// Tối ưu: Dùng BPF_CORE_READ nếu cần access cấu trúc phức tạp, hoặc đơn giản hóa điều kiện
u64 *val = bpf_map_lookup_elem(&my_map, &key);
if (val && *val > THRESHOLD) {
// Tránh printk trong hot path, thay vào đó ghi vào perf buffer
struct event e = {0};
e.ts = bpf_ktime_get_ns();
bpf_perf_event_output(ctx, &perf_buf, BPF_F_CURRENT_CPU, &e, sizeof(e));
return 1; // Block
}
return 0;
Kết quả mong đợi: Chạy lại lệnh bpftrace ở Bước 1, thời gian thực thi giảm xuống dưới 100ns.
Verify: Chạy benchmark TPC-C hoặc Sysbench, so sánh throughput (TPS) trước và sau khi tối ưu.
Xử lý Lỗi 'Permission Denied' khi Load eBPF Program
Lỗi "Permission denied" khi chạy `bpf()` syscall thường do kernel không cho phép load program không được ký (unsigned) hoặc CAP_BPF capability bị hạn chế.
Bước 1: Kiểm tra trạng thái Secure Boot và BPF restrictions.
Trên các distro hiện đại (Ubuntu 22.04+, RHEL 9+), BPF strict mode có thể được bật.
grep -r "bpf_jit_enable" /sys/kernel/bpf/ 2>/dev/null || echo "Check dmesg for bpf errors"
dmesg | grep -i "bpf" | tail -20
Kết quả mong đợi: Thông báo lỗi cụ thể như "unprivileged bpf is disabled" hoặc "verification failed".
Bước 2: Tắt Unprivileged BPF restriction (nếu không dùng Signed Program).
Để load program từ user-space mà không cần ký, ta cần disable strict mode hoặc cho phép unprivileged.
echo 0 > /proc/sys/kernel/unprivileged_bpf_disabled
Kết quả mong đợi: Lệnh chạy thành công, không báo lỗi permission.
Bước 3: Cấu hình kernel parameter để cho phép JIT và load module.
Thêm tham số vào GRUB để đảm bảo BPF hoạt động đầy đủ.
Chỉnh sửa file /etc/default/grub:
GRUB_CMDLINE_LINUX="bpf_jit_enable=1 bpf_jit_harden=0"
Cập nhật GRUB:
update-grub
Kết quả mong đợi: Sau khi reboot, lệnh `bpf()` syscall hoạt động bình thường.
Bước 4: Kiểm tra capability của user hiện tại.
Đảm bảo user chạy program có quyền CAP_BPF và CAP_PERFMON.
getcap -r /usr/bin/bpftrace
setcap cap_bpf,cap_perfmon+ep /usr/bin/bpftrace
Kết quả mong đợi: User có thể load program mà không cần sudo (hoặc sudo không báo permission denied).
Verify: Chạy lại lệnh load eBPF program của bạn (ví dụ: `clang -O2 -target bpf -c db_guard.c -o db_guard.o` sau đó load bằng bpftool).
Khắc phục sự cố Log Audit bị tràn hoặc mất dữ liệu
Khi auditd được cấu hình để log quá nhiều sự kiện (high volume), file log (`/var/log/audit/audit.log`) sẽ đầy và hệ thống có thể bị treo hoặc mất log quan trọng do cơ chế "max_log_file" và "max_log_file_backup".
Bước 1: Kiểm tra tình trạng file log và kích thước.
du -sh /var/log/audit/*
systemctl status auditd
Kết quả mong đợi: Nếu thấy file log đạt kích thước giới hạn hoặc service bị restart liên tục, cần can thiệp.
Bước 2: Điều chỉnh cấu hình auditd để xử lý tràn log.
Chỉnh sửa file /etc/audit/auditd.conf:
# /etc/audit/auditd.conf
max_log_file = 100
max_log_file_backup = 10
num_logs = 10
admin_space_left_action = SUSPEND
space_left_action = SYSLOG
action_mail_acct = root
space_left = 100MB
admin_space_left = 50MB
max_discard_rate = 50000
Kết quả mong đợi: Auditd sẽ tự động rotate log khi đạt 100MB, giữ lại 10 file backup, và giảm tốc độ ghi (drop rate) nếu quá tải.
Bước 3: Thiết lập chính sách giữ lại log quan trọng (Filtering).
Thay vì log tất cả, ta chỉ log các sự kiện database quan trọng. Xóa rule không cần thiết.
auditctl -l | grep -v "mysql\|postgres\|sql" | cut -d: -f2 | xargs -I {} auditctl -D -a exit,always -F arch=b64 -S {}
(Lưu ý: Lệnh trên là ví dụ, thực tế dùng `auditctl -D` để xóa tất cả rules cũ trước khi load lại rules file).
Tốt hơn: Sửa file /etc/audit/rules.d/audit.rules để chỉ giữ lại rules cần thiết.
# /etc/audit/rules.d/audit.rules
-D
-w /var/lib/mysql -p wa -k mysql_db
-w /var/lib/pgsql -p wa -k postgres_db
-a exit,always -F arch=b64 -S open -F a0>=0 -F success=0 -F exit=-EACCES -k mysql_access_denied
Reload rules:
auditctl -R /etc/audit/rules.d/audit.rules
Kết quả mong đợi: Tốc độ ghi log giảm, chỉ còn lại các sự kiện quan trọng, không còn tràn disk.
Bước 4: Cấu hình logrotate để tự động dọn dẹp.
Thêm file /etc/logrotate.d/audit:
# /etc/logrotate.d/audit
/var/log/audit/audit.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
postrotate
/usr/sbin/auditctl -R /etc/audit/rules.d/audit.rules
endscript
}
Kết quả mong đợi: Log cũ bị nén và xóa sau 30 ngày, không chiếm dụng disk.
Verify: Chạy `auditctl -s` để xem trạng thái buffer, đảm bảo `lost` và `disk_full` bằng 0.
Kỹ thuật Debugging eBPF sử dụng bpftool và bpftrace
Khi eBPF program bị lỗi logic (không chặn đúng, hoặc crash), chúng ta cần xem nội bộ trạng thái của program và map.
Bước 1: Dump nội dung Map để kiểm tra dữ liệu.
Sử dụng bpftool để xem key-value trong map đang được program sử dụng.
bpftool map dump pin /sys/fs/bpf/db_guard_whitelist
Kết quả mong đợi: Danh sách các IP hoặc User được whitelist. Nếu thiếu, program sẽ chặn sai.
Bước 2: Kiểm tra lỗi Verification (Verifier Errors).
Khi load program, nếu kernel từ chối, ta cần xem lý do.
bpftool prog load db_guard.o /sys/fs/bpf/db_guard --pin /sys/fs/bpf/db_guard 2>&1 | grep -A 10 "Verifier"
Kết quả mong đợi: Thông báo lỗi chi tiết như "R1 invalid mem access" hoặc "invalid mem access size".
Bước 3: Sử dụng bpftrace để trace variable trong runtime.
Thêm `bpf_printk` vào code C của bạn, sau đó dùng bpftrace để xem log.
bpftrace -e 'tracepoint:bpf:trace_printk { printf("%s\n", args->fmt); }'
Kết quả mong đợi: Xem các thông tin debug mà bạn đã chèn vào code C (ví dụ: giá trị biến `threshold`, kết quả so sánh).
Bước 4: Sử dụng bpftool để inspect program stats.
bpftool prog show name db_guard
Kết quả mong đợi: Xem `run_cnt` (số lần chạy) và `run_time_ns`. Nếu `run_cnt` tăng nhưng không có log output, program có thể bị lỗi logic (luôn return 0).
Bước 5: Debug Map Locking issues.
Nếu program bị treo, có thể do deadlock trong map.
bpftool map show | grep -i "lock"
Kết quả mong đợi: Kiểm tra xem map có sử dụng `BPF_MAP_TYPE_HASH` hay `PERCPU_HASH`. Nếu map bị lock, hãy chuyển sang loại map khác hoặc giảm kích thước.
Verify: Chạy lại test case (ví dụ: SQL Injection), quan sát log từ bpftrace để xác nhận logic đúng.
Mẹo Tối ưu Cấu hình Kernel để Giảm Latency cho Database
Cấu hình mặc định của Linux thường ưu tiên công bằng (fairness) hơn là tốc độ (latency). Để database chạy mượt với eBPF, cần tinh chỉnh kernel.
Bước 1: Điều chỉnh Scheduler.
Đổi scheduler từ CFS sang RT (Real-Time) hoặc điều chỉnh nice value cho process database.
echo -1 > /proc/sys/kernel/sched_migration_cost_ns
Hoặc set priority cho process MySQL/Postgres:
renice -n -20 -p $(pgrep -f mysqld)
Kết quả mong đợi: Process database nhận CPU ưu tiên hơn, giảm jitter.
Bước 2: Tối ưu Network Stack cho eBPF.
Giảm overhead của TCP stack để eBPF không bị nghẽn ở layer network.
sysctl -w net.core.netdev_budget=300
sysctl -w net.core.netdev_budget_usecs=8000
sysctl -w net.ipv4.tcp_ecn=0
Kết quả mong đợi: Tăng throughput mạng, giảm latency packet processing.
Bước 3: Disable unnecessary Kernel Features.
Tắt các tính năng không dùng đến để giảm overhead CPU.
sysctl -w vm.dirty_ratio=10
sysctl -w vm.dirty_background_ratio=5
sysctl -w kernel.sched_min_granularity_ns=10000
Kết quả mong đợi: Giảm I/O wait, tăng tốc độ commit transaction.
Bước 4: Cấu hình NUMA (Non-Uniform Memory Access).
Đảm bảo process database và eBPF program chạy trên cùng NUMA node.
numactl --cpunodebind=0 --membind=0 mysqld
Kết quả mong đợi: Giảm latency truy cập RAM, tăng performance.
Verify: Chạy `sysbench --test=threads --num-threads=64 --max-time=60s run` và so sánh kết quả trước/sau khi điều chỉnh.
Chiến lược Rollback An toàn khi Cấu hình bị lỗi
Khi cấu hình eBPF hoặc auditd gây ra sự cố nghiêm trọng (ví dụ: chặn luôn cả truy cập hợp lệ), cần có kế hoạch rollback ngay lập tức.
Bước 1: Tạo Script Rollback tự động.
Viết script shell để tắt tất cả rules audit và unload eBPF programs.
File /usr/local/bin/rollback_ebpf_audit.sh:
#!/bin/bash
# Unload all eBPF programs
bpftool prog list | grep "db_guard\|sql_inject" | cut -d' ' -f1 | xargs -I {} bpftool prog delete id {}
# Remove all audit rules
auditctl -D
# Disable auditd service (optional, nếu cần tắt hoàn toàn)
systemctl stop auditd
# Clear log buffer
echo "Rollback completed. System is now in safe mode."
Cấp quyền thực thi:
chmod +x /usr/local/bin/rollback_ebpf_audit.sh
Kết quả mong đợi: Script sẵn sàng để chạy khi cần thiết.
Bước 2: Thiết lập Watchdog hoặc Timeout tự động rollback.
Sử dụng systemd timer hoặc cron để chạy script rollback nếu không nhận được "heartbeat" trong 5 phút.
# /etc/cron.d/ebpf_watchdog
*/1 * * * * root /usr/local/bin/rollback_ebpf_audit.sh > /dev/null 2>&1
(Trong thực tế, script này nên kiểm tra file heartbeat trước khi rollback).
Kết quả mong đợi: Hệ thống tự động phục hồi nếu eBPF bị treo hoặc gây deadlock.
Bước 3: Sử dụng cơ chế "Soft Block" thay vì "Hard Block".
Thay vì return 1 (chặn) ngay lập tức, hãy log sự kiện và giảm điểm uy tín (score). Chỉ chặn khi score vượt ngưỡng.
// Trong code C
if (score > 100) {
// Log và giảm điểm, không chặn ngay
bpf_map_update_elem(&score_map, &key, &score, BPF_ANY);
return 0; // Cho phép qua
}
if (score > 200) {
// Chặn
return 1;
}
Kết quả mong đợi: Giảm thiểu rủi ro chặn sai, có thời gian để con người can thiệp.
Bước 4: Kiểm tra nhanh trạng thái sau rollback.
bpftool prog list | wc -l
auditctl -l | wc -l
Kết quả mong đợi: Số lượng program và rules về 0, hệ thống hoạt động như chưa có bảo mật eBPF.
Verify: Chạy thử script rollback, sau đó kiểm tra xem database có thể truy cập bình thường hay không.
Đ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 6: Tích hợp eBPF và Audit vào hệ thống giám sát tập trung