เหตุใดกระบวนการในคอนเทนเนอร์ Docker จึงไม่ควรทำงานเป็นรูท

เผยแพร่แล้ว: 2022-08-19

กราฟิกแสดงโลโก้ Docker

ไม่ควรเรียกใช้กระบวนการในคอนเทนเนอร์ Docker เป็นรูท การเรียกใช้แอปพลิเคชันของคุณเป็นผู้ใช้ที่ไม่ใช่รูทนั้นปลอดภัยกว่าซึ่งคุณระบุให้เป็นส่วนหนึ่งของ Dockerfile หรือเมื่อใช้ docker docker run สิ่งนี้ช่วยลดความเสี่ยงด้วยการนำเสนอพื้นผิวการโจมตีที่ลดลงต่อภัยคุกคามใดๆ ในคอนเทนเนอร์ของคุณ

ในบทความนี้ คุณจะได้เรียนรู้เกี่ยวกับอันตรายของการเรียกใช้แอปพลิเคชันที่มีคอนเทนเนอร์เป็นรูท คุณยังจะได้เห็นวิธีสร้างผู้ใช้ที่ไม่ใช่รูทและตั้งค่าเนมสเปซในสถานการณ์ที่ไม่สามารถทำได้

เหตุใดการวิ่งเป็นรูทจึงเป็นอันตราย

คอนเทนเนอร์ทำงานเป็นรูทโดยค่าเริ่มต้น Docker daemon ทำงานเป็นรูทบนโฮสต์ของคุณและคอนเทนเนอร์ที่รันอยู่จะเป็นรูทด้วย

แม้ว่าจะดูเหมือนรูทภายในคอนเทนเนอร์เป็นผู้ใช้อิสระ แต่จริงๆ แล้วเหมือนกับบัญชีรูทบนโฮสต์ของคุณ การแยกสารมีให้โดยกลไกการแยกคอนเทนเนอร์ของ Docker เท่านั้น ไม่มีขอบเขตทางกายภาพที่แข็งแกร่ง คอนเทนเนอร์ของคุณเป็นกระบวนการอื่นที่ดำเนินการโดยผู้ใช้รูทบนเคอร์เนลของโฮสต์ของคุณ นี่หมายถึงช่องโหว่ในแอปพลิเคชันของคุณ รันไทม์ของ Docker หรือเคอร์เนลของ Linux อาจทำให้ผู้โจมตีสามารถแยกตัวออกจากคอนเทนเนอร์และดำเนินการสิทธิ์รูทบนเครื่องของคุณ

มีการป้องกันในตัวที่ช่วยลดความเสี่ยงของเหตุการณ์นี้ รูทภายในคอนเทนเนอร์ไม่มีสิทธิ์และมีความสามารถจำกัด ซึ่งจะป้องกันไม่ให้คอนเทนเนอร์ใช้คำสั่งการดูแลระบบ เว้นแต่คุณจะเพิ่มความสามารถด้วยตนเองหรือใช้โหมดที่มีสิทธิพิเศษเมื่อคุณเริ่มคอนเทนเนอร์

แม้จะมีการบรรเทาผลกระทบนี้ การอนุญาตให้แอปพลิเคชันทำงานในขณะที่รูทยังคงเป็นอันตราย เช่นเดียวกับที่คุณจำกัดการใช้รูทในสภาพแวดล้อมแบบเดิม มันไม่ฉลาดที่จะใช้รูทภายในคอนเทนเนอร์ของคุณโดยไม่จำเป็น คุณกำลังจัดเตรียมสภาพแวดล้อมที่มีสิทธิพิเศษมากเกินไปซึ่งให้ผู้โจมตีตั้งหลักได้มากขึ้นในกรณีที่มีการละเมิดเกิดขึ้น

การรันแอปพลิเคชันคอนเทนเนอร์ในฐานะผู้ใช้ที่ไม่ใช่รูท

แนวทางปฏิบัติที่ดีที่สุดสำหรับแอปพลิเคชันที่มีคอนเทนเนอร์ในการทำงานในฐานะผู้ใช้ทั่วไป ซอฟต์แวร์ส่วนใหญ่ไม่ต้องการการเข้าถึงรูท ดังนั้นการเปลี่ยนแปลงผู้ใช้จึงเป็นชั้นการป้องกันในทันทีจากการแตกคอนเทนเนอร์

คุณควรสร้างบัญชีผู้ใช้ใหม่เป็นหนึ่งในขั้นตอนสุดท้ายใน Dockerfile ของคุณ คุณสามารถทำสิ่งนี้ได้ด้วยคำสั่ง USER :

 จากภาพฐาน:ล่าสุด
รัน apt ติดตั้งแพ็คเกจสาธิต
ผู้ใช้สาธิตผู้ใช้:กลุ่มสาธิต
ENTRYPOINT ["สาธิตไบนารี"]

คอนเทนเนอร์ที่เริ่มต้นจากอิมเมจนี้จะทำงานเป็น demo-user ผู้ใช้จะเป็นสมาชิกของ demo-group ตัวอย่าง คุณสามารถละชื่อกลุ่มได้หากคุณไม่ต้องการให้ผู้ใช้อยู่ในกลุ่ม:

 ผู้ใช้สาธิตผู้ใช้

คุณสามารถระบุ ID ผู้ใช้ (UID) และ ID กลุ่ม (GID) แทนชื่อ:

 ผู้ใช้ 950:950

การจัดสรร UID และ GID ที่รู้จักมักเป็นวิธีที่ปลอดภัยที่สุดในการดำเนินการ ป้องกันไม่ให้ผู้ใช้ในคอนเทนเนอร์จับคู่กับบัญชีโฮสต์ที่มีสิทธิ์เกิน

USER มักถูกระบุเป็นสเตจสุดท้ายใน Dockerfile ซึ่งหมายความว่าคุณยังสามารถเรียกใช้การดำเนินการที่ต้องการรูทก่อนหน้าในบิลด์อิมเมจได้ คำแนะนำในการ apt install ในตัวอย่างด้านบนมีความต้องการรูทที่ถูกต้อง หากวางคำสั่ง USER ไว้ด้านบน apt จะถูกเรียกใช้ในฐานะ demo-user ซึ่งจะไม่มีการอนุญาตที่จำเป็น เนื่องจากคำแนะนำของ Dockerfile มีผลกับการสร้างอิมเมจเท่านั้น ไม่ใช้คอนเทนเนอร์ จึงปลอดภัยที่จะปล่อยให้เปลี่ยนผู้ใช้จนกว่าจะถึงใน Dockerfile ของคุณในภายหลัง

การเปลี่ยนผู้ใช้ที่คอนเทนเนอร์ของคุณทำงาน เนื่องจากคุณอาจต้องอัปเดตสิทธิ์ในไฟล์และโฟลเดอร์ที่เข้าถึง ตั้งค่าความเป็นเจ้าของในเส้นทางที่จะใช้โดยแอปพลิเคชันของคุณ:

 คัดลอก initial-config.yaml /data/config.yaml

ผู้ใช้สาธิตผู้ใช้:กลุ่มสาธิต
เรียกใช้ chown demo-user:demo-group /data

ในตัวอย่างนี้ ไดเร็กทอรี /data จะต้องเป็นเจ้าของโดย demo-user เพื่อให้แอปพลิเคชันสามารถเปลี่ยนแปลงไฟล์ปรับแต่งได้ คำสั่ง COPY ก่อนหน้านี้จะคัดลอกไฟล์เป็นรูท ชวเลขสามารถใช้ได้โดยใช้แฟ --chown พร้อม copy :

 COPY --chown=demo-user:demo-group initial-config.yaml /data/config.yaml

การเปลี่ยนผู้ใช้เมื่อเริ่มต้นคอนเทนเนอร์

แม้ว่าคุณสามารถเปลี่ยนผู้ใช้ใน Dockerfiles ของคุณได้อย่างง่ายดาย แต่แอปพลิเคชันของบุคคลที่สามจำนวนมากยังคงทำงานเป็นรูท คุณสามารถลดความเสี่ยงที่เกี่ยวข้องกับการใช้สิ่งเหล่านี้โดยการตั้งค่า --user ทุกครั้งที่คุณเรียก docker run สิ่งนี้จะแทนที่ผู้ใช้ที่ตั้งไว้ใน Dockerfile ของอิมเมจ

 $ docker run -d --user demo-user:demo-group demo-image:latest
$ docker run -d --user demo-user demo-image:latest
$ docker run -d --user 950:950 demo-image:latest

แฟ --user รันกระบวนการของคอนเทนเนอร์ในฐานะผู้ใช้ที่ระบุ ปลอดภัยน้อยกว่าคำสั่ง Dockerfile USER เพราะคุณต้องนำไปใช้กับคำสั่ง docker docker run ทุกคำสั่ง ตัวเลือกที่ดีกว่าสำหรับรูปภาพที่ใช้เป็นประจำคือการสร้างภาพลอกเลียนแบบของคุณเองซึ่งสามารถตั้งค่าบัญชีผู้ใช้ใหม่ได้:

 จาก image-that-runs-as-root:latest
ผู้ใช้สาธิตผู้ใช้
 $ สร้างนักเทียบท่า -t image-that-now-runs-as-non-root:latest

การเปลี่ยนผู้ใช้อิมเมจบุคคลที่สามอาจทำให้เกิดปัญหาได้ หากคอนเทนเนอร์คาดว่าจะทำงานเป็นรูท หรือจำเป็นต้องเข้าถึงเส้นทางของระบบไฟล์ที่รูทเป็นเจ้าของ คุณจะเห็นข้อผิดพลาดขณะใช้แอปพลิเคชัน คุณสามารถลองเปลี่ยนการอนุญาตบนเส้นทางที่ทำให้เกิดปัญหาได้ด้วยตนเอง อีกทางหนึ่ง ตรวจสอบว่าผู้ขายมีวิธีการที่ได้รับการสนับสนุนสำหรับการเรียกใช้แอปพลิเคชันด้วยบัญชีผู้ใช้ที่ไม่มีสิทธิพิเศษหรือไม่

การจัดการแอปพลิเคชันที่ต้องทำงานเป็นรูท

เนมสเปซผู้ใช้เป็นเทคนิคในการจัดการกับแอปพลิเคชันที่ต้องการสิทธิ์รูทบางอย่าง ช่วยให้คุณสามารถแมปรูทภายในคอนเทนเนอร์กับผู้ใช้ที่ไม่ใช่รูทบนโฮสต์ของคุณ รูทจำลองภายในคอนเทนเนอร์มีสิทธิ์ที่จำเป็น แต่การฝ่าวงล้อมจะไม่ให้สิทธิ์การเข้าถึงรูทแก่โฮสต์

การแมปเนมสเปซใหม่ถูกเปิดใช้งานโดยการเพิ่ม userns-remap ให้กับไฟล์ /etc/docker/daemon.json ของคุณ:

 {
    "userns-remap": "ค่าเริ่มต้น"
}

การใช้ default เป็นค่า userns-remap สั่งให้ Docker สร้างผู้ใช้ใหม่โดยอัตโนมัติบนโฮสต์ของคุณชื่อ dockremap รูทภายในคอนเทนเนอร์จะแมปกลับไปที่ dockremap บนโฮสต์ของคุณ คุณสามารถเลือกระบุผู้ใช้และกลุ่มที่มีอยู่แทน โดยใช้ UID/GID หรือชื่อผู้ใช้/ชื่อกลุ่มรวมกัน:

 {
    "userns-remap": "สาธิตผู้ใช้"
}

รีสตาร์ท Docker daemon หลังจากใช้การเปลี่ยนแปลงของคุณ:

 $ sudo บริการนักเทียบท่าเริ่มต้นใหม่

หากคุณกำลังใช้ nsuser-remap: default ผู้ใช้ dockremap ควรมีอยู่ในโฮสต์ของคุณ:

 $ id dockremap

uid=140(dockremap) gid=119(dockremap) กลุ่ม=119(dockremap)

ผู้ใช้ควรปรากฏในไฟล์ /etc/subuid และ /etc/subgid ID รอง:

 $ dockremap:231500:65535

ผู้ใช้ได้รับการจัดสรรช่วง 65,535 ID รองเริ่มต้นจาก 231500 ภายในเนมสเปซผู้ใช้ ID 231500 จะถูกจับคู่กับ 0 ทำให้เป็นผู้ใช้รูทในคอนเทนเนอร์ของคุณ เนื่องจากเป็น UID ที่มีหมายเลขสูง 231500 ไม่มีสิทธิ์ใดๆ บนโฮสต์ ดังนั้นการโจมตีแบบแยกส่วนคอนเทนเนอร์จึงไม่สามารถสร้างความเสียหายได้มาก

คอนเทนเนอร์ทั้งหมดที่คุณเริ่มต้นจะทำงานโดยใช้เนมสเปซผู้ใช้ที่ทำการแมปใหม่ เว้นแต่คุณจะเลือกไม่ docker run --userns=host กลไกทำงานโดยการสร้างไดเรกทอรีเนมสเปซภายใน /var/lib/docker ที่เป็นเจ้าของโดย UID รองและ GID ของผู้ใช้เนมสเปซ:

 $ sudo ls -l /var/lib/docker/231500.231500

รวม 14
drwx------ 5 231500 231500 13 ก.ค. 22 19:00 aufs
drwx------ 3 231500 231500 13 ก.ค. 22 19:00 ตู้คอนเทนเนอร์
...

การเนมสเปซผู้ใช้เป็นวิธีที่มีประสิทธิภาพในการเพิ่มการแยกคอนเทนเนอร์ หลีกเลี่ยงการแบ่งส่วน และรักษาความเข้ากันได้กับแอปพลิเคชันที่ต้องการสิทธิ์รูท มีข้อเสียอยู่บ้าง: ฟีเจอร์นี้ทำงานได้ดีที่สุดบนอินสแตนซ์ Docker เวอร์ชันใหม่ โวลุ่มที่ติดตั้งจากโฮสต์ต้องได้รับการปรับสิทธิ์ และไดรเวอร์ที่เก็บข้อมูลภายนอกบางตัวไม่รองรับการทำแผนที่ผู้ใช้เลย คุณควรตรวจสอบเอกสารก่อนที่จะใช้ตัวเลือกนี้

สรุป

การเรียกใช้แอปพลิเคชันที่มีคอนเทนเนอร์เป็นรูทนั้นมีความเสี่ยงด้านความปลอดภัย แม้ว่าจะมองข้ามได้ง่าย แต่การแยกโดยคอนเทนเนอร์นั้นไม่แข็งแรงพอที่จะแยกผู้ใช้เคอร์เนลออกจากผู้ใช้คอนเทนเนอร์ได้อย่างสมบูรณ์ รูทในคอนเทนเนอร์นั้นเหมือนกับรูทบนโฮสต์ของคุณ ดังนั้นการประนีประนอมที่ประสบความสำเร็จจึงสามารถให้การควบคุมเครื่องของคุณได้

ในฐานะผู้สร้างรูปภาพ คุณควรรวมคำสั่ง USER ไว้ใน Dockerfile เพื่อให้แอปพลิเคชันของคุณทำงานโดยไม่ต้องรูท ผู้ใช้อิมเมจสามารถแทนที่สิ่งนี้ด้วย docker run --user เพื่อกำหนด UID และ GID เฉพาะ ซึ่งช่วยลดกรณีที่รูปภาพใช้รูทตามปกติ

คุณสามารถเพิ่มการรักษาความปลอดภัยให้แน่นยิ่งขึ้นด้วยการปล่อยความสามารถทั้งหมดจากคอนเทนเนอร์โดยใช้ --cap-drop=ALL จากนั้นให้อนุญาตรายการที่จำเป็นด้วย --cap-add แฟล็ก การรวมเทคนิคเหล่านี้จะเรียกใช้แอปพลิเคชันของคุณในฐานะผู้ใช้ที่ไม่ใช่รูทด้วยชุดสิทธิ์ขั้นต่ำที่จำเป็น ปรับปรุงระดับความปลอดภัยของคุณ