Phần 1 để lại một câu đáng lo về bảo mật: Docker mặc định không bật user namespace, nên uid 0 trong container là uid 0 thật trên host. Nghe như một container root là root đầy đủ của máy. Phần này đo điều ngược lại và tinh tế hơn: Linux đã tách quyền của root thành hơn bốn mươi mảnh nhỏ gọi là capability, và Docker chỉ cấp cho root trong container một phần nhỏ trong số đó. Nói cách khác, "root trong container" là một phiên bản root đã bị tước phần lớn quyền nguy hiểm ngay từ mặc định. Bài này đo chính xác root đó làm được gì, bị chặn gì, và cách --cap-add cùng --cap-drop thay đổi ranh giới quyền, kiểm chứng tất cả bằng bitmask CapEff và exit code của syscall thật.
Root bị chia nhỏ thành ~40 capability

Hình 1: Linux chia root thành hơn 40 capability, Docker cấp 14, và cách đo bằng CapEff bitmask cùng exit code.
Trong Unix cổ điển, một tiến trình hoặc là root với toàn quyền, hoặc là user thường gần như không có quyền đặc biệt nào. Linux phá vỡ nhị phân đó bằng capability: hơn bốn mươi mảnh quyền riêng biệt mà trước đây gộp chung dưới root. CAP_CHOWN cho đổi chủ file, CAP_NET_BIND_SERVICE cho nghe cổng dưới 1024, CAP_NET_ADMIN cho cấu hình mạng, CAP_SYS_TIME cho đổi đồng hồ hệ thống, CAP_SYS_ADMIN cho mount và nhiều thao tác quản trị khác, và cứ thế. Một tiến trình root có thể giữ tất cả, hoặc chỉ một tập con. Điều quan trọng với container: Docker mặc định chỉ cấp cho root trong container 14 capability, bỏ khoảng 26 cái, trong đó có những cái nguy hiểm nhất như SYS_ADMIN, SYS_TIME, NET_ADMIN, SYS_MODULE, SYS_PTRACE. Vì vậy root trong container làm được một số việc như đổi chủ file hay bind cổng thấp, nhưng không đổi được giờ host, không mount được, không sửa được mạng, không nạp được kernel module.
Ba cờ điều khiển tập này: --cap-add thêm một capability cụ thể, --cap-drop bỏ một cái, và --cap-drop ALL bỏ sạch để rồi --cap-add lại đúng những cái cần, mô hình tối thiểu được khuyến nghị. Để đo cho chắc, bài này đọc CapEff trong /proc/self/status, một bitmask hex mà mỗi bit là một capability, nên so hai giá trị trước và sau là bằng chứng trực tiếp một capability được thêm hay bỏ, không phải phỏng đoán. Và test từng thao tác bằng exit code, tức kiểm $? bằng 0 hay khác 0, chắc hơn nhiều so với grep chữ lỗi, vì chữ lỗi mỗi công cụ viết một kiểu và dễ bắt nhầm. Cách đo: giải mã CapEff mặc định bằng capsh để đếm 14 capability; chạy các lệnh chown, date -s, ip addr add, mount, tạo raw socket, mknod và xem kết quả; rồi --cap-add, --cap-drop và xem CapEff cùng kết quả đổi ra sao.
Đo thật: 14 trong 40, và CapEff đổi đúng bit

