Suốt loạt bài này ta thấy container cô lập bằng namespace và giới hạn bằng cgroup. Nhưng cô lập không phải là bảo mật tuyệt đối — container chung kernel với host, và một cấu hình lỏng lẻo biến một lỗ hổng ứng dụng nhỏ thành quyền kiểm soát cả máy chủ. Điều khiến nhiều người bất ngờ: mặc định, container chạy dưới quyền root. Nếu app trong container bị khai thác, kẻ tấn công có ngay quyền root bên trong container — và nếu tìm được cách thoát cô lập, đó là root trên host. Bài này (phần 12, khép lại loạt Docker) đo thật ba lớp phòng thủ giúp một container bị chiếm không trở thành thảm hoạ.
Mặc định nguy hiểm: container chạy root
docker run alpine id # uid=0(root) gid=0(root)
Đo thật xác nhận: container mặc định là root (uid 0). Nghĩa là app của bạn, và bất cứ ai khai thác được app đó, có toàn quyền trong container — ghi mọi file, cài phần mềm, sửa cấu hình. Đây là "mặc định tiện lợi nhưng không an toàn". Nguyên tắc bảo mật cốt lõi là đặc quyền tối thiểu: cho container đúng những gì nó cần, không hơn.

Hình 1: Container mặc định chạy root (nguy hiểm); ba lớp phòng thủ: --user chạy non-root, --cap-drop ALL bỏ capabilities, --read-only khoá rootfs — phòng thủ theo lớp theo nguyên tắc đặc quyền tối thiểu.
Ba lớp phòng thủ, đo thật

Hình 2: Chạy thật — mặc định uid=0(root); --user 1000 cho uid=1000 và ghi /etc bị Permission denied (root thì ghi OK); --cap-drop ALL khiến root không chown được (Operation not permitted); --read-only cho Read-only file system khi ghi.
- 1. Chạy non-root (
--user):docker run --user 1000:1000chouid=1000. Đo thật: thử ghi/etc/test→Permission denied(trong khi root ghi được). App non-root không sửa được hệ thống, không cài gói, không đọc file bí mật của user khác — giảm mạnh thiệt hại nếu bị khai thác. Trong Dockerfile:RUN adduser -D apprồiUSER app. - 2. Bỏ capabilities (
--cap-drop ALL): root Linux thực chất là tập hợp ~40 capability rời (chown, bind cổng <1024, mount, thay đổi mạng...). Bỏ hết thì ngay cả root cũng mất các quyền đặc quyền. Đo thật: root bình thườngchownđược, nhưng với--cap-drop ALLthìchownbáoOperation not permitted. Cần capability nào thì--cap-addlại đúng cái đó — đặc quyền tối thiểu. - 3. Khoá hệ thống file (
--read-only): cho rootfs chỉ đọc. Đo thật: bình thường ghi/tmp/xOK, với--read-onlythìRead-only file system. Malware không ghi được binary/backdoor vào container. Cần ghi tạm thì thêm--tmpfs /tmp(dùng RAM, mất khi container dừng).
Phòng thủ theo lớp
Không lớp nào là "viên đạn bạc"; sức mạnh đến từ chồng nhiều lớp:
docker run --user 1000 --cap-drop ALL --read-only --tmpfs /tmp \
--security-opt no-new-privileges myapp
Thêm --security-opt no-new-privileges (chặn leo thang qua setuid), --pids-limit (chống fork bomb), và profile seccomp (giới hạn syscall). Mỗi lớp là một rào chắn — kẻ tấn công phải vượt tất cả mới gây hại thật. Đây chính là ý nghĩa của "defense in depth".
Đánh đổi cần cân nhắc
Non-root đôi khi vướng, nhưng gần như luôn giải quyết được. Một số image giả định chạy root (ghi vào đường dẫn hệ thống, bind cổng <1024). Cách xử lý đúng: đổi app ghi vào thư mục nó sở hữu, dùng cổng >1024 rồi để reverse proxy/-p ánh xạ, và chown sẵn thư mục cần ghi trong Dockerfile. Rất hiếm khi thực sự cần root — thường là do image viết ẩu. Đừng bỏ non-root chỉ vì gặp một lỗi quyền; sửa nguyên nhân.
Cô lập container không mạnh bằng VM — biết giới hạn. Mọi biện pháp trên giảm rủi ro, nhưng container vẫn chung kernel host. Với workload chạy code hoàn toàn không tin cậy (nền tảng multi-tenant chạy code của khách), cân nhắc thêm một lớp ảo hoá thật: gVisor (sandbox syscall) hay Kata Containers (container trong micro-VM). Container cứng hoá là đủ cho hầu hết trường hợp, không đủ cho mọi trường hợp.
Quét image và giữ nó nhỏ cũng là bảo mật. Ba lớp trên là bảo mật lúc chạy; còn bản thân image có thể chứa lỗ hổng (gói cũ, CVE). Quét image bằng công cụ (Trivy, Grype), cập nhật base image, và dùng image nhỏ (multi-stage, distroless — bài trước) để giảm bề mặt tấn công. Image không có shell/compiler thì kẻ tấn công cũng ít công cụ để khai thác.
Ba ý mang về
- Container mặc định chạy root — sửa ngay: đo thật
docker run alpine idchouid=0;--user 1000(hoặcUSERtrong Dockerfile) chạy non-root, đo thật ghi/etcbịPermission denied— giảm mạnh thiệt hại nếu app bị khai thác. - Bỏ quyền không cần:
--cap-drop ALLvà--read-only: đo thật--cap-drop ALLkhiến root khôngchownđược,--read-onlykhoá rootfs (Read-only file system) — chặn cả những gì root thường làm được, theo nguyên tắc đặc quyền tối thiểu. - Phòng thủ theo lớp: chồng non-root + cap-drop + read-only + no-new-privileges + seccomp; và nhớ container cô lập yếu hơn VM (workload không tin cậy cần gVisor/Kata), cùng quét image và giữ image nhỏ.
Nguồn
- Docker docs — Docker security: https://docs.docker.com/engine/security/
- OWASP — Docker Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html
- man7.org — capabilities(7): https://man7.org/linux/man-pages/man7/capabilities.7.html
Đến đây khép lại loạt "Docker và container: nhìn từ bên trong" — 12 phần đi từ container không phải máy ảo, qua các namespace (PID, mount, network), cgroups giới hạn tài nguyên, overlayfs và image layer, cách build (Dockerfile cache, build context, multi-stage), mạng và DNS nội bộ, dữ liệu bền (volume/bind mount), tới bảo mật ở bài này. Sợi chỉ xuyên suốt: container không phải phép màu, mà là vài tính năng kernel Linux ghép lại — namespace (cô lập tầm nhìn) + cgroup (giới hạn tài nguyên) + overlayfs (chia sẻ layer). Hiểu lớp bên dưới, bạn không còn "gõ lệnh docker theo trí nhớ" mà suy luận được vì sao mọi thứ hành xử như vậy — và debug, tối ưu, bảo mật container với sự tự tin.