rootless Docker ปลอดภัยขึ้นจริง แต่สิทธิ์ root ไม่ได้หายไป มันย้ายไปอยู่ใน user namespace
rootless Docker ย้าย daemon ออกจาก root ได้จริง ส่วนสิทธิ์ไม่ได้หายไป แต่ย้ายไปอยู่ใน user namespace มาดูกันว่าต้องแลกกับอะไรบ้าง

ระบบจัดการคอนเทนเนอร์อย่าง rootless Docker ช่วยให้เราไม่ต้องรัน Docker daemon ด้วยสิทธิ์ root บนเครื่องอีกต่อไป ชื่อของมันฟังดูเหมือนปิดจ็อบเรื่องความปลอดภัยไปแล้วในตัว แต่คำว่า "rootless" บอกแค่ว่า daemon ทำงานภายใต้บัญชีผู้ใช้ธรรมดา ไม่ได้แปลว่าสิทธิ์ root จะหายไปจากระบบ
เพราะการสร้าง container ยังต้องพึ่งพาคำสั่งและการควบคุมระดับเคอร์เนลเหมือนเดิม สิทธิ์เหล่านั้นไม่ได้หายไปไหน เพียงแต่ย้ายเข้าไปอยู่ใน user namespace แทน ซึ่งก็มาพร้อมกับความเสี่ยงชุดใหม่ บทความสองตอนของ Ken Muse อธิบายเรื่องนี้ไว้อย่างละเอียด ตั้งแต่เหตุผลว่าทำไม Docker แบบเดิมต้องใช้ root ไปจนถึงสิ่งที่เราต้องแลกเมื่อเปลี่ยนมาใช้ rootless คำถามสำคัญจึงไม่ใช่แค่ "ควรใช้หรือไม่" แต่เป็น "เรารู้ตัวไหมว่ากำลังแลกอะไรไปบ้าง"
root ตัวจริงไม่ได้อยู่ที่คำสั่ง docker
เวลาเราพิมพ์คำสั่ง docker run ตัวโปรแกรม docker ที่เรารันทำงานด้วยสิทธิ์ของผู้ใช้ทั่วไปตามปกติ แต่ตัวที่ทำงานจริงอยู่ลึกลงไปอีกชั้นคือ dockerd ซึ่งทำงานอยู่เบื้องหลังด้วยสิทธิ์ root แล้วส่งต่องานระดับล่างให้ containerd และ runc จัดการอีกที โดยฝั่ง client กับ daemon จะสื่อสารกันผ่าน socket file ที่ /var/run/docker.sock
ls -la /var/run/docker.sock
srw-rw---- 1 root docker 0 Apr 4 18:06 /var/run/docker.sockตัวอักษร s หน้าสุดบอกว่านี่คือ UNIX domain socket เจ้าของไฟล์คือ root และกลุ่มของไฟล์คือ docker สิทธิ์ rw-rw---- หรือรหัส 660 หมายความว่ามีแค่ root กับผู้ใช้ในกลุ่ม docker เท่านั้นที่อ่านและเขียนไฟล์นี้ได้ สำหรับใครที่ยังไม่คุ้นกับการอ่านสิทธิ์ไฟล์แบบนี้ สามารถอ่านพื้นฐานได้ที่บทความ บรรทัดคำสั่ง Linux ฉบับมือใหม่
ผลกระทบของสิทธิ์นี้ร้ายแรงกว่าที่คิด เพราะในทางปฏิบัติ ใครก็ตามที่อยู่ในกลุ่ม docker จะมีสิทธิ์เทียบเท่า root บนเครื่องทันที เนื่องจากสามารถสั่งรัน container ที่ดึง root filesystem (/) ของเครื่องแม่เข้ามา แล้วเข้าไปแก้ไขไฟล์ระบบอะไรก็ได้ จุดที่พลาดง่ายอยู่ตรงนี้ การเพิ่มผู้ใช้เข้ากลุ่ม docker จึงไม่ใช่แค่การให้สิทธิ์ใช้ Docker แต่คือการมอบสิทธิ์ root ของเครื่องให้ไปเลย
ทำไมสุดท้ายแล้วต้องพึ่งสิทธิ์ root
ความจริงแล้ว container ไม่ได้แยกขาดออกมาเหมือน Virtual Machine แต่เป็น process ธรรมดาบน Linux ที่ถูกจำกัดขอบเขตด้วย namespace หลายประเภทรวมกัน ทั้ง namespace ของ PID, จุดเชื่อมต่อระบบไฟล์ (mount), เครือข่าย และชื่อเครื่อง (UTS)
การสร้าง namespace แต่ละชนิด ปกติต้องอาศัย capabilities ของ Linux โดยต้องใช้สิทธิ์อย่าง CAP_SYS_ADMIN ขึ้นไป เพราะ Linux ไม่ได้มองสิทธิ์ root เป็นแค่เปิดหรือปิด แต่แบ่งสิทธิ์ออกเป็นความสามารถย่อยกว่า 40 รายการ เช่น CAP_NET_ADMIN สำหรับตั้งค่าเครือข่าย หรือ CAP_SYS_CHROOT สำหรับเปลี่ยน root filesystem นี่ไม่ใช่ข้อจำกัดของ Docker แต่เป็นกลไกความปลอดภัยของเคอร์เนล Linux เอง หากลองใช้ผู้ใช้ธรรมดาสร้าง namespace ตรงๆ เคอร์เนลจะปฏิเสธทันที:
unshare --pid --fork --mount-proc bash -c 'ps aux'
unshare: unshare failed: Operation not permittedคำสั่ง unshare ใช้สำหรับสร้าง namespace ใหม่ ซึ่งเคอร์เนลจะตอบกลับมาทันทีว่า Operation not permitted
ฝั่งการ build image ก็เจอปัญหาเดียวกัน เพราะเครื่องมือ build มาตรฐานของ Docker ตั้งแต่เวอร์ชัน 23.0 อย่าง BuildKit มองทุกคำสั่ง RUN ใน Dockerfile เป็นการสร้าง container ใหม่ขึ้นมารันคำสั่ง บันทึกส่วนต่างของไฟล์เป็นเลเยอร์ แล้วลบ container นั้นทิ้ง ซึ่งการซ้อนเลเยอร์แบบนี้ก็ต้องใช้ CAP_SYS_ADMIN เช่นกัน ที่ผ่านมาจึงกลายเป็นว่า ถ้า Docker daemon มีช่องโหว่ หรือมีใคร escape หลุดออกจาก container ได้ ผู้โจมตีก็จะได้สิทธิ์ root ของเครื่องแม่ไปทันทีโดยไม่ต้องทำอะไรเพิ่ม
rootless ย้าย daemon มาอยู่ในบัญชีผู้ใช้ของคุณ

