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.

Ảnh chụp đoạn mã nền tối minh hoạ bảo mật container giảm quyền để một container bị chiếm không thành thảm hoạ, mặc định nguy hiểm container chạy root docker run alpine id ra uid 0 root container mặc định là root kẻ khai thác được app có root trong container và root đó bằng root host nếu thoát được cô lập, một chạy non-root --user hoặc USER trong Dockerfile docker run --user 1000 1000 alpine uid 1000 không ghi được etc không cài gói không sửa hệ thống trong Dockerfile RUN adduser app USER app, hai bỏ capabilities --cap-drop ALL docker run --cap-drop ALL alpine root có khoảng 40 capability chown bind cổng dưới 1024 mount bỏ hết ngay cả root cũng không làm được các thao tác đặc quyền cần cái nào thì --cap-add lại đúng cái đó nguyên tắc tối thiểu, ba khoá hệ thống file --read-only docker run --read-only alpine rootfs chỉ đọc malware không ghi được binary backdoor cần ghi tạm thêm --tmpfs tmp RAM mất khi dừng phòng thủ theo lớp non-root cộng cap-drop cộng read-only cộng seccomp

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

Ảnh chụp bảng kết quả chạy thật docker security flags output thật, một mặc định bằng root docker run alpine id ra uid 0 root gid 0 root, hai --user 1000 non-root ghi etc bị chặn id ra uid 1000 gid 1000 ghi etc test ra Permission denied root thì ghi OK root ghi mọi nơi, ba --cap-drop ALL root mất quyền đặc quyền root bình thường chown ra chown OK root cộng --cap-drop ALL chown ra chown Operation not permitted, bốn --read-only khoá rootfs bình thường ghi tmp x ra ghi OK --read-only ghi tmp x ra Read-only file system cần ghi tmp thêm --tmpfs tmp

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:1000 cho uid=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 app rồi USER 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ường chown được, nhưng với --cap-drop ALL thì chown báo Operation not permitted. Cần capability nào thì --cap-add lạ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/x OK, với --read-only thì 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ề

  1. Container mặc định chạy root — sửa ngay: đo thật docker run alpine id cho uid=0; --user 1000 (hoặc USER trong Dockerfile) chạy non-root, đo thật ghi /etc bị Permission denied — giảm mạnh thiệt hại nếu app bị khai thác.
  2. Bỏ quyền không cần: --cap-drop ALL và --read-only: đo thật --cap-drop ALL khiến root không chown được, --read-only khoá 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.
  3. 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

Đế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.