Triển khai Rootless Docker trên Linux: Chạy Container Không Cần Quyền Root để Bảo Vệ Server

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Chuyện xảy ra khi container thoát ra ngoài

Một buổi sáng thức dậy thấy server chạy ngon lành, nhưng bên trong có ai đó đang khai thác tài nguyên để đào tiền ảo. Điều tra kỹ hơn, phát hiện kẻ tấn công đã leo thang đặc quyền từ bên trong một container Docker — rồi chiếm toàn bộ máy chủ chỉ vì Docker daemon đang chạy với quyền root.

Chuyện đó không phải hư cấu. Mình thấy nó xảy ra thực tế với nhiều server chạy Docker theo cài đặt mặc định — phần đáng lo là phần lớn sysadmin không hay biết nguy cơ này đang nằm đó.

Tại sao Docker mặc định lại nguy hiểm đến vậy?

Khi cài Docker theo hướng dẫn chính thức, dockerd (Docker daemon) chạy với quyền root và lắng nghe trên socket /var/run/docker.sock. Bất kỳ tiến trình nào kết nối được vào socket này đều có thể làm mọi thứ: mount thư mục hệ thống vào container, chạy container privileged, thậm chí đọc ghi file tùy ý trên host system.

Chuỗi tấn công trông thường là thế này:

  1. Hacker khai thác lỗ hổng trong ứng dụng đang chạy trong container
  2. Từ trong container, truy cập socket Docker nếu nó được bind-mount vào
  3. Dùng Docker API để tạo container mới, mount / của host vào đó
  4. Chroot vào host filesystem — toàn bộ máy chủ bị kiểm soát

Nhiều người nghĩ: “Mình không bind-mount socket vào container thì an toàn rồi.” Ít rủi ro hơn, đúng. Nhưng vẫn còn vector khác: container escape, runc vulnerabilities (CVE-2019-5736 từng cho phép thoát container bằng cách chạy một binary đặc biệt), namespace breakout. Khi container thoát ra, nó rơi vào namespace của root trên host — game over.

Ba cách thường thấy để xử lý — và vì sao chúng chưa đủ

Cách 1: Thêm user vào group docker

sudo usermod -aG docker youruser

Nghe có vẻ ổn, nhưng thực ra thành viên group docker có thể làm mọi thứ Docker daemon làm — tức là tương đương root trên host. Docker tài liệu chính thức gọi thẳng đây là “docker group grants privileges equivalent to the root user”. Thêm developer vào group docker để họ tiện làm việc thực chất là cho họ quyền root.

Cách 2: Dùng sudo với whitelist lệnh

# /etc/sudoers.d/docker-restricted
developer ALL=(root) NOPASSWD: /usr/bin/docker run *

Phức tạp để duy trì, dễ bị bypass qua argument injection, và vẫn không giải quyết gốc rễ vì daemon vẫn chạy root.

Cách 3: Dùng AppArmor/SELinux để giới hạn container

Hữu ích, nhưng là lớp bổ sung — không thay đổi thực tế daemon chạy root. Khi daemon bị compromise, profile bảo vệ container chứ không bảo vệ daemon.

Rootless Docker: Cắt đứt đặc quyền từ gốc

Từ Docker 20.10, Rootless mode đã là tính năng stable — cho phép chạy cả daemon lẫn container trong user namespace thường, không có quyền root ở bất kỳ bước nào. Dù kẻ tấn công thoát được khỏi container, chúng chỉ có quyền của user đó trên host — không leo lên được root hệ thống.

Yêu cầu trước khi cài

  • Linux kernel 5.11+ (hoặc 4.18+ với cấu hình bổ sung)
  • Package uidmap đã cài (newuidmap, newgidmap)
  • User có entry trong /etc/subuid và /etc/subgid
  • Không cần sudo để chạy — đó là điểm mấu chốt

Cài đặt trên Ubuntu/Debian

# Cài uidmap
sudo apt-get install -y uidmap

# Kiểm tra subuid/subgid đã có chưa
grep $(whoami) /etc/subuid /etc/subgid

