Khi xây dựng nền tảng cho phép người dùng thực thi mã tùy ý — như hệ thống OJ (Online Judge) hay IDE trực tuyến — việc cách ly mã độc là ưu tiên hàng đầu. Docker không phải "tường đồng vách sắt", mà chỉ là lớp vỏ bọc dựa trên kernel chung của host. Bài viết này trình bày cách triển khai sandbox mã nguồn an toàn bằng Docker thông qua nhiều lớp bảo vệ chồng lấn.
Cơ chế cô lập trong Docker: Hiểu đúng để phòng tránh
Docker sử dụng hai cơ chế chính:
- Namespace: Tạo cảm giác "độc lập" cho tiến trình bên trong container (PID, Mount, Network, IPC, UTS, User).
- Cgroup: Giới hạn tài nguyên (CPU, RAM, số lượng tiến trình...).
Tuy nhiên, đây là "cô lập mềm". Nếu có lỗi kernel hoặc cấu hình sai, mã độc có thể thoát ra host. Do đó, cần áp dụng mô hình defense in depth — nhiều lớp bảo vệ kết hợp.
Lớp 1: Giới hạn tài nguyên nghiêm ngặt
Ngăn fork bomb, vòng lặp vô hạn hoặc tiêu tốn tài nguyên:
docker run \
--memory="512m" \
--memory-swap="512m" \
--cpus="1" \
--pids-limit=50 \
--cpu-shares=512 \
your-image
Ghi chú:
--memory-swap="512m"vô hiệu hóa swap để tránh bypass giới hạn RAM.--pids-limit=50chặn các tấn công dạng:(){ :|:& };:.
Lớp 2: Loại bỏ Capability không cần thiết
Linux phân rã quyền root thành các capability nhỏ. Trong sandbox, gần như không cần capability nào:
docker run --cap-drop=ALL your-image
Nếu cần bind port dưới 1024, mới thêm --cap-add=NET_BIND_SERVICE. Tuyệt đối tránh --privileged.
Lớp 3: Lọc system call với Seccomp
Seccomp hoạt động như tường lửa ở cấp độ kernel. Cấu hình mặc định của Docker vẫn quá rộng cho sandbox. Nên dùng profile tùy chỉnh:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{ "names": ["read", "write", "open", "close", "execve", ...], "action": "SCMP_ACT_ALLOW" }
]
}
Khởi chạy container kèm profile:
docker run --security-opt seccomp=./strict.json your-image
Tip: Dùng strace để thu thập syscall cần thiết trước khi khóa cứng.
Lớp 4: Kiểm soát truy cập file với AppArmor
AppArmor giới hạn thao tác đọc/ghi file, mạng, mount... Ví dụ profile sandbox:
profile code-sandbox {
/usr/** r,
/lib/** r,
/tmp/** rw,
deny network,
deny mount,
deny ptrace,
}
Sử dụng:
docker run --security-opt apparmor=code-sandbox your-image
Lớp 5: Kích hoạt User Namespace
Map UID/GID trong container sang dải không đặc quyền trên host:
# /etc/docker/daemon.json
{ "userns-remap": "default" }
Sau khi khởi động lại Docker, UID 0 trong container sẽ tương ứng với UID ~100000 trên host — giảm thiểu hậu quả nếu xảy ra escape.
Lớp 6: Filesystem chỉ đọc + tmpfs
Không cho phép ghi vào hệ thống tập tin gốc:
docker run \
--read-only \
--tmpfs /tmp:rw,size=100m \
--tmpfs /dev/shm:rw,size=64m \
your-image
Mọi thay đổi chỉ tồn tại trong bộ nhớ và biến mất khi container dừng.
Lớp 7: Cô lập mạng hoàn toàn
Hầu hết sandbox không cần mạng:
docker run --network=none your-image
Nếu bắt buộc phải có mạng, hãy tạo bridge riêng và dùng iptables để whitelist địa chỉ đích.
Lớp 8: Giới hạn thời gian thực thi
Ngăn mã chạy vô hạn:
timeout 10 docker run --rm your-image
Hoặc kiểm soát timeout từ bên trong container bằng subprocess với tham số timeout.
Giải pháp nâng cao: gVisor hoặc Kata Containers
Với yêu cầu bảo mật cực cao (multi-tenant, mã hoàn toàn không tin cậy), nên dùng runtime sandbox:
- gVisor: Cài đặt kernel giả (Sentry) ở userspace, chỉ forward ~20 syscall xuống host. Cài đặt đơn giản, overhead vừa phải.
- Kata Containers: Mỗi container chạy trong VM nhẹ, có kernel riêng — cô lập mạnh hơn nhưng tốn tài nguyên.
Sử dụng gVisor với Docker:
docker run --runtime=runsc your-image
Cấu hình tổng hợp cho production
docker run \
--rm \
--runtime=runsc \
--network=none \
--read-only \
--memory="512m" \
--pids-limit=50 \
--cap-drop=ALL \
--security-opt seccomp=/etc/seccomp/strict.json \
--security-opt apparmor=code-sandbox \
--tmpfs /tmp:rw,size=100m \
--user=1000:1000 \
your-image \
timeout 10 python3 /submission.py
Bài học thực tế
- Không bao giờ mount
/var/run/docker.sockvào container. - Luôn dùng image digest thay vì tag
latest. - Cập nhật kernel thường xuyên — nhiều CVE escape xuất phát từ lỗi kernel.
- Giám sát hành vi bất thường bằng Falco hoặc auditd.