- CVE-2026-31431 Copy Fail ทำให้ผู้ใช้ภายในเครื่องที่ไม่มีสิทธิ์สามารถได้เชลล์
rootและยังสามารถยกระดับสิทธิ์เป็นrootภายในคอนเทนเนอร์ Podman แบบ rootless ได้ด้วย - คอนเทนเนอร์ Podman แบบ rootless ใช้การผสานกันของ user namespace, การแยก UID และ Linux capabilities เพื่อแมป
rootภายในคอนเทนเนอร์ให้เป็นผู้ใช้บนโฮสต์ที่ไม่มีสิทธิ์ และจำกัดสิทธิ์บนโฮสต์ - ในการทดสอบ ผู้ใช้
fooในคอนเทนเนอร์ rootless non-root สามารถกลายเป็นrootภายในคอนเทนเนอร์ได้หลังรัน Copy Fail แต่สิทธิ์ยังถูกจำกัดอยู่ในขอบเขตที่ผู้ใช้บนโฮสต์ที่ไม่มีสิทธิ์อย่างbarทำได้ และไม่สามารถอ่านไฟล์ที่โฮสต์rootเป็นเจ้าของได้ - หากใช้
--security-opt=no-new-privilegesหรือ--cap-drop=allหลังรัน Copy Fail แล้ว เชลล์จะยังคงเป็นfooและมี capabilities เป็นnoneทำให้ป้องกันการได้เชลล์rootทันทีและการยกระดับ capability ได้ - ผลของ Copy Fail อาจคงอยู่ต่อไปเกินกว่าวงจรชีวิตของคอนเทนเนอร์ จึงต้องแพตช์เคอร์เนลและรีบูต รวมถึงควรใช้ defense in depth ร่วมกัน เช่น root filesystem แบบอ่านอย่างเดียว, การจำกัดทรัพยากรด้วย cgroups, อิมเมจรันไทม์แบบบาง และไฟร์วอลล์
ขอบเขตการได้รับผลกระทบของ Copy Fail และคอนเทนเนอร์ Podman แบบ rootless
- CVE-2026-31431 ถูกเปิดเผยเมื่อวันที่ 29 เมษายนที่ copy.fail และเมื่อรัน สคริปต์ Python ที่เผยแพร่ไว้ ผู้ใช้ภายในเครื่องที่ไม่มีสิทธิ์ก็สามารถได้เชลล์
rootได้ - Copy Fail สามารถถูกใช้โจมตีได้แม้ภายในคอนเทนเนอร์ Linux และสามารถได้เชลล์
rootภายในคอนเทนเนอร์ Podman แบบ rootless ได้เช่นกัน - ในการทดสอบ
rootของคอนเทนเนอร์ถูกจำกัดไว้ในขอบเขตสิทธิ์ของผู้ใช้บนโฮสต์ที่ไม่มีสิทธิ์ชื่อbarซึ่งเป็นผู้ที่รันคอนเทนเนอร์ในระดับโฮสต์ - การทำงานแบบ rootless ของ Podman ใช้ user namespace, การแยก UID และ Linux capabilities ร่วมกันเพื่อจำกัดสิทธิ์บนโฮสต์ของโปรเซสในคอนเทนเนอร์
- Copy Fail แสดงให้เห็นว่าคอนเทนเนอร์ rootless ไม่ได้มีภูมิคุ้มกันต่อช่องโหว่นี้ แต่สามารถลดขอบเขตผลกระทบหลังถูกเจาะได้ด้วยการตั้งค่า Podman
วิธีการทำงานของคอนเทนเนอร์ rootless
-
ตัวอย่างพื้นฐาน: ผู้ใช้
barที่ไม่มีสิทธิ์รัน HTTP server- สภาพแวดล้อมตัวอย่างคือผู้ใช้
barที่ไม่มีสิทธิ์และมี UID1001สร้างอิมเมจบนฐานubuntu:latestด้วย Podman และรันpython3 -m http.server - เมื่อดูจากโฮสต์ด้วย
psจะเห็นว่าโปรเซสpython3รันอยู่ภายใต้ผู้ใช้bar - Podman ใช้โมเดล fork/exec ดังนั้นโปรเซสของคอนเทนเนอร์จึงเป็นลูกหลานของโปรเซส
podman runและสามารถแยกโปรเซสคอนเทนเนอร์ออกจากrootของโฮสต์หรือผู้ใช้อื่นได้ด้วยการแยก UID ตามปกติ - ในการตั้งค่า Docker ทั่วไป แม้ผู้ใช้ที่ไม่มีสิทธิ์จะรัน
docker runแต่ Docker client จะสื่อสารกับเดมอนที่มีสิทธิ์ root และเดมอนจะเป็นผู้สร้างโปรเซสคอนเทนเนอร์ในท้ายที่สุด ทำให้บนโฮสต์อาจเห็นโปรเซสคอนเทนเนอร์เป็นroot
- สภาพแวดล้อมตัวอย่างคือผู้ใช้
-
rootful แบบ rootless
- โดยทั่วไป หากอิมเมจคอนเทนเนอร์ไม่มีคำสั่ง
USERหรือแฟลก--userอย่างชัดเจน คำสั่งในคอนเทนเนอร์จะถูกรันในฐานะrootภายในคอนเทนเนอร์ - ในผลลัพธ์ของ
podman topโปรเซส HTTP server จะถูกแมปเป็นผู้ใช้โฮสต์1001แต่รันเป็นผู้ใช้rootภายในคอนเทนเนอร์ - การตั้งค่านี้คือสถานะ rootless rootful ซึ่งหมายถึงรันเป็นผู้ใช้ไม่มีสิทธิ์บนโฮสต์ แต่เป็น
rootภายในคอนเทนเนอร์
- โดยทั่วไป หากอิมเมจคอนเทนเนอร์ไม่มีคำสั่ง
-
user namespace
- คอนเทนเนอร์ Podman แบบ rootless ใช้ user namespace เพื่อแมป UID/GID ภายในและภายนอกคอนเทนเนอร์ให้ต่างกัน
- ในตัวอย่าง
rootที่มี UID0ภายในคอนเทนเนอร์จะถูกแมปเป็นbarที่มี UID1001บนโฮสต์ - การตั้งค่า
bar:165536:65536ใน/etc/subuidกำหนดช่วง UID ที่สามารถจัดสรรให้กับโปรเซส namespace ของbarได้ - ในตัวอย่าง นอกจาก UID
1001ของbarแล้ว UID ตั้งแต่165536ถึง231072ยังสามารถถูกจัดสรรให้กับโปรเซสของbarได้ด้วย - หากรัน
sleepด้วยผู้ใช้www-dataภายในคอนเทนเนอร์ ภายในจะเป็นwww-dataแต่บนโฮสต์จะแสดงเป็น165568 - หากเข้าไปยัง user namespace ด้วย
podman unshareโฮมไดเรกทอรีที่บนโฮสต์เป็นของbar:barจะมองเห็นเป็นroot:rootภายใน namespace - Docker ก็รองรับ user namespace เช่นกัน แต่ต้องตั้งค่าแยกต่างหากและอนุญาตได้เพียง user namespace เดียว ขณะที่ Podman จะรันคอนเทนเนอร์ rootless ของผู้ใช้ UNIX แต่ละรายภายใน user namespace ของผู้ใช้นั้น
-
งานด้านสิทธิ์และ Linux capabilities
- Podman ใช้ Linux capabilities เพื่อมอบสิทธิ์ระดับ root แบบละเอียดให้กับโปรเซสในคอนเทนเนอร์
- ระหว่างการ build อิมเมจ งานอย่าง
apt installทำได้ด้วยชุด capability เช่นchown,dac_override,fowner,setgid,setuid,net_bind_service,sys_chroot - หากลบ capability ทั้งหมดด้วย
podman build --cap-drop=allแล้วaptจะล้มเหลวกับsetgroups,setegid,seteuid,chownและอื่น ๆ ทำให้การ build อิมเมจไม่สำเร็จ - สามารถใช้วิธีเพิ่มเฉพาะ capability ที่จำเป็นได้ และในตัวอย่างมีการเพิ่ม
CAP_SETUID,CAP_SETGID,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNERเพื่อทำการติดตั้งแพ็กเกจ - HTTP server ในสถานะรันปกติจะทำงานเป็น
rootภายในคอนเทนเนอร์ และมี effective capabilities จำนวนมาก เช่นCHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT - แต่ HTTP server ไม่จำเป็นต้องใช้สิทธิ์เหล่านี้ จึงสามารถลบ capability ทั้งหมดได้ด้วย
podman run --cap-drop=allและในกรณีนี้podman topจะแสดง effective capabilities เป็นnone
-
rootless non-root
- หากต้องการรัน HTTP server เป็นผู้ใช้ที่ไม่มีสิทธิ์แม้ภายในคอนเทนเนอร์ ก็สามารถใช้ผู้ใช้ที่มีอยู่ใน
/etc/passwdเช่นwww-dataหรือสร้างผู้ใช้เฉพาะระหว่างการ build อิมเมจได้ - ในตัวอย่างมีการสร้างผู้ใช้และกลุ่ม
fooที่มี UID1002ให้สิทธิ์อ่านกับ/var/www/htmlแล้วตั้งค่าUSER foo:foo - เมื่อรันอิมเมจนี้ด้วย
--cap-drop=allโปรเซสจะอยู่ในสถานะเป็นfooภายในคอนเทนเนอร์, โฮสต์ UID166537และ effective capabilities เป็นnone - โปรเซสของคอนเทนเนอร์ควรรันด้วยสิทธิ์ขั้นต่ำที่จำเป็น และตัวอย่างเช่น หาก
fooต้อง bind กับ privileged port80ก็ต้องเพิ่ม--cap-add=CAP_NET_BIND_SERVICE - วิธีรันคอนเทนเนอร์แบ่งได้เป็น 4 แบบ
- ผู้ใช้โฮสต์
root+ คอนเทนเนอร์root: root rootful - ผู้ใช้โฮสต์
root+ ผู้ใช้คอนเทนเนอร์ที่ไม่มีสิทธิ์: root non-root - ผู้ใช้โฮสต์ที่ไม่มีสิทธิ์ + คอนเทนเนอร์
root: rootless rootful - ผู้ใช้โฮสต์ที่ไม่มีสิทธิ์ + ผู้ใช้คอนเทนเนอร์ที่ไม่มีสิทธิ์: rootless non-root
- ผู้ใช้โฮสต์
- Podman ทำให้การรันคอนเทนเนอร์ rootless rootful ทำได้ง่าย และหากสามารถรันโปรเซสคอนเทนเนอร์เป็นผู้ใช้ที่ไม่มีสิทธิ์ได้ การตั้งค่าแบบ rootless non-root ก็ทำได้ค่อนข้างง่ายเช่นกัน
- หากต้องการรัน HTTP server เป็นผู้ใช้ที่ไม่มีสิทธิ์แม้ภายในคอนเทนเนอร์ ก็สามารถใช้ผู้ใช้ที่มีอยู่ใน
bind mount และการแยก UID
- เมื่อนำไดเรกทอรีของโฮสต์มาเมานต์ในคอนเทนเนอร์ การเข้าถึงไฟล์ที่เป็นเจ้าของโดยโฮสต์
root, โฮสต์barและเนมสเปซfooจะต่างกันไปตามการแมป UID - ในตัวอย่าง มีการสร้างไฟล์
root.txtที่โฮสต์rootเป็นเจ้าของ และbar.txtที่โฮสต์barเป็นเจ้าของ ในไดเรกทอรี/var/lib/bar/testแล้วเมานต์แบบอ่าน/เขียนเป็น/testภายในคอนเทนเนอร์ - เมื่อรันคอนเทนเนอร์ด้วย
fooไฟล์ที่โฮสต์barเป็นเจ้าของจะมองเห็นเป็นroot:rootภายในคอนเทนเนอร์ ส่วนไฟล์ที่โฮสต์rootเป็นเจ้าของจะไม่ถูกแมปในเนมสเปซ จึงมองเห็นเป็นnobody:nogroup fooภายในคอนเทนเนอร์ไม่สามารถอ่านbar.txtและroot.txtได้ และ rootless non-root ให้การแยกเพิ่มเติมมากกว่า rootless rootfulfoo.txtที่fooสร้างในไดเรกทอรีที่เมานต์ไว้จะแสดงบนโฮสต์ว่าเป็นของ UID166537และผู้ใช้โฮสต์barไม่สามารถอ่านเนื้อหาไฟล์นั้นได้- หากรันคอนเทนเนอร์เป็น
rootภายใน เนมสเปซrootจะสามารถอ่านไฟล์ที่โฮสต์barเป็นเจ้าของและไฟล์ของfooได้ แต่ยังอ่านไฟล์root.txtที่โฮสต์rootเป็นเจ้าของไม่ได้ - หากรันเป็น
rootภายในพร้อมใช้--cap-drop=allจะอ่านไฟล์ของfooไม่ได้เช่นกัน และอ่านได้เฉพาะไฟล์ที่โฮสต์barเป็นเจ้าของ
การทดสอบ Copy Fail
-
เงื่อนไขการทดสอบ
- การทดสอบ Copy Fail ใช้เวอร์ชันเอ็กซ์พลอยต์จากคอมมิตที่เปิดเผยเดิม
8e918b5 - อิมเมจคอนเทนเนอร์ตัวอย่างเพิ่ม
curlเข้าไปในอิมเมจ HTTP server เดิม เพื่อให้ดาวน์โหลดสคริปต์เอ็กซ์พลอยต์จากภายในคอนเทนเนอร์ได้ - อิมเมจถูก build ด้วยชื่อ
copyfail - เคอร์เนลที่ใช้ทดสอบคือ
6.12.74+deb13+1-amd64ของ Debian และตามเกณฑ์ของ Debian เวอร์ชันล่าสุดที่ต่ำกว่า6.12.85ถือว่ายังเป็นเคอร์เนลที่ไม่ถูกแพตช์และนำมาใช้ทดสอบได้ - โดยทั่วไปเมื่อผู้ใช้ที่ไม่มีสิทธิ์
fooเรียกsuจะต้องใส่รหัสผ่านของroot - ในแต่ละการทดสอบ ผู้ใช้คอนเทนเนอร์จะดาวน์โหลดสคริปต์ Copy Fail ไปไว้ที่
/tmpแล้วรัน จากนั้นหากได้เชลล์rootก็จะเรียกsleep - Copy Fail คงอยู่ข้ามวงจรชีวิตของคอนเทนเนอร์ ดังนั้นจึงรีบูต VM ก่อนการทดสอบแต่ละครั้ง
- การทดสอบ Copy Fail ใช้เวอร์ชันเอ็กซ์พลอยต์จากคอมมิตที่เปิดเผยเดิม
-
ผลลัพธ์ใน rootless rootful
- เมื่อรันคอนเทนเนอร์ด้วย
--user=rootโปรเซสภายในคอนเทนเนอร์จะเป็นrootอยู่แล้ว - ในสถานะนี้ หากรันสคริปต์ Copy Fail แล้วเรียก
suจะได้เชลล์uid=0(root)แต่เดิมผู้ใช้rootก็สามารถเปิด root shell อื่นด้วยsuโดยไม่ต้องใช้รหัสผ่านอยู่แล้ว จึงแทบไม่มีสิ่งที่ Copy Fail เพิ่มให้จริง - ใน
podman topจะเห็นว่า/bin/bash,python3 copy_fail_exp.py,su,sleepทั้งหมดแสดงเป็นrootภายในคอนเทนเนอร์ และผู้ใช้โฮสต์1001 - ชุด capability เดิมยังคงอยู่ และเห็น
CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT rootภายในสามารถอ่านbar.txtและfoo.txtใน/testที่เมานต์ไว้ได้ แต่ยังอ่านroot.txtที่โฮสต์rootเป็นเจ้าของไม่ได้
- เมื่อรันคอนเทนเนอร์ด้วย
-
ผลลัพธ์ใน rootless non-root
- หลังจากรันคอนเทนเนอร์ด้วย
fooแล้วรันสคริปต์ Copy Fail และเรียกsuจะเกิดการยกระดับสิทธิ์เป็นrootภายในคอนเทนเนอร์ idของเชลล์ที่ได้จะแสดงเป็นuid=0(root) gid=1002(foo) groups=1002(foo)- ใน
podman topจะเห็นว่า/bin/bashเริ่มต้น, โปรเซสที่รันเอ็กซ์พลอยต์ และการเรียกsuแสดงเป็น UID โฮสต์166537, ผู้ใช้คอนเทนเนอร์fooและ capabilities เป็นnone - หลังยกระดับสิทธิ์แล้ว
[sh]และsleepจะแสดงเป็นผู้ใช้โฮสต์1001, ผู้ใช้คอนเทนเนอร์rootและได้รับชุด capability เดียวกับ rootless rootful - แม้แต่
rootของคอนเทนเนอร์ที่ยกระดับสิทธิ์แล้วก็ยังอ่านroot.txtที่โฮสต์rootเป็นเจ้าของไม่ได้ - ในสถานะนี้แม้คอนเทนเนอร์จะถูกเจาะได้ แต่ขอบเขตการโจมตียังถูกจำกัดอยู่ในขอบเขตที่คอนเทนเนอร์และผู้ใช้โฮสต์ที่ไม่มีสิทธิ์
barทำได้
- หลังจากรันคอนเทนเนอร์ด้วย
-
ผลลัพธ์เมื่อใช้
no-new-privileges- Podman สามารถใช้
--security-opt=no-new-privilegesเพื่อไม่ให้โปรเซสในคอนเทนเนอร์ได้รับสิทธิ์มากกว่าตอนเริ่มต้น - เมื่อใช้ตัวเลือกนี้กับคอนเทนเนอร์ rootless non-root แล้วรัน Copy Fail จะเปิดเชลล์ได้ แต่ยังคงเป็น
uid=1002(foo) - ใน
podman topเช่นกัน ทุกโปรเซสยังคงเป็น UID โฮสต์166537, ผู้ใช้คอนเทนเนอร์fooและ capabilitiesnone - ที่
/testซึ่งถูกเมานต์ไว้fooก็ยังอ่านได้เฉพาะไฟล์ของตัวเอง และไม่สามารถอ่านbar.txtกับroot.txtได้ - แม้คอนเทนเนอร์จะถูกเจาะ แต่ก็ยังถูกจำกัดไว้ที่ผู้ใช้ภายในที่ไม่มีสิทธิ์
fooและสถานะที่ไม่มี capability
- Podman สามารถใช้
-
ผลลัพธ์เมื่อใช้
--cap-drop=all- แม้จะรันคอนเทนเนอร์ rootless non-root ด้วย
--cap-drop=allแต่เดิมfooก็ไม่มี capability อยู่แล้ว - ในสถานะนี้ หากรัน Copy Fail และเรียก
suเชลล์ที่เปิดขึ้นจะยังคงเป็นuid=1002(foo) - ใน
podman topเช่นกัน/bin/bash, การรันเอ็กซ์พลอยต์,su, เชลล์ และsleepทั้งหมดจะยังเป็นfooและ capabilitiesnone - เอ็กซ์พลอยต์ไม่สามารถยึด root shell ได้ และ
fooอ่านได้เฉพาะไฟล์ของตัวเองใน/test - ผลลัพธ์นี้คล้ายกับการทดสอบ
no-new-privilegesและสามารถใช้ทั้งสองมาตรการร่วมกันเพื่อลดการเปิดเผย capability ได้อย่างมีประสิทธิภาพ
- แม้จะรันคอนเทนเนอร์ rootless non-root ด้วย
-
ความคงอยู่ของเอ็กซ์พลอยต์
- แม้การได้ root shell และ capability ทันทีจะถูกป้องกันได้ด้วย
no-new-privilegesหรือ--cap-drop=allแต่ผลของเอ็กซ์พลอยต์เองยังคงอยู่ - หลังจากนั้น หากรันคอนเทนเนอร์ใหม่โดยไม่มีการจำกัด capability ผู้ใช้คอนเทนเนอร์ที่ไม่มีสิทธิ์
fooก็อาจกลายเป็นrootของคอนเทนเนอร์ได้เพียงแค่เรียกsu - ดังนั้นจึงยังจำเป็นต้องแพตช์เคอร์เนลและรีบูต
- แม้การได้ root shell และ capability ทันทีจะถูกป้องกันได้ด้วย
กลยุทธ์การป้องกันเชิงลึก
-
อิมเมจแบบอ่านอย่างเดียว
- การเพิ่ม
--read-onlyให้กับpodman runจะเมานต์รูทไฟล์ซิสเต็มของคอนเทนเนอร์เป็นแบบอ่านอย่างเดียว - โดยปกติ Podman จะเมานต์บางไดเรกทอรีอย่าง
/tmp,/run,/var/tmpให้เขียนได้อยู่แล้ว ดังนั้นหากต้องการให้เป็นแบบอ่านอย่างเดียวทั้งหมด ต้องเพิ่ม--read-only-tmpfs=falseด้วย - หากคอนเทนเนอร์แบบอ่านอย่างเดียวถูกเจาะ ผู้โจมตีจะไม่สามารถเขียนลงระบบได้ จึงช่วยจำกัดการโจมตีบางส่วนหลังการเอ็กซ์พลอยต์
- อย่างไรก็ตาม สามารถ pipe เอาต์พุตของ
curlไปยังpython3ได้ ดังนั้นการตั้งค่าแบบอ่านอย่างเดียวเพียงอย่างเดียวไม่ได้ป้องกันการรันเอ็กซ์พลอยต์โดยตรง python3HTTP server ในตัวอย่างไม่ต้องเขียนลงไฟล์ซิสเต็ม จึงสามารถใช้ตัวเลือกนี้ได้อย่างปลอดภัย- อิมเมจที่พรีบิลด์ไว้จำนวนมากตั้งสมมติฐานว่าจะต้องเขียนลงบางไดเรกทอรีได้ จึงอาจทำงานได้ไม่ถูกต้องบนรูทไฟล์ซิสเต็มแบบอ่านอย่างเดียว
- รูทไฟล์ซิสเต็มแบบอ่านอย่างเดียวแยกจากวอลุ่มที่เขียนได้ซึ่งต่อเข้ากับคอนเทนเนอร์ และหากถูกเจาะก็ยังคงเขียนลงไดเรกทอรีเมานต์เหล่านั้นได้
- การเพิ่ม
-
การจำกัดทรัพยากร
- Docker และ Podman สามารถใช้ cgroups เพื่อจำกัดทรัพยากรที่ให้กับคอนเทนเนอร์ได้
- คอนเทนเนอร์ไม่จำเป็นต้องใช้หน่วยความจำ, CPU หรือ PID แบบไม่จำกัด
- สามารถตรวจสอบการใช้ทรัพยากรของคอนเทนเนอร์ด้วย
podman statsแล้วจึงกำหนดขีดจำกัดให้เหมาะสมได้
-
จำกัดไบนารีที่ใช้งานได้
- ตัวอย่างนี้ใช้อิมเมจ
ubuntuเพื่อความเรียบง่าย แต่อิมเมจubuntuมีไบนารีจำนวนมากที่ผู้โจมตีสามารถใช้ได้หากคอนเทนเนอร์ถูกเจาะ - การรัน HTTP server ไม่ได้ต้องใช้ไบนารีส่วนใหญ่เหล่านี้
- อิมเมจรันไทม์ควรทำให้บางที่สุดเท่าที่เป็นไปได้
- สามารถใช้ multi-stage build เพื่อแยกสภาพแวดล้อมตอน build ออกจากสภาพแวดล้อมตอนรันไทม์ได้
- สามารถใช้อิมเมจเฉพาะงานอย่าง python3, เวอร์ชัน
-slimของ Debian หรือดิสทริบิวชันขนาดเล็กกว่าอย่างalpineเป็นฐานได้ - หากเข้ากันได้กับโปรเซสของคอนเทนเนอร์ สามารถใช้ distroless images หรือ
scratchเพื่อสร้างรันไทม์ที่ไม่มีเชลล์, package manager และยูทิลิตีของระบบได้
- ตัวอย่างนี้ใช้อิมเมจ
-
ไฟร์วอลล์
- สามารถใช้
iptablesหรือnftablesเพื่อ จำกัดโปรเซสของคอนเทนเนอร์ด้วยไฟร์วอลล์ ได้ - ควรอนุญาตเฉพาะการเชื่อมต่อขาเข้าและขาออกที่จำเป็นจริง ๆ สำหรับโปรเซสของคอนเทนเนอร์
- ในตัวอย่าง HTTP server ไม่จำเป็นต้องเชื่อมต่อกับ DNS หรือเซิร์ฟเวอร์ภายใน/ภายนอก ดังนั้นจึงสามารถจำกัดให้ยอมรับเฉพาะแพ็กเก็ต
tcpที่มาจากการเชื่อมต่อขาเข้าที่สร้างขึ้นแล้วได้
- สามารถใช้
ความหมายในเชิงปฏิบัติการ
- คอนเทนเนอร์ Podman แบบ rootless มาตรฐานให้การแยกตัวที่ดีกว่าคอนฟิกคอนเทนเนอร์ Docker มาตรฐานโดยค่าเริ่มต้น
- Docker ก็สามารถ รันแบบ rootless และ ใช้ user namespace แบบไม่มีสิทธิพิเศษ ได้เช่นกัน แต่ต้องตั้งค่ามากกว่า Podman และความแตกต่างด้านสถาปัตยกรรมก็มีผลด้วย
- Docker ยังคงถูกใช้อย่างแพร่หลาย และเครื่องมือ self-hosting อย่าง Dokku, Kamal, Coolify, Dokploy ก็ใช้ Docker เป็นค่าเริ่มต้น
- หากรันอิมเมจจาก Docker Hub โดยไม่ได้ตรวจสอบอย่างเพียงพอหรือไม่ได้ใช้มาตรการล็อกดาวน์ บริการอาจทำงานด้วยพื้นผิวการโจมตีที่กว้างเกินความจำเป็น
- ต้องเข้าใจรายละเอียดการติดตั้งใช้งานของอิมเมจคอนเทนเนอร์
- ต้องรู้ว่ามีผู้ใช้คนใดหรือผู้ใช้กลุ่มใดที่รันโปรเซสของคอนเทนเนอร์
- ต้องรู้ว่าโปรเซสของคอนเทนเนอร์พึ่งพาไดเรกทอรีใดบ้างในรูทไฟล์ซิสเต็ม
- ต้องแยกให้ออกว่า Linux capabilities ใดจำเป็น และ capabilities ใดไม่จำเป็น
- การผสานหลายกลไกที่ Podman และคอนเทนเนอร์มีให้สามารถช่วย harden คอนเทนเนอร์และลด blast radius เมื่อถูกเจาะได้
- ขึ้นอยู่กับลักษณะของเวิร์กโหลด ไม่ควรพึ่งคอนเทนเนอร์เป็นขอบเขตความปลอดภัยเพียงอย่างเดียว
- การใช้คอนเทนเนอร์ร่วมกับเครื่องจริงหรือเครื่องเสมือนที่แยกต่างหากสามารถช่วยแยกส่วนได้อย่างมีประสิทธิภาพ
- Podman มีวิธีแยกเวิร์กโหลดแต่ละตัวภายในโฮสต์เดียวกัน โดยให้รันด้วยผู้ใช้แบบไม่มีสิทธิพิเศษคนละรายและมี user namespace ของตัวเอง
1 ความคิดเห็น
ความเห็นจาก Lobste.rs
ควรโฟกัสที่ พฤติกรรมพื้นฐาน ที่ทำให้ช่องโหว่นี้เกิดขึ้นได้ มากกว่าตัวเอ็กซ์พลอยต์ที่ถูกเผยแพร่ออกมา
ช่องโหว่นี้ทำให้สามารถเขียนลง page cache ได้ไม่ว่าจะเป็นแบบอ่านอย่างเดียวหรือไม่ก็ตาม ดังนั้นคอนเทนเนอร์ที่เป็นอันตรายจึงสามารถแก้ไขหน้าเพจที่เป็นของไฟล์ base image ของ overlayfs ได้ และผลกระทบอาจลามไปยังคอนเทนเนอร์อื่นได้ด้วย ขึ้นอยู่กับวิธีการ deploy คอนเทนเนอร์
สำหรับการตั้งค่าแบบ rootless ในกรณีนี้ เป้าหมายจะเป็นคอนเทนเนอร์อื่นที่รันด้วยผู้ใช้คนเดียวกันบนระบบโฮสต์
วิธีเอ็กซ์พลอยต์อีกแบบคือ รันหรือค้นหาคอนเทนเนอร์ที่อิงกับ base image ซึ่งรู้แน่ว่ากำลังถูกใช้งานอยู่ แล้วแก้ไข page cache ภายในคอนเทนเนอร์นั้น จากนั้นทำให้คอนเทนเนอร์อื่นที่ใช้ runtime เดียวกันและแชร์ข้อมูล overlayfs เดียวกันไปรันโค้ดนั้น
rootless และ user namespace นั้นสำคัญ แต่ในกรณีนี้แทบไม่ช่วยอะไร และอย่างที่เว็บ copy.fail บอกไว้ ควรพิจารณาบล็อก system call
socket(AF_ALG, ...)ด้วย seccomp ภายในคอนเทนเนอร์ถ้าช่วยอธิบายเพิ่มได้จะดีว่า “ขึ้นอยู่กับวิธีการ deploy คอนเทนเนอร์” หมายถึงอะไรแบบเจาะจง
ข้อดีของ rootless Podman คือขึ้นอยู่กับ workload ว่าไม่จำเป็นต้องรันคอนเทนเนอร์ด้วยผู้ใช้คนเดียวกันบนโฮสต์
ถ้าหมายถึงกรณีที่รัน rootless container หลายตัวด้วยผู้ใช้หลักของเวิร์กสเตชัน ก็เห็นด้วย แต่บนเซิร์ฟเวอร์สามารถแยกแต่ละตัวเป็นคนละผู้ใช้ได้ และแม้จะเป็น container image เดียวกันก็ยังรันด้วยผู้ใช้ที่ไม่มีสิทธิ์ต่างกันได้
มันต่างจากค่าเริ่มต้นของ Docker ที่มักรันเกือบทุกอย่างเป็น
rootมาก แต่ผมก็เขียนไว้ท้ายบทความแล้วว่านี่ไม่ใช่ security boundary ขั้นสุดท้าย และการกระจาย rootless container ไปให้ผู้ใช้ที่ไม่มีสิทธิ์หลายคนใช้นั้นเหมาะสมแค่ไหนก็ขึ้นกับงานบาง workload ผมแยกไปใช้ VM อยู่แล้ว
เลยสงสัยว่าที่บอกว่า rootless กับ user namespace ไม่ช่วยในที่นี้ หมายถึงไม่ช่วยป้องกันเอ็กซ์พลอยต์ใช่ไหม
ผมยังไม่เคยเขียนนโยบาย seccomp แบบชัดเจนให้คอนเทนเนอร์เลย เลยไม่ได้พูดถึง แต่ก็นับเป็นจุดเริ่มต้นที่ดีให้ไปศึกษาต่อ
ผมชอบ Podman และ rootless container แต่พอเห็น CopyFail ก็ได้ข้อสรุปเหมือนคอมเมนต์ข้างเคียง
ต่อให้
podman+rootlessมีข้อดีด้านการควบคุมการเข้าถึงเพิ่มขึ้น สุดท้ายก็ยืนยันคำเตือนคลาสสิกอีกครั้งว่า คอนเทนเนอร์ไม่ใช่ security boundary และเอ็กซ์พลอยต์เคอร์เนลเพียงตัวเดียวก็พังได้ทั้งหมดผมเป็นแค่คนดูแลระบบเป็นงานอดิเรก แต่แนวโน้มใหม่ในพื้นที่นี้ที่ผมเห็นคือ libkrun backend for crun with podman
มันสัญญาว่ายังจัดการ workload ที่ถูกทำให้เป็นคอนเทนเนอร์ส่วนใหญ่ได้เหมือนเดิม แต่ภายในจริง ๆ จะไปรันใน MicroVM ที่มี guest kernel แยกต่างหาก แม้ผมจะยังไม่แน่ใจเรื่องความสุกงอม การพิสูจน์ในภาคสนาม หรือระดับการตรวจสอบด้านความปลอดภัย และบางส่วนก็ดูค่อนข้างล้ำมาก
MicroVM กำลังถูกนำไปใช้เชิงรุกในเครื่องมือเขียนโค้ดสำหรับ LLM ดังนั้นสถานะนั้นอาจอยู่ต่อไปอีกนาน
podman machineก็ดูมีอนาคต แต่เสียดายที่ดูเหมือนออกแบบมาเพื่อเวิร์กสเตชันของนักพัฒนาเท่านั้น และเป็นโมเดลที่มี VM สำหรับรันคอนเทนเนอร์เพียงตัวเดียวต่อระบบโฮสต์หนึ่งเครื่องถึงอย่างนั้น ผมก็คิดว่าคำว่า “คอนเทนเนอร์ไม่ใช่ security boundary” มันง่ายเกินไป คอนเทนเนอร์เป็น security boundary อยู่แน่ เพียงแต่ไม่แข็งแรงเท่าที่เราอยากเชื่อ
สำหรับการ deploy แบบโลคัล เส้นแบ่งนี้จะพร่ามัวขึ้นหน่อย
ในมุมมองฮาร์ดแวร์ VM ไม่ได้ปลอดภัยกว่าพรอเซสโดยเนื้อแท้ แต่มีสามเหตุผลที่ทำให้มันเป็นขอบเขตที่ป้องกันได้มากกว่า
การหนีออกจาก VM เกิดได้น้อยกว่า system call จึงมีพื้นที่ให้ใส่มาตรการลด side channel เพิ่มได้โดยไม่กระทบประสิทธิภาพมาก
อินเทอร์เฟซฝั่งโฮสต์ของ VM นั้นง่ายกว่ามาก อุปกรณ์บล็อกมีอินเทอร์เฟซอ่าน-เขียนเป็นบล็อก และอุปกรณ์เครือข่ายก็ส่งรับเฟรม
คำสั่ง
setsockoptที่ Linux หรือ *BSD ให้กับ socket มีพื้นผิวการโจมตีใหญ่กว่าพวกไดรเวอร์จำลองหรือ paravirtualized ส่วนใหญ่เสียอีก และนั่นก็ยังเป็นเพียงส่วนเล็กมากของพื้นผิวโจมตีทั้งหมดของเคอร์เนลอินเทอร์เฟซของ VM ยังมี state น้อยกว่ามากด้วย มีเพียงทรานแซกชันที่กำลังดำเนินอยู่ใน ring แบบ request-response เป็นหลัก นอกนั้นแทบไม่มี
สิ่งอย่าง credential, UID, GID, ตาราง file descriptor ล้วนเพิ่มความซับซ้อนเชิงสถานะให้กับเคอร์เนล และถ้ามีบั๊ก พรอเซสก็อาจใช้ประโยชน์จากมันได้
ความยากของรูปแบบเวิร์กสเตชันก็คือ มันดึงความซับซ้อนเหล่านี้กลับเข้ามาอีกครั้ง
ตัวอย่างเช่น base layer ของคอนเทนเนอร์อาจถูกเปิดให้เห็นเป็น block device ที่บรรจุไฟล์ซิสเต็มแบบ immutable แต่ volume และ shared folder ก็น่าจะถูกเมานต์ผ่าน 9pfs หรือ VirtIO-FS ซึ่งก็คือ 9p หรือ FUSE บน VirtIO
แบบนั้นพื้นผิวการโจมตีก็จะเพิ่มขึ้นอีก
ถ้าโชคดี อาจต้องใช้เอ็กซ์พลอยต์เป็นลูกโซ่
ผมคุ้นกับฝั่ง FreeBSD มากกว่า ซึ่งโดยทั่วไปองค์ประกอบที่ให้บริการอุปกรณ์ paravirtualized หรือ emulated จะถูก sandbox ด้วย Capsicum ดังนั้นก่อนอื่นต้องยึดโปรเซสบนโฮสต์ให้ได้ และถ้าจะเข้าถึงสิ่งที่ VM เดิมไม่มีสิทธิ์เข้าถึงต่อ ก็ต้องทะลุไปถึงเคอร์เนลอีกชั้น
แต่ถ้าไม่มี sandbox เพิ่มแบบนี้ การหนีจากคอนเทนเนอร์ก็จะกลับไปสู่โลกที่ทำได้ทุกอย่างที่ผู้ใช้คนนั้นทำได้ ซึ่งบนเดสก์ท็อปก็แทบไม่ได้ดีกว่าการที่ root ถูกเจาะเท่าไร
ส่วนตัวผมชอบ gVisor มากกว่า แม้จะไม่ใช่ VMM runtime แต่ก็มีมาหลายปีแล้ว และมีบริษัทอย่าง Tencent ใช้งานอยู่ อีกทั้งยังเข้ากับสภาพแวดล้อมของผมที่รันคอนเทนเนอร์ทั้งหมดภายใน Proxmox VM อยู่แล้ว
อีกตัวที่กำลังทดลองคือ syd-oci ซึ่งดูเหมือนได้รับความสนใจน้อยกว่าตัวเลือกพื้นฐานอย่าง MicroVM หรือ gVisor อยู่บ้าง
ขอบคุณสำหรับข้อมูลอ้างอิงเรื่อง libkrun ด้วย ดูเป็นความเป็นไปได้ที่น่าสนใจ
ก็ดูมีโอกาสสูงที่จะตามมาด้วยการตรวจสอบด้านความปลอดภัยเช่นกัน