- CVE-2026-31431 Copy Fail cho phép người dùng cục bộ không có đặc quyền lấy được shell
root, và ngay cả trong container rootless của Podman cũng có thể leo thang lên quyền root bên trong container
- Container rootless của Podman kết hợp user namespace, tách biệt UID và Linux capabilities để ánh xạ
root bên trong container thành người dùng không có đặc quyền trên host và hạn chế quyền trên host
- Trong thử nghiệm, người dùng
foo của container rootless non-root có thể trở thành root bên trong container sau khi chạy Copy Fail, nhưng quyền vẫn bị giới hạn trong phạm vi mà người dùng host không có đặc quyền bar được phép, và không thể đọc các tệp thuộc sở hữu root trên host
- Khi áp dụng
--security-opt=no-new-privileges hoặc --cap-drop=all, ngay cả sau khi chạy Copy Fail, shell vẫn giữ nguyên ở foo và capabilities ở trạng thái none, nhờ đó ngăn việc giành ngay shell root và leo thang capability
- Tác động của Copy Fail có thể còn tồn tại vượt quá vòng đời container, nên cần vá kernel và khởi động lại, đồng thời phải áp dụng thêm phòng thủ theo chiều sâu như hệ thống tệp gốc chỉ đọc, giới hạn tài nguyên bằng cgroups, image runtime tối giản và tường lửa
Phạm vi phơi lộ của Copy Fail và container rootless của Podman
- CVE-2026-31431 được công bố ngày 29 tháng 4 tại copy.fail, và khi chạy script Python đã được công khai, người dùng cục bộ không có đặc quyền có thể lấy được shell
root
- Copy Fail cũng có thể bị khai thác bên trong container Linux, và ngay cả trong container rootless của Podman cũng có thể giành được shell
root bên trong container
- Trong thử nghiệm,
root của container bị giới hạn ở phạm vi quyền của người dùng không có đặc quyền bar đã chạy container trên host
- Cách triển khai rootless của Podman kết hợp user namespace, tách biệt UID và Linux capabilities để hạn chế quyền trên host của các tiến trình container
- Copy Fail cho thấy ngay cả container rootless cũng không miễn nhiễm với lỗ hổng, nhưng cấu hình Podman có thể giúp giảm phạm vi tấn công sau khi bị xâm nhập
Cách hoạt động của container rootless
-
Ví dụ cơ bản: người dùng không có đặc quyền bar chạy máy chủ HTTP
- Môi trường ví dụ là người dùng không có đặc quyền
bar với UID 1001 dùng Podman để build image dựa trên ubuntu:latest và chạy python3 -m http.server
- Khi xem bằng
ps trên host, tiến trình python3 chạy dưới quyền sở hữu của người dùng bar
- Podman dùng mô hình fork/exec, nên tiến trình container trở thành tiến trình con cháu của tiến trình
podman run, và có thể tách biệt tiến trình container khỏi host root hoặc người dùng khác bằng cơ chế tách UID thông thường
- Trong cấu hình Docker thông thường, ngay cả khi người dùng không có đặc quyền chạy
docker run, Docker client vẫn giao tiếp với daemon có quyền root, và daemon cuối cùng tạo tiến trình container, nên trên host tiến trình container có thể hiện là root
-
Rootless rootful
- Nếu image container không có chỉ thị
USER rõ ràng hoặc cờ --user, thông thường lệnh trong container sẽ chạy với root bên trong container
- Trong đầu ra
podman top, tiến trình máy chủ HTTP được ánh xạ tới người dùng host 1001, nhưng với tư cách người dùng bên trong container thì nó chạy dưới root
- Cấu hình này là trạng thái rootless rootful: chạy như người dùng không có đặc quyền trên host nhưng là
root bên trong container
-
User namespace
- Container rootless của Podman dùng user namespace để ánh xạ UID/GID khác nhau giữa bên trong và bên ngoài container
- Trong ví dụ,
root với UID 0 bên trong container được ánh xạ tới bar với UID 1001 trên host
- Thiết lập
bar:165536:65536 trong /etc/subuid xác định dải UID có thể được cấp cho các tiến trình namespace của bar
- Trong ví dụ, ngoài UID
1001 của bar, các UID từ 165536 đến 231072 có thể được gán cho các tiến trình của bar
- Nếu chạy
sleep với người dùng www-data bên trong container, thì bên trong nó là www-data nhưng trên host sẽ hiển thị là 165568
- Khi vào user namespace bằng
podman unshare, thư mục home thuộc sở hữu bar:bar trên host sẽ hiện là root:root bên trong namespace
- Docker cũng hỗ trợ user namespace, nhưng cần cấu hình riêng và chỉ cho phép một user namespace duy nhất, trong khi Podman chạy các container rootless của từng người dùng UNIX trong user namespace tương ứng của người đó
-
Các thao tác đặc quyền và Linux capabilities
- Podman dùng Linux capabilities để cấp các quyền root tinh vi cho tiến trình container
- Trong lúc build image, các tác vụ như
apt install có thể thực hiện được nhờ tổ hợp capability như chown, dac_override, fowner, setgid, setuid, net_bind_service, sys_chroot
- Nếu loại bỏ toàn bộ capability bằng
podman build --cap-drop=all, apt sẽ thất bại ở các thao tác như setgroups, setegid, seteuid, chown, khiến việc build image thất bại
- Cũng có thể chỉ thêm những capability cần thiết; trong ví dụ,
CAP_SETUID,CAP_SETGID,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER được thêm vào để cài đặt gói
- Ở trạng thái chạy mặc định, máy chủ HTTP chạy dưới
root bên trong container và có nhiều effective capabilities như CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT
- Máy chủ HTTP không cần các quyền này, nên có thể loại bỏ toàn bộ bằng
podman run --cap-drop=all; khi đó podman top sẽ hiển thị effective capabilities là none
-
Rootless non-root
- Để chạy máy chủ HTTP bằng người dùng không có đặc quyền ngay cả bên trong container, có thể dùng người dùng sẵn có trong
/etc/passwd, ví dụ www-data, hoặc tạo người dùng chuyên dụng trong lúc build image
- Trong ví dụ, một người dùng và nhóm
foo với UID 1002 được tạo, cấp quyền đọc cho /var/www/html, rồi thiết lập USER foo:foo
- Khi chạy image này với
--cap-drop=all, tiến trình sẽ ở trạng thái người dùng foo bên trong container, UID host 166537, và effective capabilities là none
- Tiến trình container nên chạy với mức đặc quyền tối thiểu cần thiết; ví dụ nếu
foo cần bind vào cổng đặc quyền 80 thì phải thêm --cap-add=CAP_NET_BIND_SERVICE
- Có thể phân loại cách chạy container thành bốn kiểu
- người dùng host
root + container root: root rootful
- người dùng host
root + người dùng không có đặc quyền trong container: root non-root
- người dùng host không có đặc quyền + container
root: rootless rootful
- người dùng host không có đặc quyền + người dùng không có đặc quyền trong container: rootless non-root
- Podman giúp việc chạy container rootless rootful trở nên dễ dàng, và nếu có thể chạy tiến trình container dưới người dùng không có đặc quyền thì cũng tương đối dễ xây dựng cấu hình rootless non-root
Bind mount và cách ly UID
- Khi mount một thư mục của host vào container, khả năng truy cập các tệp do
root của host, bar của host và foo trong namespace sở hữu sẽ thay đổi tùy theo ánh xạ UID
- Trong ví dụ, tạo các tệp
root.txt do root của host sở hữu và bar.txt do bar của host sở hữu trong thư mục /var/lib/bar/test, rồi mount đọc/ghi vào /test trong container
- Khi chạy container bằng
foo, tệp do bar của host sở hữu sẽ hiện là root:root bên trong container, còn tệp do root của host sở hữu sẽ hiện là nobody:nogroup vì không được ánh xạ vào namespace
foo bên trong container không thể đọc bar.txt và root.txt, và rootless non-root cung cấp thêm một lớp cách ly so với rootless rootful
foo.txt do foo tạo trong thư mục đã mount sẽ hiển thị trên host là do UID 166537 sở hữu, và người dùng bar trên host không thể đọc nội dung tệp đó
- Khi chạy container dưới dạng
root bên trong, root trong namespace có thể đọc tệp do bar của host sở hữu và tệp do foo sở hữu, nhưng không thể đọc tệp root.txt do root của host sở hữu
- Khi chạy dưới dạng
root bên trong và áp dụng --cap-drop=all, ngay cả tệp của foo cũng không đọc được, và chỉ có thể đọc tệp do bar của host sở hữu
Kiểm thử Copy Fail
-
Điều kiện kiểm thử
- Bài kiểm thử Copy Fail sử dụng phiên bản exploit từ commit đã công khai ban đầu
8e918b5
- Ảnh container mẫu được thêm
curl vào ảnh máy chủ HTTP hiện có để có thể tải script exploit từ bên trong container
- Ảnh được build với tên
copyfail
- Kernel dùng để kiểm thử là
6.12.74+deb13+1-amd64 của Debian, và theo tiêu chuẩn Debian, các bản gần đây dưới 6.12.85 được xem là vẫn có thể dùng làm kernel chưa vá
- Thông thường, khi người dùng không đặc quyền
foo gọi su, sẽ bị yêu cầu mật khẩu root
- Trong mỗi bài kiểm thử, người dùng container tải script Copy Fail về
/tmp, chạy nó, rồi nếu lấy được shell root thì gọi sleep
- Do Copy Fail tồn tại vượt qua vòng đời của container, VM được khởi động lại trước mỗi lần kiểm thử
-
Kết quả trong rootless rootful
- Khi chạy container với
--user=root, tiến trình bên trong container vốn đã là root
- Trong trạng thái này, nếu chạy script Copy Fail rồi gọi
su, sẽ nhận được shell uid=0(root), nhưng vì người dùng root vốn đã có thể mở một shell root khác bằng su mà không cần mật khẩu, nên Copy Fail về thực chất không bổ sung gì thêm
- Trong
podman top, /bin/bash, python3 copy_fail_exp.py, su, sleep đều hiện là root bên trong container, người dùng host 1001
- Cùng một tập capability được giữ nguyên, gồm
CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT
root bên trong có thể đọc bar.txt và foo.txt trong /test đã mount, nhưng không thể đọc root.txt do root của host sở hữu
-
Kết quả trong rootless non-root
- Sau khi chạy container bằng
foo, nếu chạy script Copy Fail rồi gọi su, quyền sẽ được leo thang lên root bên trong container
id của shell kết quả hiển thị là uid=0(root) gid=1002(foo) groups=1002(foo)
- Trong
podman top, /bin/bash ban đầu, tiến trình chạy exploit và lệnh gọi su đều hiện với UID host 166537, người dùng container foo, và capabilities none
- Sau khi leo thang đặc quyền,
[sh] và sleep hiện là người dùng host 1001, người dùng container root, và có cùng tập capability như rootless rootful
- Ngay cả
root của container sau khi leo thang đặc quyền cũng không thể đọc root.txt do root của host sở hữu
- Trong trạng thái này, container đã bị xâm phạm, nhưng phạm vi tấn công bị giới hạn trong phạm vi mà container và người dùng không đặc quyền
bar trên host có thể thực hiện
-
Kết quả khi áp dụng no-new-privileges
- Podman có thể dùng
--security-opt=no-new-privileges để ngăn tiến trình container có thêm đặc quyền so với thời điểm khởi chạy
- Khi áp dụng tùy chọn này cho container rootless non-root và chạy Copy Fail, shell vẫn mở nhưng vẫn ở trạng thái
uid=1002(foo)
- Trong
podman top, mọi tiến trình vẫn giữ UID host 166537, người dùng container foo, capabilities none
- Trong
/test đã mount, foo cũng chỉ có thể đọc tệp của chính mình và không thể đọc bar.txt hay root.txt
- Container đã bị xâm phạm, nhưng bị giới hạn ở người dùng không đặc quyền
foo bên trong cùng trạng thái không có capability
-
Kết quả khi áp dụng --cap-drop=all
- Ngay cả khi chạy container rootless non-root với
--cap-drop=all, foo vốn dĩ cũng không có capability nào
- Trong trạng thái này, khi chạy Copy Fail và gọi
su, shell mở ra vẫn giữ uid=1002(foo)
- Trong
podman top, /bin/bash, tiến trình exploit, su, shell và sleep đều ở trạng thái foo và capabilities none
- Exploit không lấy được shell
root, và foo chỉ có thể đọc tệp của chính mình trong /test
- Kết quả này tương tự bài kiểm thử
no-new-privileges, và hai biện pháp này có thể được dùng cùng nhau để giảm hiệu quả mức độ lộ capability
-
Tính dai dẳng của exploit
- Dù có thể ngăn việc lấy shell
root ngay lập tức và capability bằng no-new-privileges hoặc --cap-drop=all, hiệu quả của chính exploit vẫn còn tồn tại
- Nếu sau đó chạy một container mới mà không giới hạn capability, người dùng container không đặc quyền
foo vẫn có thể trở thành root của container chỉ bằng cách gọi su
- Vì vậy, việc vá kernel và khởi động lại vẫn là cần thiết
Chiến lược phòng thủ theo chiều sâu
-
Ảnh chỉ đọc
- Thêm
--read-only vào podman run sẽ gắn hệ thống tệp gốc của container ở chế độ chỉ đọc
- Podman mặc định vẫn gắn một số thư mục như
/tmp, /run, /var/tmp ở chế độ có thể ghi, nên để biến nó thành hoàn toàn chỉ đọc thì cần thêm cả --read-only-tmpfs=false
- Nếu container chỉ đọc bị xâm nhập, hệ thống sẽ không cho phép ghi, nên có thể hạn chế một số kiểu tấn công sau khai thác
- Tuy vậy, vì có thể pipe đầu ra của
curl sang python3, chỉ riêng thiết lập chỉ đọc không ngăn được việc thực thi exploit
- Máy chủ HTTP
python3 trong ví dụ không cần ghi vào hệ thống tệp, nên có thể dùng tùy chọn này một cách an toàn
- Nhiều ảnh dựng sẵn giả định có quyền ghi vào một số thư mục nhất định, nên có thể không hoạt động đúng với hệ thống tệp gốc chỉ đọc
- Hệ thống tệp gốc chỉ đọc là độc lập với các volume có thể ghi được gắn vào container; khi bị xâm nhập, vẫn có thể ghi vào các thư mục mount đó
-
Giới hạn tài nguyên
- Docker và Podman có thể dùng cgroups để giới hạn tài nguyên cấp cho container
- Container không cần bộ nhớ, CPU và PID không giới hạn
- Có thể kiểm tra mức sử dụng tài nguyên của container bằng
podman stats rồi áp dụng giới hạn phù hợp
-
Giới hạn các binary khả dụng
- Ví dụ dùng ảnh
ubuntu để đơn giản hóa, nhưng ảnh ubuntu chứa nhiều binary mà kẻ tấn công có thể lợi dụng khi container bị xâm nhập
- Việc chạy máy chủ HTTP không cần đến phần lớn các binary đó
- Nên giữ ảnh runtime mỏng nhất có thể
- Có thể dùng multi-stage build để tách môi trường build-time và runtime
- Có thể dùng các ảnh chuyên mục đích như python3, biến thể
-slim của Debian, hoặc các bản phân phối nhỏ hơn như alpine
- Nếu tương thích với tiến trình container, có thể dùng distroless images hoặc
scratch để tạo runtime không có shell, trình quản lý gói hay tiện ích hệ thống
-
Tường lửa
- Có thể dùng
iptables hoặc nftables để giới hạn tiến trình container bằng tường lửa
- Chỉ nên cho phép các kết nối vào/ra thực sự cần thiết cho tiến trình container
- Trong ví dụ máy chủ HTTP, không cần DNS hay kết nối tới máy chủ cục bộ/từ xa, nên có thể giới hạn theo hướng chỉ cho phép các gói
tcp đến từ những kết nối vào đã được thiết lập
Ý nghĩa trong vận hành
- Container rootless chuẩn của Podman mặc định cung cấp khả năng cô lập tốt hơn cấu hình container Docker tiêu chuẩn
- Docker cũng hỗ trợ chạy rootless và dùng user namespace không đặc quyền, nhưng cần nhiều công sức cấu hình hơn Podman và còn bị ảnh hưởng bởi khác biệt về kiến trúc
- Docker vẫn được dùng rất rộng rãi, và các công cụ self-hosting như Dokku, Kamal, Coolify, Dokploy cũng mặc định dùng Docker
- Nếu chạy ảnh từ Docker Hub mà không rà soát kỹ hoặc không áp dụng các biện pháp khóa chặt, dịch vụ có thể chạy với bề mặt tấn công rộng hơn mức cần thiết
- Cần hiểu các chi tiết triển khai của ảnh container
- Cần biết tiến trình container chạy dưới người dùng nào hoặc những người dùng nào
- Cần biết tiến trình container phụ thuộc vào những thư mục nào trong hệ thống tệp gốc
- Cần phân biệt Linux capabilities nào là cần thiết và capabilities nào là không cần
- Kết hợp nhiều cơ chế mà Podman và container cung cấp có thể giúp harden container và giảm blast radius khi bị xâm nhập
- Tùy theo workload, không nên coi container là ranh giới bảo mật duy nhất
- Có thể kết hợp container với máy vật lý hoặc máy ảo riêng để cô lập hiệu quả
- Podman cung cấp cách cô lập từng workload ngay trên cùng một host bằng cách chạy chúng dưới các người dùng không đặc quyền riêng biệt và user namespace riêng của chúng
Tài liệu bổ sung
1 bình luận
Ý kiến trên Lobste.rs
Cần tập trung vào hành vi nguyên thủy mà lỗ hổng này cho phép, hơn là exploit đã được công bố
Lỗ hổng này cho phép ghi vào page cache bất kể có phải chỉ đọc hay không, nên container độc hại có thể sửa đổi các trang thuộc về file ảnh nền của overlayfs, và tùy cách triển khai container, ảnh hưởng còn có thể lan sang các container khác
Trong cấu hình rootless ở đây, mục tiêu sẽ là các container khác đang chạy dưới cùng một người dùng trên hệ thống host
Một cách exploit khác là chạy hoặc tìm một container dựa trên base image đã biết là đang được sử dụng, sửa page cache bên trong container đó, rồi khiến các container khác dùng chung cùng runtime và dữ liệu overlayfs thực thi đoạn mã đó
Rootless và user namespace là quan trọng, nhưng trong trường hợp này không giúp ích nhiều; như trang copy.fail nói, nên cân nhắc chặn lời gọi hệ thống
socket(AF_ALG, ...)bằng seccomp trong containerSẽ tốt hơn nếu giải thích cụ thể “tùy cách triển khai container” ở đây nghĩa là gì
Điểm mạnh của rootless Podman là, tùy workload, không cần phải chạy container bằng cùng một người dùng trên host
Nếu ý là trường hợp chạy nhiều container rootless dưới tài khoản người dùng chính của máy trạm thì tôi đồng ý, nhưng trên server có thể tách từng cái thành người dùng riêng, và cùng một image container cũng có thể chạy dưới các người dùng không đặc quyền khác nhau
Điều này khá khác với mặc định của Docker là chạy phần lớn mọi thứ bằng
root, nhưng ở cuối bài tôi cũng đã viết rằng đây không phải ranh giới bảo mật tối hậu, và việc chia container rootless cho nhiều người dùng không đặc quyền có phù hợp hay không còn tùy mục đích sử dụngMột số workload cụ thể thì tôi tách riêng bằng VM
Tôi muốn hỏi việc nói rootless và user namespace không giúp ích ở đây có phải là đang nói tới việc ngăn exploit hay không
Tôi chưa từng dùng seccomp với policy tường minh cho container nên không đề cập, nhưng đây là dịp tốt để tìm hiểu thêm
Tôi thích Podman và container rootless, nhưng sau khi xem CopyFail thì cũng đi đến cùng kết luận như bình luận bên cạnh
Dù
podman+rootlesscó lợi thế kiểm soát truy cập bổ sung, cuối cùng nó vẫn xác nhận lại lời khuyên kinh điển rằng container không phải là ranh giới bảo mật, và chỉ cần một kernel exploit là có thể xuyên thủng toàn bộTôi chỉ quản trị hệ thống như một sở thích, nhưng như một hướng đi mới trong mảng này tôi có để ý libkrun backend for crun with podman
Nó hứa hẹn vẫn xử lý được phần lớn workload đã được container hóa, nhưng thực chất chạy bên trong MicroVM có guest kernel riêng
Tôi không rõ mức độ trưởng thành, mức kiểm chứng thực tế hay mức độ audit bảo mật của nó ra sao, và một số phần trông vẫn khá tiên phong
MicroVM đang được các công cụ lập trình dùng LLM tích cực tiếp nhận, nên tình trạng đó có thể còn kéo dài một thời gian
podman machinecũng có vẻ đầy hứa hẹn, nhưng tiếc là dường như chỉ nhắm tới máy trạm của lập trình viên, với mô hình mỗi hệ thống host chỉ có một VM để chạy containerDù vậy, tôi vẫn thấy câu “container không phải là ranh giới bảo mật” là quá đơn giản. Container rõ ràng vẫn là một ranh giới bảo mật, chỉ là không mạnh đến mức chúng ta muốn tin mà thôi
Trong triển khai cục bộ, ranh giới này mờ hơn đôi chút
Xét từ góc độ phần cứng, VM không an toàn hơn process một cách bản chất, nhưng có ba lý do khiến ranh giới của nó dễ phòng thủ hơn
Thoát VM hiếm hơn system call, nên có nhiều khoảng trống hơn để áp dụng các biện pháp giảm thiểu side-channel mà không phải trả giá hiệu năng quá lớn
Giao diện host của VM cũng đơn giản hơn nhiều. Thiết bị khối có giao diện đọc ghi theo block, còn thiết bị mạng thì gửi và nhận frame
Lời gọi
setsockoptmà Linux hay *BSD cung cấp cho socket có bề mặt tấn công lớn hơn rất nhiều so với hầu hết driver giả lập hoặc paravirtualization, và ngay cả như vậy thì đó cũng chỉ là một phần rất nhỏ trong toàn bộ bề mặt tấn công của kernelGiao diện VM cũng có ít trạng thái hơn nhiều. Có các giao dịch đang diễn ra trong vòng ring theo mô hình request-response, nhưng ngoài ra hầu như không có gì
Những thứ như credential, UID, GID, bảng file descriptor làm tăng độ phức tạp dựa trên trạng thái trong kernel, và nếu có bug thì process có thể lợi dụng chúng
Khó khăn của biến thể dùng cho workstation là nó lại đưa những độ phức tạp đó trở về
Ví dụ, layer nền của container có thể được phơi ra như một block device chứa filesystem bất biến, nhưng volume và thư mục chia sẻ thì có lẽ sẽ được mount bằng 9pfs hoặc VirtIO-FS, tức 9p hoặc FUSE trên VirtIO
Khi đó bề mặt tấn công lại mở rộng ra
Nếu may mắn thì exploit sẽ cần tới cả một chuỗi tấn công
Tôi quen phía FreeBSD hơn, ở đó thường sandbox các thành phần cung cấp thiết bị paravirtualization hoặc giả lập bằng Capsicum, nên trước tiên phải chiếm được process trên host, rồi sau đó nếu muốn truy cập thứ mà VM vốn không có quyền, còn phải xuyên tiếp vào kernel
Nhưng nếu không có lớp sandbox bổ sung như vậy, thì thoát container lại trở về thế giới nơi kẻ tấn công có thể làm mọi thứ mà người dùng đó làm được, và không khá hơn bao nhiêu so với việc root bị phá trên desktop
Cá nhân tôi còn thích gVisor hơn. Nó không phải runtime VMM, nhưng đã tồn tại nhiều năm và còn được các công ty như Tencent sử dụng, nên rất hợp với môi trường của tôi, nơi mọi container vốn đã chạy bên trong VM Proxmox
Một thứ khác tôi đang thử là syd-oci, có vẻ như nhận được ít chú ý hơn đôi chút so với hai lựa chọn mặc định là MicroVM hoặc gVisor
Cảm ơn vì tài liệu tham khảo về libkrun, trông nó là một hướng đi đầy hứa hẹn
Khả năng cao điều đó cũng sẽ dẫn tới các đợt audit bảo mật