Hình 2: Tập 14 capability mặc định, root làm được gì bị chặn gì, và CapEff đổi đúng bit khi cap-add hay cap-drop.
| phép đo | kết quả |
|---|---|
| CapEff mặc định | a80425fb, 14 capability |
| chown / mknod / raw socket / bind cổng thấp | OK (có trong tập) |
| date -s / mount tmpfs / ip addr add | CHẶN (thiếu SYS_TIME, SYS_ADMIN, NET_ADMIN) |
| --cap-add NET_ADMIN → CapEff | a80435fb (+bit 12), ip addr add OK |
| --cap-add SYS_TIME → date -s | OK |
| --cap-drop CHOWN → chown | CHẶN |
| --cap-drop ALL → CapEff | 0000000000000000, chặn tất cả |
| --cap-drop ALL --cap-add NET_ADMIN → CapEff | 0000000000001000 (chỉ bit 12) |
Tập mặc định là 14 capability, không phải toàn quyền. Giải mã CapEff mặc định a80425fb cho đúng 14 capability: chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap. Đây là những quyền đủ cho một ứng dụng thông thường chạy: đổi chủ file khi cài đặt, hạ quyền từ root xuống user thường, nghe cổng 80 hay 443. Nhưng khoảng 26 capability khác bị bỏ, gồm những cái cho phép can thiệp sâu vào host. Con số 14 trên hơn 40 là điểm cốt lõi: ngay cả khi không bật user namespace, root trong container đã bị Docker thu hẹp đáng kể quyền từ mặc định, nên nó không phải root đầy đủ theo nghĩa cổ điển.
Root mặc định làm được việc file và mạng cơ bản, nhưng không chạm được host. Test bằng exit code cho bức tranh rõ ràng. Làm được: chown đổi chủ file vì có CAP_CHOWN; tạo device node bằng mknod vì có CAP_MKNOD; tạo raw socket, thứ ping cần, vì có CAP_NET_RAW; và bind cổng dưới 1024 vì có CAP_NET_BIND_SERVICE. Bị chặn: date -s để đổi giờ hệ thống trả lỗi vì thiếu CAP_SYS_TIME, một điều quan trọng vì nó nghĩa là container không thể làm lệch đồng hồ của host, thứ sẽ phá mô hình an toàn TLS như sê-ri trước đã bàn; mount một tmpfs bị chặn vì thiếu CAP_SYS_ADMIN; và ip addr add để sửa cấu hình mạng trả RTNETLINK answers: Operation not permitted vì thiếu CAP_NET_ADMIN. Ba thao tác bị chặn này là ba hướng tấn công phổ biến mà Docker đóng sẵn: không cho container đổi giờ, không cho mount tùy tiện, không cho cấu hình lại mạng của host.
CapEff đổi đúng bit khi cap-add hay cap-drop, kiểm chứng được bằng số hex. Thêm --cap-add NET_ADMIN làm CapEff chuyển từ a80425fb thành a80435fb; khác biệt đúng một bit, bit 12 tương ứng 0x1000, và với capability đó, ip addr add chạy được. Thêm --cap-add SYS_TIME bật bit 25 và date -s chạy được. Ở chiều ngược lại, --cap-drop CHOWN bỏ bit 0 và chown bị chặn. Mạnh nhất là --cap-drop ALL: CapEff về 0000000000000000, không còn capability nào, và chown cùng mọi thao tác đặc quyền đều bị chặn, dù tiến trình vẫn là uid 0. Đây là bằng chứng rằng uid và capability là hai trục độc lập: một tiến trình có thể là root mà không có quyền đặc biệt nào. Kết hợp --cap-drop ALL --cap-add NET_ADMIN cho CapEff đúng 0000000000001000, chỉ một bit NET_ADMIN và không gì khác, mô hình tối thiểu: cấp đúng một quyền cần thiết và không hơn. Và --cap-drop NET_RAW khiến tạo raw socket ném PermissionError, minh họa rằng bỏ một capability cụ thể chặn đúng lớp thao tác của nó.
Đánh đổi cần cân nhắc
Dùng --cap-drop ALL rồi add đúng cái cần, đây là biện pháp giảm quyền rẻ nhất. Tập mặc định 14 capability là hợp lý cho nhiều ứng dụng, nhưng phần lớn ứng dụng backend không cần cả 14. Một web service thường chỉ cần net_bind_service nếu nghe cổng thấp, hoặc không cần gì nếu nghe cổng cao; nó không cần mknod, không cần sys_chroot, không cần chown nếu image đã đặt quyền đúng. Bắt đầu từ --cap-drop ALL và thêm lại đúng những cái đo được là cần, thường chỉ một hai cái, thu hẹp bề mặt tấn công đáng kể với chi phí gần bằng không. Nhiều framework triển khai và Kubernetes securityContext hỗ trợ cấu hình này; đây là một trong những việc đơn giản nhất có hiệu quả bảo mật rõ ràng.
Capability không thay thế user namespace, mà bổ sung. Giảm capability hạn chế cái root trong container làm được, nhưng nếu một lỗ hổng cho phép escalate, hoặc nếu ai đó --cap-add quá tay, root vẫn là uid 0 thật. User namespace, bàn ở phần 1, ánh xạ root container sang uid không đặc quyền trên host, một lớp phòng thủ khác. Hai cơ chế bổ sung nhau: capability siết quyền trong khi chạy bình thường, user namespace giới hạn thiệt hại khi thoát container. Phòng thủ sâu dùng cả hai cộng với seccomp, chủ đề phần sau.
Phạm vi đo. CapEff đọc được là tập effective; còn có permitted, inheritable, bounding, ambient, mỗi cái một vai trò trong mô hình đầy đủ, nhưng effective là cái quyết định syscall nào chạy ngay. Chưa đo tương tác với file capability trên binary, chưa đo ambient capability dùng khi hạ quyền mà vẫn giữ một số khả năng, và chưa đo seccomp, lớp lọc syscall độc lập có thể chặn ngay cả thao tác mà capability cho phép, chủ đề của phần 8.
Ba ý mang về
- Root trong container không phải root đầy đủ: Docker cấp 14 trong hơn 40 capability. chown, mknod, raw socket, bind cổng thấp chạy được; date -s, mount, ip addr add bị chặn vì thiếu SYS_TIME, SYS_ADMIN, NET_ADMIN.
- CapEff là bitmask kiểm chứng được.
--cap-addvà--cap-dropđổi đúng bit; so hai số hex là bằng chứng, không phải phỏng đoán. uid và capability độc lập:--cap-drop ALLcho root không quyền. --cap-drop ALLrồi--cap-addđúng cái cần là cách giảm quyền rẻ và hiệu quả nhất. Phần lớn ứng dụng cần một hai capability; thu hẹp bề mặt tấn công với chi phí gần bằng không.
Nguồn
- Linux
capabilities(7)— danh sách đầy đủ hơn 40 capability và ngữ nghĩa: https://man7.org/linux/man-pages/man7/capabilities.7.html - Docker — Runtime privilege and Linux capabilities (
--cap-add,--cap-drop): https://docs.docker.com/engine/containers/run/#runtime-privilege-and-linux-capabilities - Docker — danh sách 14 capability mặc định (seccomp và default caps): https://docs.docker.com/engine/security/#linux-kernel-capabilities
- Linux
proc(5)—/proc/PID/status, trường CapEff, CapBnd: https://man7.org/linux/man-pages/man5/proc.5.html capsh(1)— giải mã và kiểm tra capability: https://man7.org/linux/man-pages/man1/capsh.1.html- Kubernetes — securityContext và
capabilities: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
Phần sau đo lớp bảo vệ bổ sung cho capability: seccomp, bộ lọc chặn syscall ở tầng thấp hơn. Xem profile seccomp mặc định của Docker chặn những syscall nào, đo một syscall bị trả về EPERM dù tiến trình có quyền, và chi phí của việc lọc syscall qua strace.