Container đang chạy thẳng trên kernel host — và đó là vấn đề
Nếu bạn đang dùng Docker mà chưa từng nghĩ đến việc một container có thể “thoát ra” và tấn công host, thì bài này dành cho bạn.
Bản chất của container Linux là chia sẻ kernel với host. Không giống VM có hypervisor ngăn cách hoàn toàn, container chỉ dùng namespace và cgroup để cô lập. Nghĩa là: attacker khai thác được một syscall nguy hiểm bên trong container là có thể leo thang đặc quyền ra ngoài host.
Kiểu tấn công này gọi là Container Escape. Các CVE nổi tiếng như runc CVE-2019-5736 hay Dirty Pipe CVE-2022-0847 đều theo cơ chế đó — exploit kernel từ bên trong container.
Mình chạy homelab với Proxmox VE quản lý 12 VM và container — đây là playground để test mọi thứ trước khi đưa lên production. Sau một lần đọc post-mortem về container escape trên cluster Kubernetes production của công ty khác, mình bắt đầu hardening nghiêm túc hơn thay vì chỉ dùng --read-only hay drop capabilities.
Giải pháp mình tìm được: gVisor.
gVisor là gì và tại sao nó khác với cách cô lập thông thường
gVisor là một sandbox runtime cho container, do Google phát triển và open-source. Thay vì để container gọi syscall thẳng vào kernel host, gVisor đặt một tầng trung gian gọi là Sentry ở giữa.
Sentry là một kernel viết bằng Go, chạy trong user space. Khi app trong container gọi open(), read(), hay execve(), Sentry bắt những syscall đó lại, xử lý trong sandbox. Chỉ những gì thực sự cần thiết mới được gọi xuống kernel host — qua một tập syscall rất hạn chế.
Hãy hình dung như thế này:
- Docker thông thường: App → syscall → Linux kernel host (trực tiếp)
- gVisor: App → syscall → Sentry (kernel giả trong user space) → một vài syscall an toàn → Linux kernel host
Kết quả: attack surface của kernel host giảm hẳn. Dù attacker khai thác được lỗ hổng trong app container, họ cũng chỉ thoát ra được Sentry — không phải host kernel thật.
gVisor hỗ trợ hai platform:
- ptrace: Chạy được ở mọi nơi, nhưng chậm hơn
- KVM: Cần CPU hỗ trợ virtualization, nhanh hơn đáng kể (mình dùng cái này trong homelab)
Cài đặt gVisor trên Ubuntu/Debian
Bước 1: Thêm repository và cài gVisor
# Thêm GPG key và repo chính thức của gVisor
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" \
| sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc
Sau khi cài xong, kiểm tra version:
runsc --version
# runsc version release-20240401.0
Bước 2: Cấu hình Docker sử dụng gVisor runtime
Mở hoặc tạo file /etc/docker/daemon.json:
sudo nano /etc/docker/daemon.json
Thêm nội dung sau (nếu file đã có nội dung, merge vào — đừng ghi đè):
{
"runtimes": {
"runsc": {
"path": "/usr/bin/runsc"
}
}
}
Restart Docker để áp dụng:
sudo systemctl restart docker
Xác nhận runtime đã được nhận diện:
docker info | grep -i runtime
# Runtimes: io.containerd.runc.v2 runsc runc
Bước 3: Chạy container với gVisor
Chỉ cần thêm flag --runtime=runsc vào lệnh Docker bình thường:
# Container thông thường (dùng runc, kernel host)
docker run --rm ubuntu uname -r
# Container với gVisor (dùng runsc, kernel Sentry)
docker run --rm --runtime=runsc ubuntu uname -r
Thú vị ở chỗ: uname -r trong container gVisor trả về kernel version khác hoàn toàn so với host — đó là kernel của Sentry, không phải kernel thật của máy bạn.
# Ví dụ output
# Host kernel: 6.8.0-87-generic
# gVisor kernel: 4.4.0
# (Sentry giả lập kernel version cũ để tương thích)
Bước 4: Dùng gVisor với Docker Compose
Trong docker-compose.yml, thêm runtime vào service cần bảo vệ:
version: '3.8'
services:
webapp:
image: nginx:alpine
runtime: runsc
ports:
- "8080:80"
database:
image: postgres:15
runtime: runsc
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Chạy bình thường:
docker compose up -d
Bước 5: Đặt gVisor làm runtime mặc định (tùy chọn)
Muốn mọi container đều qua gVisor trừ khi chỉ định khác? Cập nhật daemon.json:
{
"default-runtime": "runsc",
"runtimes": {
"runsc": {
"path": "/usr/bin/runsc"
},
"runc": {
"path": "/usr/bin/runc"
}
}
}
Khi đó nếu muốn dùng runtime gốc, chỉ định --runtime=runc.
Kiểm tra gVisor có thực sự cô lập không
Phần này mình hay test đầu tiên sau khi setup xong — vừa để confirm sandbox hoạt động, vừa để thấy trực quan sự khác biệt:
# Xem /proc/self/status trong container gVisor
docker run --rm --runtime=runsc ubuntu cat /proc/self/status
# CapEff sẽ khác với container thông thường
# Thử đọc thông tin kernel
docker run --rm --runtime=runsc ubuntu cat /proc/version
# Linux version 4.4.0 (#1 SMP ...) — đây là Sentry, không phải kernel thật
# Kiểm tra hostname của sandbox
docker run --rm --runtime=runsc ubuntu hostname
# Mỗi container có sandbox riêng biệt
Thêm một test về syscall restriction — thử gọi ptrace (syscall hay bị lạm dụng trong các kỹ thuật exploit):
docker run --rm --runtime=runsc ubuntu bash -c \
'strace -e trace=ptrace ls 2>&1 | head -5'
# Sẽ thấy lỗi hoặc syscall bị chặn — đây là hành vi mong muốn
Lưu ý về hiệu năng và giới hạn
gVisor không phải silver bullet. Trước khi deploy production, có mấy trade-off cần nắm rõ:
- Overhead syscall: Mỗi syscall phải đi qua Sentry nên latency cao hơn runc thuần. Benchmark thực tế cho thấy I/O-heavy workload (database ghi liên tục, file processing) có thể chậm hơn 20–40%, còn CPU-bound workload (nén, mã hóa) thường chỉ thêm 2–5%.
- Compatibility: Sentry không implement 100% Linux syscall. App dùng syscall ít phổ biến hoặc kernel feature mới có thể không chạy được — test kỹ trước khi đưa lên production.
- Volume mount: I/O với bind mount chậm hơn container thường. Nếu có thể, dùng named volume hoặc tmpfs cho thư mục cần throughput cao.
- Không phải VM: gVisor vẫn nhẹ hơn VM nhiều, start time tính bằng millisecond. Nhưng nếu cần cô lập hoàn toàn ở mức hardware, VM với KVM mới là đúng tool.
Trong homelab, mình dùng gVisor cho container chạy code untrusted (CI runner nhận code từ bên ngoài) và service public-facing. Database nội bộ vẫn chạy runc bình thường vì tần suất write cao — overhead 20–40% ở đó không đáng đổi.
Kết luận
Sau vài tháng dùng gVisor trong homelab, mình thấy đây là lớp bảo mật đáng add vào stack nếu bạn đang lo về container escape. Không cần đụng vào Dockerfile hay quy trình build — chỉ --runtime=runsc là container đã chạy trong sandbox với kernel riêng.
Recap nhanh:
- gVisor intercept syscall qua Sentry — kernel host không bị expose trực tiếp
- Cài đặt chỉ mất 5 phút, không cần thay đổi image hay Dockerfile
- Phù hợp nhất cho workload untrusted, service public-facing, môi trường CI/CD
- Có trade-off về hiệu năng — I/O-heavy app nên benchmark trước, không deploy mù
Nếu bạn đang dùng Kubernetes, gVisor cũng hỗ trợ qua RuntimeClass — apply per-pod mà không ảnh hưởng toàn cluster. Đó là hướng tiếp theo nếu infra của bạn đã lên đến tầng orchestration.
