Tự viết Seccomp Profile cho Docker: Siết chặt System Calls từ Dev đến Prod

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Bối cảnh: Vì sao Seccomp lại quan trọng?

Container không phải máy ảo. Về bản chất, chúng chỉ là tiến trình chạy chung Linux kernel với máy host, được bọc lại bằng cgroups và namespaces. Nếu web app dính lỗi Remote Code Execution (RCE), kẻ tấn công sẽ tương tác thẳng với kernel host qua các system call (syscall).

Hệ điều hành Linux x86_64 hiện có hơn 450 syscall. Một service Node.js hay Go API thông thường chỉ cần khoảng 40 đến 70 syscall để phục vụ request. Thế nhưng theo mặc định, Docker chỉ chặn khoảng 44 syscall nhạy cảm nhất (như reboot, kexec_load). Hơn 300 syscall còn lại vẫn mở toang.

Nếu kernel xuất hiện lỗ hổng Zero-day ở một syscall hiếm dùng như bpf() hay keyctl(), hacker có thể leo thang đặc quyền để thoát khỏi container (container breakout). Lúc đó, máy chủ của bạn coi như mất trắng.

Cách xử lý triệt để nhất: chuyển từ Blacklist sang Whitelist (Default Deny). Ứng dụng cần syscall nào, ta mở đúng syscall đó. Toàn bộ phần còn lại đều bị chặn.

Chuẩn bị môi trường

Trước tiên, bạn cần kiểm tra xem kernel và Docker daemon đã hỗ trợ Seccomp hay chưa.

Kiểm tra cờ cấu hình của kernel:

grep CONFIG_SECCOMP= /boot/config-$(uname -r)
# Kết quả trả về CONFIG_SECCOMP=y là ổn

Xác nhận Docker runtime đã nhận diện Seccomp:

docker info | grep -i seccomp
# Output mong muốn:
# Security Options:
#  seccomp
#  Profile: builtin

Cài đặt thêm strace và auditd trên máy staging để phục vụ việc trích xuất và debug syscall:

sudo apt-get update && sudo apt-get install -y strace jq auditd

Các bước thực hiện

Cấu trúc một Seccomp profile của Docker sử dụng format JSON chuẩn OCI. Ba trường bạn cần chú ý nhất gồm: defaultAction, architectures và mảng syscalls.

Bước 1: Lấy danh sách syscall thực tế của ứng dụng

Đừng ngồi đoán ứng dụng cần gọi những gì. Hãy để strace thống kê chính xác hoạt động của app trong môi trường test:

# Chạy thử container và gom thống kê syscall
docker run --rm --cap-add SYS_PTRACE -v $(pwd):/app -w /app python:3.11-slim \
  strace -c -f python -c "print('Hello from isolated container')"

Kết quả trả về sẽ là bảng tổng hợp các lệnh gọi kèm số lần thực thi (như read, write, mmap, execve, brk). Để an toàn, hãy chạy kèm bộ test suite hoặc chạy load test tự động. Bước này giúp gom đủ các syscall phát sinh khi app xử lý tác vụ nặng.

Bước 2: Tạo file Seccomp Profile dạng Whitelist

Tạo file custom-seccomp.json. Điểm mấu chốt nằm ở defaultAction: gán giá trị SCMP_ACT_ERRNO để từ chối mọi syscall không nằm trong danh sách khai báo:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_AARCH64"
  ],
  "syscalls": [
    {
      "names": [
        "accept4",
        "arch_prctl",
        "brk",
        "clock_gettime",
        "clone",
        "close",
        "connect",
        "epoll_create1",
        "epoll_ctl",
        "epoll_pwait",
        "execve",
        "exit",
        "exit_group",
        "fcntl",
        "fstat",
        "futex",
        "getcwd",
        "getdents64",
        "getpid",
        "getrandom",
        "madvise",
        "mmap",
        "mprotect",
        "munmap",
        "nanosleep",
        "newfstatat",
        "openat",
        "poll",
        "read",
        "readlinkat",
        "restart_syscall",
        "rt_sigaction",
        "rt_sigprocmask",
        "rt_sigreturn",
        "sched_yield",
        "set_robust_list",
        "set_tid_address",
        "sigaltstack",
        "statx",
        "write"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Bước 3: Gắn profile khi chạy container

Với CLI thông thường, bạn dùng cờ --security-opt:

docker run --rm -it \
  --security-opt seccomp=/path/to/custom-seccomp.json \
  alpine:latest sh

Với Docker Compose trong production stack, hãy khai báo vào block của service:

services:
  api:
    image: my-secure-api:1.0
    security_opt:
      - seccomp:/etc/docker/seccomp/custom-seccomp.json
    ports:
      - "8080:8080"

Kiểm tra và Giám sát vi phạm

Sau khi siết chặt policy, bạn cần kiểm chứng xem cơ chế bảo vệ có thực sự hoạt động như kỳ vọng hay không.

1. Test chủ động bằng hành vi trái phép

Thử gọi một syscall không có trong danh sách trắng, ví dụ lệnh đổi hostname (gọi sethostname):

docker run --rm \
  --security-opt seccomp=/path/to/custom-seccomp.json \
  alpine:latest hostname hacker-box

# Kết quả mong đợi:
# hostname: sethostname: Operation not permitted

Lỗi Operation not permitted xuất hiện ngay lập tức. Kernel đã ngắt tiến trình trước khi thao tác kịp ghi vào bộ nhớ hệ thống.

2. Đọc log vi phạm bằng Auditd

Nếu container đột ngột thoát bất thường, rất có thể app vừa gọi một syscall chưa được cấp phép. Bạn có thể tra cứu nhanh qua audit log:

sudo ausearch -m SECCOMP -ts recent

Hoặc lọc trực tiếp từ file audit:

sudo grep "type=SECCOMP" /var/log/audit/audit.log | tail -n 5

Trong log, hãy để ý trường syscall=170. Để biết mã 170 là hàm nào, chạy lệnh sau:

ausyscall x86_64 170
# Output: sethostname

Mẹo nhỏ khi lên staging: thay vì dùng SCMP_ACT_ERRNO khiến app sập ngay khi thiếu quyền, bạn hãy đổi sang "defaultAction": "SCMP_ACT_LOG" (hỗ trợ từ Linux 4.14 trở lên). Ở chế độ này, app vẫn chạy trơn tru, còn mọi syscall vi phạm đều được ghi nhận vào audit log. Bạn chỉ việc theo dõi log trong 2 đến 3 ngày, bổ sung các syscall còn thiếu rồi mới siết sang SCMP_ACT_ERRNO khi lên production.

Share: