Docker container chia sẻ chung nhân Linux với máy chủ host. Nếu cấu hình mặc định không được thắt chặt, tiến trình chạy dưới quyền root (UID 0) bên trong container cũng chính là UID 0 trên hệ điều hành của VPS. Khi ứng dụng web bị khai thác lỗ hổng thực thi mã từ xa, sự kết hợp giữa quyền root mặc định và các cấu hình lỏng lẻo như gắn cờ --privileged hoặc chia sẻ socket /var/run/docker.sock sẽ tạo điều kiện để kẻ tấn công thoát khỏi rào chắn namespace và kiểm soát toàn bộ máy chủ. Đây cũng là lý do vì sao trong chiến lược tối ưu hóa và làm sạch Docker trên VPS, việc quản lý vòng đời container luôn cần đi đôi với kỷ luật phân quyền chặt chẽ ngay từ đầu.
Việc thiết lập cơ chế phòng thủ nhiều lớp cho Docker—từ chỉ thị USER non-root, thu hồi Linux Capabilities đến ánh xạ không gian định danh người dùng (userns-remap)—giúp cô lập triệt để tiến trình container khỏi tài nguyên cốt lõi của máy chủ.
1. Bản chất phân quyền trong Docker và rủi ro UID 0 mặc định
Khác với máy ảo phần cứng (KVM, VMware) sở hữu nhân hệ điều hành và lớp ảo hóa hypervisor tách biệt hoàn toàn, container chỉ là các tiến trình được cô lập bằng các tính năng của nhân Linux: Namespaces (phân vùng tầm nhìn mạng, tiến trình, mount point) và Control Groups (giới hạn CPU, RAM).
Khi bạn tạo một container bằng lệnh:
docker run -d --name web-test nginxTiến trình Nginx bên trong container sẽ chạy với người dùng root (UID 0). Nếu bạn mở một cửa sổ dòng lệnh khác trên máy chủ VPS và kiểm tra tiến trình bằng lệnh của hệ điều hành host:
ps -ef | grep nginxBạn sẽ thấy tiến trình nginx: master process hiển thị trên máy chủ host với cột UID là root (hoặc số 0).
Về mặt kỹ thuật, nhân Linux không phân biệt "root của container" với "root của VPS host". Quyền hạn của tiến trình trong container chỉ bị kiềm chế bởi các rào chắn namespace và danh sách capabilities bị cắt bớt của Docker. Nếu kẻ tấn công tìm được lỗ hổng tràn bộ đệm trong nhân Linux (kernel vulnerability) hoặc cấu hình container bị mở rộng đặc quyền, chúng sẽ có đầy đủ quyền root trên toàn bộ máy chủ VPS.
2. Hai sai lầm cấu hình nghiêm trọng dẫn đến chiếm quyền VPS
Trong quá trình triển khai thực tế, nhiều hệ thống bị chiếm quyền điều khiển không phải do lỗi của bản thân Docker, mà do sử dụng các tùy chọn cấu hình nguy hiểm nhằm giải quyết nhanh các vấn đề về phân quyền hoặc tiện ích quản trị.
Cờ --privileged: Vô hiệu hóa toàn bộ rào chắn bảo vệ
Cờ --privileged thường được sử dụng khi người vận hành muốn chạy Docker bên trong Docker (DinD), can thiệp vào card mạng ảo hoặc mount phân vùng ổ đĩa từ bên trong container.
Tuy nhiên, khi gắn cờ này:
- Docker cấp toàn bộ 41 Linux Capabilities cho container, bao gồm
CAP_SYS_ADMINvàCAP_SYS_RAWIO. - Bỏ qua toàn bộ bộ lọc bảo mật Seccomp và AppArmor mặc định.
- Cho phép container nhìn thấy và truy cập trực tiếp vào toàn bộ các tệp tin thiết bị trong thư mục
/devcủa máy chủ host (như/dev/vda,/dev/sda).
Với cờ --privileged, nếu ứng dụng bên trong container bị chèn webshell, kẻ tấn công chỉ cần chạy một lệnh mount đơn giản là có thể đọc và ghi đè toàn bộ hệ điều hành của VPS:
# Lệnh chạy từ bên trong container có gắn cờ --privileged
mkdir /mnt/host
mount /dev/vda1 /mnt/host
# Ghi thêm khóa SSH của kẻ tấn công thẳng vào VPS host
echo "ssh-ed25519 AAAAC3NzaC... attacker@c2" >> /mnt/host/root/.ssh/authorized_keysSau thao tác này, kẻ tấn công đăng nhập thẳng vào VPS qua cổng SSH với quyền root mà không gặp bất kỳ trở ngại nào. Kịch bản này vô hiệu hóa hoàn toàn mọi nỗ lực thiết lập SSH key truy cập VPS an toàn mà bạn đã cấu hình trước đó trên máy chủ.
Mount UNIX Socket /var/run/docker.sock vào container
File /var/run/docker.sock là giao diện IPC (Inter-Process Communication) cho phép giao tiếp trực tiếp với dịch vụ nền Docker Daemon (dockerd). Dịch vụ này luôn chạy dưới quyền root trên host.
Cấu hình mount socket này thường gặp khi cài đặt các công cụ quản lý giao diện như Portainer, reverse proxy tự động phát hiện container như Traefik, hoặc các agent CI/CD (GitLab Runner, Jenkins):
volumes:
- /var/run/docker.sock:/var/run/docker.sockKhi ứng dụng chạy trong container đó bị chiếm quyền, kẻ tấn công không cần khai thác lỗ hổng kernel phức tạp. Chúng chỉ cần cài đặt công cụ dòng lệnh docker hoặc gửi request HTTP thuần vào UNIX socket thông qua lệnh curl để ra lệnh cho Docker daemon trên host khởi tạo một container mới có quyền root:
# Gửi lệnh trực tiếp vào docker.sock từ bên trong container bị chiếm quyền
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{"Image": "alpine", "Cmd": ["chroot", "/host", "sh", "-c", "echo pwned > /tmp/hacked"], "Binds": ["/:/host"]}' \
-X POST http://localhost/v1.41/containers/createBiện pháp thay thế an toàn:
- Tránh chia sẻ socket trực tiếp cho các container công khai ra Internet.
- Nếu bắt buộc phải dùng (như trường hợp của Traefik hoặc Portainer), hãy đặt một proxy trung gian chuyên dụng như
docker-socket-proxy(của Tecativa). Proxy này chỉ cho phép các lệnh đọcGET /containers/jsonvà chặn hoàn toàn các phương thứcPOST,DELETEcó khả năng tạo hoặc xóa container.
3. Cấu hình container với quyền tối thiểu (Least Privilege)
Để bảo vệ máy chủ, quy trình đóng gói và khởi chạy container cần tuân thủ nguyên tắc cấp đặc quyền tối thiểu.
Thiết lập người dùng non-root trong Dockerfile
Không bao giờ để ứng dụng chạy với quyền root mặc định của base image. Hãy chủ động tạo một nhóm và người dùng chuyên dụng ở các dòng cuối của Dockerfile:
FROM node:20-alpine
WORKDIR /app
# Tạo group và user với UID cụ thể không có quyền root
RUN addgroup -g 10001 -S appgroup && \
adduser -u 10001 -S appuser -G appgroup
COPY package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
# Chuyển quyền thực thi sang user không đặc quyền
USER appuser:appgroup
EXPOSE 3000
CMD ["node", "server.js"]Khi khởi chạy container trong môi trường sản xuất, bạn có thể bổ sung cờ --security-opt=no-new-privileges:true để ngăn chặn các tiến trình con bên trong container nhận thêm quyền thông qua các nhị phân có cờ setuid hoặc setgid:
docker run -d \
--security-opt=no-new-privileges:true \
--name web-app \
my-app:1.0Thu hồi Linux Capabilities không cần thiết
Theo mặc định, Docker cấp khoảng 14 capabilities cho container (như CAP_CHOWN, CAP_FOWNER, CAP_KILL, CAP_NET_BIND_SERVICE). Hầu hết các ứng dụng web backend (Node.js, Python FastAPI, Go, PHP-FPM) hoàn toàn không cần đến các quyền này trong suốt quá trình xử lý request.
Cấu hình an toàn nhất là thu hồi toàn bộ capabilities bằng --cap-drop=ALL và chỉ cấp lại đúng capability tối thiểu nếu ứng dụng yêu cầu:
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--name web-app \
my-app:1.0Trong ví dụ trên, --cap-add=NET_BIND_SERVICE chỉ cần thiết nếu tiến trình của bạn cần lắng nghe trực tiếp trên các cổng mạng đặc quyền dưới 1024 (như cổng 80 hoặc 443). Nếu ứng dụng chạy cổng 3000 hoặc 8080, bạn thậm chí không cần cấp thêm bất kỳ capability nào.
Khóa hệ thống tệp chỉ đọc (Read-only Root Filesystem)
Phần lớn mã độc khi xâm nhập vào ứng dụng web thông qua lỗ hổng upload file sẽ cố gắng ghi các đoạn mã webshell hoặc tải tệp thực thi vào các thư mục như /tmp hoặc mã nguồn ứng dụng.
Bạn có thể kích hoạt chế độ chỉ đọc cho toàn bộ hệ thống tệp của container bằng cờ --read-only, kết hợp mount ramdisk tmpfs cho các thư mục bắt buộc phải ghi dữ liệu tạm:
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /run:rw,noexec,nosuid,size=16m \
--name secure-web \
my-app:1.0Với cấu hình này, ngay cả khi kẻ tấn công có quyền thực thi lệnh từ xa (RCE), mọi nỗ lực tải file nhị phân độc hại về container đều sẽ bị hệ điều hành từ chối với lỗi Read-only file system.
4. Triệt tiêu rủi ro UID 0 với User Namespace Remapping (userns-remap)
Trong các môi trường phức tạp mà một số container bên thứ ba bắt buộc phải chạy dưới quyền root bên trong (ví dụ các dịch vụ khởi động cần bind port hoặc quản lý quyền file nội bộ), cơ chế User Namespace Remapping (userns-remap) là giải pháp bảo vệ toàn diện ở cấp độ nhân Linux.
Nguyên lý hoạt động của userns-remap
Tính năng User Namespaces của Linux cho phép ánh xạ một dải UID/GID bên trong container sang một dải UID/GID hoàn toàn khác trên máy chủ host:
- Tiến trình trong container tin rằng nó đang chạy với
UID 0(root), do đó mọi thao tác cấu hình bên trong container vẫn diễn ra bình thường. - Tuy nhiên, nhân Linux trên máy chủ VPS sẽ dịch (remap)
UID 0đó thành một người dùng thông thường không có đặc quyền trên host, ví dụUID 165536.
Nếu kẻ tấn công thoát khỏi rào chắn container, hệ điều hành VPS chỉ nhìn nhận kẻ tấn công là tiến trình của user 165536. Kẻ tấn công không thể đọc file /etc/shadow, không thể chỉnh sửa thư mục /root và không thể can thiệp vào các dịch vụ khác của hệ thống.
Hướng dẫn kích hoạt userns-remap trên VPS
Kiểm tra xem hệ thống Linux của bạn đã cấu hình dải SubUID và SubGID hay chưa:
cat /etc/subuid
cat /etc/subgidNếu chưa có dòng cấu hình cho Docker, thêm vào cuối hai file trên:
# Cấp dải 65536 UID/GID bắt đầu từ 165536 cho người dùng dockremap
dockremap:165536:65536Tiếp theo, chỉnh sửa tệp cấu hình trung tâm của Docker Daemon tại /etc/docker/daemon.json (tạo mới nếu chưa có):
{
"userns-remap": "default"
}Khởi động lại dịch vụ Docker để áp dụng cấu hình:
systemctl restart dockerSau khi kích hoạt, kiểm tra thư mục lưu trữ của Docker tại /var/lib/docker/: bạn sẽ thấy một thư mục mới có định dạng <UID>.<GID> (ví dụ /var/lib/docker/165536.165536/). Toàn bộ dữ liệu image và container từ thời điểm này sẽ được cô lập hoàn toàn dưới quyền của UID phụ đó.
Lưu ý quan trọng khi vận hành userns-remap
- Khả năng tương thích của Images cũ: Khi bật
userns-remap, Docker sẽ quản lý layer lưu trữ ở thư mục mới. Các container và image đã tải trước đó sẽ không hiển thị (chúng vẫn nằm ở thư mục cũ và không bị xóa). Bạn sẽ cần tải lại các image cần thiết. - Quyền sở hữu Volume Mount: Nếu bạn mount một thư mục từ máy chủ host vào container bằng
-v /data/web:/var/www, thư mục/data/webtrên máy chủ host phải được cấp quyền đọc/ghi cho UID remapped (chown -R 165536:165536 /data/web), nếu không ứng dụng trong container sẽ báo lỗi quyền truy cậpPermission denied.
5. Checklist kiểm tra an toàn phân quyền Docker trên VPS
Trước khi đưa một dịch vụ Docker vào vận hành chính thức trên VPS, hãy rà soát cấu hình theo bảng đối chiếu sau:
| Hạng mục kiểm tra | Nguy cơ tiềm ẩn | Cấu hình khuyến nghị |
|---|---|---|
| User trong Container | Chạy root (UID 0) trùng với root của máy chủ host | Khai báo USER non-root trong Dockerfile; gán cờ --security-opt=no-new-privileges:true |
Cờ --privileged | Vô hiệu hóa bảo vệ, truy cập toàn bộ thiết bị /dev của host | Tuyệt đối không dùng trên môi trường production |
Mount /var/run/docker.sock | Chiếm quyền điều khiển Docker daemon để tạo container root | Không mount ra Internet; sử dụng docker-socket-proxy có giới hạn quyền đọc |
| Linux Capabilities | Giữ lại các capabilities quản trị mạng và kernel | Khởi chạy với --cap-drop=ALL, chỉ mở thêm quyền tối thiểu (--cap-add) |
| Hệ thống tệp (Rootfs) | Mã độc tải và thực thi binary độc hại | Bật cờ --read-only, mount thư mục ghi tạm qua tmpfs |
| User Namespace Remap | Lỗ hổng kernel dẫn đến leo thang quyền host | Bật "userns-remap": "default" trong /etc/docker/daemon.json |
Việc áp dụng đồng bộ các nguyên tắc trên giúp biến Docker thành một lớp phòng thủ tin cậy, ngăn chặn nguy cơ một ứng dụng web bị xâm nhập làm ảnh hưởng đến tính an toàn của toàn bộ máy chủ VPS. Trong trường hợp nghi ngờ một container từng bị khai thác hoặc xuất hiện các tiến trình lạ trên host, bạn nên lập tức thực hiện quy trình rà soát mã độc và backdoor trên Linux VPS để kiểm tra toàn diện các socket mạng và cơ chế persistence trước khi đưa hệ thống trở lại hoạt động.