ในโหมด rootless ตัว dockerd จะรันด้วยบัญชีผู้ใช้ธรรมดา และไม่มี daemon กลางตัวเดียวทั้งเครื่องอีกต่อไป ใครต้องการใช้ Docker ก็รัน daemon ภายใต้บัญชีของตัวเอง พร้อม socket แยกต่างหากที่ $XDG_RUNTIME_DIR/docker.sock (เช่น /run/user/1000/docker.sock) แทน /var/run/docker.sock แบบเดิม
คำถามคือ เมื่อ daemon รันภายใต้บัญชีผู้ใช้ธรรมดา จะเอาสิทธิ์จากไหนมาสร้าง namespace?
คำตอบคือ user namespace ซึ่งเป็น namespace เพียงชนิดเดียวที่ Linux อนุญาตให้ผู้ใช้ทั่วไปสร้างขึ้นเองได้ แต่เคอร์เนลก็ไม่ได้ให้สิทธิ์มาฟรีๆ ถ้าเราสร้าง user namespace ขึ้นมาลอยๆ โดยไม่ทำ UID mapping (จับคู่รหัสประจำตัวผู้ใช้) ผู้ใช้ข้างในจะมีค่า UID เป็น 65534 หรือ nobody ไม่ใช่ root
เครื่องมือที่เข้ามาช่วยจัดการเรื่องนี้คือ RootlessKit ซึ่งทำหน้าที่สร้าง user namespace ขึ้นมาก่อน แล้วเรียกโปรแกรมระบบอย่าง newuidmap และ newgidmap มาช่วยจับคู่ UID และ GID ระหว่างเครื่องจริงกับใน namespace โปรแกรมสองตัวนี้ติดตั้งมาพร้อมแฟลกพิเศษอย่าง setuid จึงทำงานด้วยสิทธิ์ root ชั่วคราวเพื่อจับคู่สิทธิ์ให้เสร็จได้ เมื่อจับคู่เรียบร้อยแล้ว เคอร์เนลจึงยอมให้สร้าง namespace ชนิดอื่นซ้อนอยู่ข้างใน user namespace ได้
การจับคู่สิทธิ์นี้คือหัวใจสำคัญ ตัวอย่างเช่น UID 0 ข้างใน container จะจับคู่เข้ากับ UID 1000 ซึ่งเป็นผู้ใช้จริงบนเครื่องแม่ ส่วน UID 1 ถึง 65535 จะจับคู่เข้ากับช่วงย่อยที่กำหนดไว้ในไฟล์ /etc/subuid ผลลัพธ์คือไฟล์ที่สร้างขึ้นมา ถ้ามองจากข้างใน container จะเห็นเจ้าของเป็น root แต่ถ้ามองจากเครื่องแม่ภายนอก ไฟล์นั้นจะเป็นของผู้ใช้ของเราเอง สิทธิ์ root ข้างใน container กับสิทธิ์ root ของเครื่องจริงจึงเป็นคนละตัวกันตั้งแต่ต้น
สิ่งที่ดีขึ้นอย่างเห็นได้ชัด
ถ้า Docker daemon ถูกเจาะ ผู้โจมตีจะได้สิทธิ์เพียงแค่ผู้ใช้ธรรมดาที่รัน daemon นั้น ไม่ใช่ root ของเครื่อง และถ้าหลุดออกจาก container ได้ แม้ข้างในจะเป็น UID 0 แต่ทันทีที่หลุดออกมายังเครื่องแม่ ก็จะมีสิทธิ์เท่ากับผู้ใช้ธรรมดาคนหนึ่งเท่านั้น
นั่นหมายความว่า ผู้โจมตีจะไม่สามารถโหลด kernel module, แก้ไขไฟล์ระบบ หรือเข้าถึงข้อมูลของผู้ใช้อื่นบนเครื่องได้ ขอบเขตความเสียหายจึงลดลงอย่างมาก เมื่อเทียบกับการปล่อยให้ daemon รันด้วยสิทธิ์ root ค้างไว้ตลอดเวลา โหมด rootless จึงช่วยยกระดับความปลอดภัยได้ดีกว่าอย่างชัดเจนในหลายสถานการณ์
ข้อจำกัดชุดแรก: สิ่งที่ต้องแลกแม้ยังไม่ถูกโจมตี
เรื่องแรกที่ต้องแลกยังไม่ใช่ความปลอดภัย แต่เป็นความเข้ากันได้และประสิทธิภาพในการทำงาน:
- เคอร์เนลต้องอนุญาตก่อน: เครื่องต้องเปิดให้ผู้ใช้ทั่วไปสร้าง user namespace ได้ผ่านการตั้งค่า
kernel.unprivileged_userns_cloneซึ่งบน Ubuntu 24.04 ขึ้นไป ระบบควบคุมความปลอดภัยอย่าง AppArmor จะปิดฟีเจอร์นี้ไว้เป็นค่าเริ่มต้น อย่างไรก็ตาม ระบบปฏิบัติการตระกูล Linux หรือ Linux distribution รุ่นใหม่ๆ ส่วนใหญ่เปิดใช้งานเป็นปกติอยู่แล้วเพราะเบราว์เซอร์และ container ต้องใช้ และในปี 2025 ทางระบบจัดการคลัสเตอร์คอนเทนเนอร์อย่าง Kubernetes ก็ได้เปิดใช้งาน user namespace เป็นค่าเริ่มต้น ตามมา - ระบบไฟล์แบบเลเยอร์ทำงานช้าลง: ในอดีต rootless มักใช้
overlayfsของเคอร์เนลรวมเลเยอร์ไฟล์โดยตรงไม่ได้ เพราะติดสิทธิ์CAP_SYS_ADMINจึงต้องเปลี่ยนไปใช้ไดรเวอร์จำลองอย่างfuse-overlayfsใน userspace ซึ่งทำงานช้ากว่าเล็กน้อย แต่ตั้งแต่เคอร์เนล Linux 5.11 ขึ้นไป ก็ใช้ overlayfs ใน user namespace ได้แล้ว - ระบบเครือข่ายต้องจำลองขึ้นมาเอง: ไดรเวอร์เชื่อมต่อเครือข่ายแบบ bridge มาตรฐานของ Docker ใช้งานไม่ได้ เพราะต้องใช้สิทธิ์เครือข่ายระดับ root โหมด rootless จึงต้องใช้
slirp4netnsหรือpastaจำลอง network stack ทั้งหมดใน userspace แทน ทำให้เครือข่ายช้าลง - ไม่สามารถรัน container แบบ privileged ได้: ตัวเลือก
--privilegedที่ใช้มอบสิทธิ์ของเครื่องแม่ให้ container จะใช้ไม่ได้ เพราะ daemon ใน user namespace ไม่มีสิทธิ์ root จริงของเครื่องแม่อยู่แล้ว จึงไม่มีสิทธิ์ที่จะส่งต่อให้ container
user namespace เปิดช่องโจมตีใหม่ในเคอร์เนล
สิ่งที่ต้องแลกข้อที่สองลึกกว่าเรื่องความสะดวก เพราะตัว user namespace เองก็เปิด attack surface ใหม่ขึ้นมาในระดับเคอร์เนล
เมื่อ process ทำงานอยู่ข้างใน user namespace ก็จะได้รับ capabilities ครบชุดภายใน namespace นั้น รวมถึง CAP_SYS_ADMIN และ CAP_NET_ADMIN ด้วย ส่งผลให้สามารถเข้าถึงฟังก์ชันระดับลึกของเคอร์เนลได้ เช่น API สำหรับตั้งค่าเครือข่าย, ระบบเชื่อมต่อพื้นที่จัดเก็บข้อมูล และไฟร์วอลล์อย่าง iptables ซึ่งแต่เดิมมีเพียง root จริงของระบบเท่านั้นที่เข้าถึงได้
ปัญหาคือ ซอร์สโค้ดเคอร์เนลจำนวนมากเขียนขึ้นบนสมมติฐานว่า "คนที่เรียกใช้งานคือ root ที่น่าเชื่อถือ" การทำ input validation ในส่วนนั้นจึงอาจไม่เข้มงวด พอเปิดให้ผู้ใช้ทั่วไปเรียกใช้โค้ดชุดเดิมผ่าน user namespace ได้ บั๊กเดิมที่เคยซ่อนอยู่ ไม่ว่าจะเป็น buffer overflow ใน network stack, use-after-free ในระบบเชื่อมต่อพื้นที่จัดเก็บ หรือข้อผิดพลาดใน iptables จึงกลายเป็นช่องโหว่ Privilege Escalation สำหรับยกระดับสิทธิ์เข้ายึดระบบได้จริง
Andy Lutomirski ผู้พัฒนา Linux kernel ที่ร่วมดูแล user namespace ได้ให้ความเห็นไว้บน LWN ถึงความเสี่ยงจากการเปิดให้ผู้ใช้ทั่วไปเข้าถึง iptables ผ่าน user namespace ว่า:
มั่นใจได้เลยว่าในนั้นต้องมีช่องโหว่ที่ใช้ยกระดับสิทธิ์ซ่อนอยู่แน่ๆ
เรื่องนี้ไม่ได้อยู่แค่ในเชิงทฤษฎี ในปี 2025 ทีมวิจัยความปลอดภัยจาก Qualys เปิดเผยเทคนิค 3 วิธีที่ข้ามข้อจำกัดด้าน namespace บน Ubuntu จนได้สิทธิ์ root เต็มรูปแบบ นอกจากนี้ในปี 2025 และ 2026 ยังมีรายงาน CVE อีกหลายรายการที่อาศัยช่องโหว่ใน user namespace เพื่อเข้ายึดเครื่องแม่
unconfined สองตัวที่สาย CI ต้องรู้ว่ากำลังปิดอะไร

เวลาที่เราต้อง build container image แบบ rootless ซ้อนใน container อีกทีหนึ่ง เช่น ภายใน pod ของ Kubernetes เรามักจะพบว่าต้องใส่ seccomp=unconfined และบน Ubuntu 24.04 ขึ้นไป ยังต้องใส่ apparmor=unconfined หรือตั้งค่า kernel.apparmor_restrict_unprivileged_userns=0 เพิ่มเติม ค่าเหล่านี้ดูเหมือนเป็นแค่ flag เล็กๆ แต่ความจริงคือการปิดเกราะป้องกันสำคัญที่ Docker เปิดไว้ให้ทุก container เป็นค่าเริ่มต้น:
- seccomp (Secure Computing Mode): ระบบความปลอดภัยที่กรอง system call ซึ่งเป็นคำสั่งที่โปรแกรมใช้สื่อสารกับเคอร์เนล เช่น เปิดไฟล์ ขอหน่วยความจำ หรือสร้าง process โปรไฟล์มาตรฐานของ Docker จะบล็อกคำสั่งที่เสี่ยงอันตรายไว้กว่า 40 รายการ รวมถึงคำสั่ง
unshareและmountที่โหมด rootless ต้องใช้ด้วย - AppArmor: เป็นโมดูลความปลอดภัยของ Linux ที่คอยควบคุมว่าโปรแกรมสามารถเข้าถึงไฟล์ เครือข่าย หรือ capabilities ใดได้บ้างตามโปรไฟล์ที่กำหนด
seccomp ทำหน้าที่คุมว่าโปรแกรม "คุยอะไรกับเคอร์เนลได้บ้าง" ส่วน AppArmor ทำหน้าที่คุมว่าโปรแกรม "เข้าถึงทรัพยากรไหนได้บ้าง" เมื่อเราปิดทั้งสองตัวด้วยค่า unconfined process ใน container ก็สามารถส่ง system call ใดๆ ไปยังเคอร์เนลได้โดยตรง รวมถึงคำสั่งที่เคยถูกบล็อกไว้แต่แรก
สิ่งที่เกิดขึ้นจริงจึงเป็นการย้ายความเสี่ยง จากเดิมที่มี daemon รันด้วย root กลายมาเป็น process ที่ไม่ใช่ root แต่ไม่มีเกราะป้องกันระหว่างตัวมันกับเคอร์เนลหลงเหลืออยู่เลย หากงานที่รันเป็นโค้ดภายในทีมของเราเองทั้งหมด การยอมปิดระบบป้องกันเพื่อแลกกับความสะดวกอาจเป็นสิ่งที่รับได้ แต่ถ้าต้องรันโค้ดจากภายนอกที่ไม่ทราบที่มา การปลดเกราะป้องกันทั้งสองชั้นนี้ก็คือการเปิดรับความเสี่ยงเข้ามาเต็มๆ
สิทธิ์ต้องมาจากที่ใดที่หนึ่งเสมอ
ตัวอย่างที่เห็นภาพชัดที่สุดคือการใช้ Actions Runner Controller (ARC) ของ GitHub เพื่อรัน CI/CD runner บน Kubernetes โดยทีมที่ต้อง build image ด้วยคำสั่ง docker มักพยายามหลีกเลี่ยงการรัน Docker-in-Docker (DinD) เพราะไม่อยากรัน container แบบ privileged
แต่การ build image ตาม Dockerfile ให้สำเร็จ ระบบต้องได้สิทธิ์มาจากทางใดทางหนึ่งเสมอ:
- การให้สิทธิ์เข้าถึง Kubernetes API Server ในขอบเขตที่กว้างพอ เพื่อให้ runner สั่งสร้าง container ใหม่บน node อื่นได้
- หรือการติดตั้ง Docker daemon แบบ privileged ไว้บน node เพื่อให้มีสิทธิ์สร้างและจัดการ container บน node นั้นโดยตรง
ทั้งสองวิธีต่างมีความเสี่ยงในตัวเอง ถ้าตั้งค่าไม่รัดกุมก็อาจเปิดช่องให้ผู้ไม่หวังดีเข้าควบคุม node หรือยึดได้ทั้ง cluster เหมือนกัน
การ build container image ย่อมต้องอาศัยสิทธิ์ในการทำงานเสมอ เราไม่สามารถทำให้สิทธิ์หายไปได้ สิ่งที่เลือกได้มีเพียงว่าจะให้สิทธิ์นั้นมาจากทางไหน และจะควบคุมขอบเขตให้แคบแค่ไหน
สามคำถามที่ตอบได้ตั้งแต่วันนี้
ถ้าปัจจุบันใช้งาน rootless Docker อยู่แล้ว มี 3 คำถามสำคัญที่สามารถนำไปตรวจสอบกับระบบได้ทันทีโดยไม่ต้องรื้อระบบใหม่:
- ตอนนี้เราปิดระบบความปลอดภัยอะไรไปบ้างเพื่อให้รันได้?: ลองตรวจสอบดูว่ามี
seccomp=unconfinedหรือapparmor=unconfinedอยู่ในคอนฟิกหรือไม่ และแต่ละจุดใส่ไว้ด้วยเหตุผลอะไร - เคอร์เนลของเครื่องแม่อัปเดตแพตช์ล่าสุดเมื่อไร?: ช่องโหว่ส่วนใหญ่ในกลุ่มนี้อาศัยบั๊กในเคอร์เนลเป็นหลัก การปล่อยให้เครื่องใช้เคอร์เนลเวอร์ชันเก่าที่ยังไม่แพตช์ ก็เหมือนกับการเปิดประตูทิ้งไว้
- โค้ดที่รันอยู่ข้างในน่าเชื่อถือแค่ไหน?: โค้ดที่พัฒนาขึ้นเองภายในทีมกับโค้ดที่ดึงมาจากภายนอก ต้องการการตั้งค่าความปลอดภัยคนละระดับกัน
rootless Docker ไม่ได้ลบสิทธิ์ root ให้หายไปจากระบบ แต่แค่เปลี่ยนคำถามจาก "ใครคือ root บนเครื่องนี้" ไปเป็น "เคอร์เนลของเครื่องนี้ไว้ใจคนที่เรียกใช้งานมากแค่ไหน"
ที่มา:
- บทความ Rootless Docker and Its Hidden Security Trade-Offs จาก Ken Muse
- บทความ How Docker Uses Root Privileges จาก Ken Muse
ชอบเรื่องแนวนี้ มีอีบุ๊คฟรีให้อ่านต่อ
NotebookLM ฉบับเข้าใจง่าย โยนเอกสารให้ AI อ่าน แล้วได้สรุป พอดแคสต์ และคลังความรู้ส่วนตัว
กดสมัครแล้วเราจะส่งเทคนิค AI และของแจกใหม่ๆ ให้ทางอีเมล เลิกรับได้ตลอด
Claude Cowork · The Business Playbook

ฉบับภาษาไทย 15 บท เรียนรู้ผ่านโปรเจกต์จำลองต่อเนื่องทั้งเล่ม ตั้งแต่ตั้งค่า Workspace จัดการไฟล์ เชื่อมแอป ตั้งระบบอัตโนมัติ จนถึงสร้าง Plugin