# Nếu chưa có entry, thêm vào:
echo "$(whoami):100000:65536" | sudo tee -a /etc/subuid
echo "$(whoami):100000:65536" | sudo tee -a /etc/subgid

# Chạy script cài đặt rootless Docker
dockerd-rootless-setuptool.sh install
# Hoặc dùng convenience script:
curl -fsSL https://get.docker.com/rootless | sh

Thiết lập environment variables

# Thêm vào ~/.bashrc hoặc ~/.zshrc
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

source ~/.bashrc

Khởi động daemon với systemd user

# Bật và start service ở user level — không cần sudo
systemctl --user enable docker
systemctl --user start docker

# Bật lingering để daemon chạy ngay cả khi user logout
sudo loginctl enable-linger $(whoami)

# Kiểm tra trạng thái
systemctl --user status docker

Kiểm tra xem có thực sự rootless không

# Chạy container và xem UID bên trong
docker run --rm alpine id
# Output: uid=0(root) gid=0(root) — trông như root nhưng đây là UID namespace

# Kiểm tra UID thực của process trên host
docker run -d --name test nginx
ps aux | grep nginx | head -3
# PID của nginx sẽ chạy với UID của bạn trên host, KHÔNG phải 0

Điểm thú vị ở đây: bên trong container, process thấy mình là root (uid=0), nhưng ra ngoài host thì nó chạy với UID của bạn — user namespace làm cầu nối dịch UID giữa hai bên. Container tưởng có quyền root, nhưng không đụng được gì trên host system.

Workflow hàng ngày với Rootless Docker

# Chạy web server bình thường
docker run -d -p 8080:80 --name webserver nginx

# Docker Compose hoạt động y chang
docker compose up -d

# Build image
docker build -t myapp:latest .

# Xem logs
docker logs webserver

Phần lớn workflow hàng ngày không đổi — build, run, compose, logs dùng y như cũ.

Những giới hạn cần biết trước

Rootless Docker cũng có trade-off — một số thứ không làm được hoặc cần workaround:

  • Bind port dưới 1024: User thường không bind port <1024. Cần dùng sysctl hoặc map sang port cao rồi dùng reverse proxy.
  • Overlay filesystem: Cần kernel 5.11+ hoặc FUSE-overlayfs. Trên kernel cũ dùng VFS driver — chậm hơn 2-3x so với overlay2 ở workload write-heavy.
  • Cgroups v1: Resource limits (memory/CPU) cần cgroups v2. Ubuntu 22.04+ đã dùng v2 mặc định, không cần lo.
  • Network mode host: --net=host không dùng host network namespace thực sự — container chỉ dùng được user network namespace của bạn.
# Workaround cho port 80/443: cho phép user bind port thấp
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-rootless.conf
sudo sysctl -p /etc/sysctl.d/99-rootless.conf

# Hoặc giữ port cao và dùng Caddy/nginx làm reverse proxy
docker run -d -p 8080:80 nginx
# Rồi Caddy/nginx trên host proxy :80 → localhost:8080

Kết hợp với bảo mật tổng thể

Rootless Docker bịt được một lỗ hổng lớn, nhưng server security không dừng ở đó. Song song với nó, mình hay rà lại credential — đảm bảo mật khẩu đủ mạnh, không tái sử dụng. Mình dùng password generator tại toolcraft.app/vi/tools/security/password-generator để tạo mật khẩu cho server — 100% chạy trên trình duyệt, không lo lộ qua mạng. Kết hợp với SSH key-based authentication là đủ vững cho phần authentication.

Với server mới, mình coi Rootless Docker là config mặc định chứ không phải tuỳ chọn — setup mất thêm khoảng 10 phút nhưng bịt được một lớp rủi ro mà cài mặc định bỏ qua. Với team dev trên server chia sẻ thì càng hữu ích hơn: mỗi người có daemon riêng, container của người này không ảnh hưởng đến người kia. Và không ai có đường tắt lên root host — dù vô tình hay cố ý.

Share: