วิธีรัน service ด้วย unprivileged user
ลดความเสี่ยงจากการถูกเจาะระบบด้วยหลักการ least privilege โดยการสร้าง system user แยกเฉพาะ หรือใช้ DynamicUser ของ systemd เพื่อจำกัดสิทธิ์ไม่ให้เป็น root
ทำไมไม่ควรเรียกใช้งานทุกอย่างด้วย root
root สามารถทำทุกอย่างบนเครื่องได้ ไม่ว่าจะเป็นการอ่านไฟล์ทุกไฟล์ การเปลี่ยนการตั้งค่าใดๆ หรือการลบระบบทั้งหมดทิ้ง หากคุณเรียกใช้งาน service ด้วย root คุณกำลังมอบอำนาจทั้งหมดนั้นให้แก่ service นั้น หาก service มี bug ที่ผู้โจมตีสามารถใช้ช่องโหว่ได้ พวกเขาจะไม่ได้รับเพียงแค่สิทธิ์ใน service เท่านั้น แต่จะได้สิทธิ์ root ซึ่งหมายถึงการควบคุมทั้ง server หากรันด้วย unprivileged user จะเป็นการจำกัดความเสียหาย หากเกิด bug ใน service ที่รันด้วย account ที่มีสิทธิ์จำกัด ผู้โจมตีจะเข้าถึงได้เพียงสิ่งที่ account นั้นเข้าถึงได้เท่านั้น ซึ่งควรจะเป็นสิ่งที่น้อยที่สุด
นี่คือหลักการ least privilege: มอบสิทธิ์การเข้าถึงให้แต่ละส่วนของระบบเท่าที่จำเป็นต่อการทำงานเท่านั้น และไม่เกินกว่านั้น นี่คือวิธีที่มีประสิทธิภาพที่สุดในการจำกัด blast radius ของการถูกเจาะระบบ และสำหรับการใช้งานบน server สมัยใหม่ การทำเช่นนี้แทบไม่มีค่าใช้จ่ายใดๆ
การใช้ account เฉพาะสำหรับแต่ละ service
วิธีมาตรฐานคือการสร้าง system user แยกกันสำหรับแต่ละ service โดย user นั้นต้องเป็นเจ้าของเฉพาะไฟล์ของ service นั้นๆ และไม่สามารถ login ได้ ตัวอย่าง account สำหรับ web app อาจเป็นดังนี้:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appsvcทุก flag มีความสำคัญ --system ทำให้เป็น service account ไม่ใช่ user สำหรับ login ของมนุษย์ --no-create-home ข้ามการสร้าง home directory ที่ไม่จำเป็น --shell /usr/sbin/nologin หมายความว่าแม้ผู้โจมตีจะยึด account ได้ แต่ก็ไม่สามารถเปิด shell ได้ Account นี้มีไว้เพื่อเป็นเจ้าของ process และไฟล์ของมันเท่านั้น
จากนั้นให้สิทธิ์ user นั้นเข้าถึงเฉพาะไฟล์ที่จำเป็นเท่านั้น:
sudo chown -R appsvc:appsvc /opt/myappตอนนี้ service จะสามารถอ่านและเขียนไฟล์ใน directory ของตัวเองได้ และไม่มีสิทธิ์ไปยุ่งกับส่วนอื่นบน disk หากถูกเจาะระบบ ไฟล์ที่ผู้โจมตีสามารถแก้ไขได้จะถูกจำกัดอยู่ที่ /opt/myapp โดย account ยังคงสามารถอ่านไฟล์ใดๆ ที่เปิดให้คนทั่วไปอ่านได้ (world-readable) แต่ไม่สามารถแก้ไขส่วนที่เหลือของระบบได้
ให้ systemd รันด้วย user นั้น
เมื่อสร้าง account แล้ว ให้สั่งให้ systemd รัน service ด้วย account นั้น ใน unit file สามารถทำได้ด้วยบรรทัดเดียว:
[Service]
ExecStart=/opt/myapp/bin/server
User=appsvc
Group=appsvcUser=appsvc หมายถึง process จะเริ่มต้นด้วยสิทธิ์ที่จำกัดของ account นั้นแทนที่จะเป็น root นี่คือวิธีมาตรฐานที่ใช้กันทั่วไปในการรัน application ภายใต้ systemd และควรทำสำหรับทุก service ที่คุณเขียน unit file ขึ้นมา
หรือข้ามการสร้าง account ไปเลยด้วย DynamicUser
systemd สามารถทำได้มากกว่านั้นโดยการสร้าง throwaway user ให้คุณ ซึ่งจะมีอยู่เฉพาะในขณะที่ service กำลังทำงาน หากตั้งค่า DynamicUser=yes คุณจะไม่ต้องจัดการ account ใดๆ เลย:
[Service]
ExecStart=/opt/myapp/bin/server
DynamicUser=yes
StateDirectory=myappเมื่อเริ่มทำงาน systemd จะจัดสรร user ID ที่ไม่ได้ใช้งาน และเมื่อหยุดทำงานจะคืนค่า ID นั้น บริการจะได้รับ /tmp ส่วนตัว ซึ่งเป็น view ของ filesystem ส่วนใหญ่แบบ read-only และมี state directory ที่เขียนได้ภายใต้ /var/lib/myapp ซึ่ง StateDirectory= เป็นผู้จัดเตรียมและส่งมอบให้ สำหรับ service ที่ทำงานแบบ self-contained และต้องการเพียง state directory ของตัวเองเท่านั้น DynamicUser=yes คือวิธีที่ใช้ความพยายามน้อยที่สุดเพื่อให้ได้การแยกส่วน (isolation) ที่แข็งแกร่ง เพราะไม่มี account ที่มีอายุการใช้งานยาวนานให้ผู้โจมตีใช้เป็นเป้าหมาย
การเขียน unit file ด้วยมืออาจมีความยุ่งยาก และการตั้งค่า hardening directives ให้ถูกต้องคือส่วนที่สำคัญที่สุด generator ใน the systemd service and timer guide สามารถช่วยเติม option เหล่านี้ให้คุณเพื่อให้ unit file ถูกต้องตั้งแต่ครั้งแรก
การทำงานร่วมกับส่วนอื่นๆ
Least privilege เป็นเพียงหนึ่งในชั้นของการป้องกัน และทำงานร่วมกับชั้นอื่นๆ แทนที่จะมาแทนที่กัน default-deny firewall ควบคุมสิ่งที่สามารถเข้าถึง service ได้ การรันด้วย unprivileged user ควบคุมสิ่งที่ service สามารถทำได้หากถูกเจาะระบบ และ hardened SSH ช่วยป้องกันไม่ให้ผู้โจมตีเข้าถึงเครื่องได้ตั้งแต่แรก ไม่มีวิธีใดวิธีหนึ่งที่เพียงพอ แต่เมื่อใช้ร่วมกันจะทำให้ bug ใน service หนึ่งไม่กลายเป็นการถูกเจาะระบบทั้ง server
ก่อนจะดำเนินการต่อ ให้ตรวจสอบ hardening checklist สำหรับทั้งเครื่องและสร้างสำเนาส่วนตัวเพื่อใช้งาน:
FAQ
Why should I not run a service as root?
เพราะ root สามารถทำทุกอย่างบนเครื่องได้ หาก service ที่รันด้วย root ถูกเจาะระบบ ผู้โจมตีจะได้สิทธิ์ควบคุมทั้ง server ไม่ใช่แค่ตัว service การรัน service ด้วย limited, unprivileged account จะจำกัดความเสียหายไว้เพียงแค่สิ่งที่ account นั้นเข้าถึงได้เท่านั้น ควรเก็บ root ไว้สำหรับการบริหารจัดการ และรันทุก service ที่ทำงานต่อเนื่องด้วย restricted user
How do I create a user that cannot log in?
รันคำสั่ง sudo useradd --system --no-create-home --shell /usr/sbin/nologin NAME โดย nologin shell หมายถึง account นั้นไม่สามารถเปิด interactive session ได้แม้จะถูกขโมย credentials ไปก็ตาม --system จะระบุว่าเป็น service account และ --no-create-home จะข้ามการสร้าง home directory ที่ไม่จำเป็น ให้สิทธิ์ความเป็นเจ้าของเฉพาะไฟล์ของตัวเองด้วย chown
What is systemd DynamicUser?
DynamicUser=yes บอกให้ systemd สร้าง user ชั่วคราวสำหรับ service ซึ่งจะมีอยู่เฉพาะในขณะที่ service ทำงานเท่านั้น ทำให้คุณไม่ต้องจัดการ account ที่มีอายุการใช้งานยาวนาน นอกจากนี้ยังให้ /tmp ส่วนตัวแก่ service ซึ่งเป็น filesystem view ที่เกือบทั้งหมดเป็นแบบ read-only และมี managed state directory นี่คือวิธีที่ง่ายที่สุดในการรัน self-contained service ภายใต้ identity ที่มีสิทธิ์ต่ำและเป็นแบบ throwaway
Does running as a non-root user replace a firewall?
ไม่ เพราะทั้งสองอย่างป้องกันคนละส่วนกัน การรันด้วย unprivileged user จำกัดสิ่งที่ service สามารถทำได้หากถูกเจาะระบบ ในขณะที่ firewall จำกัดสิ่งที่สามารถเข้าถึง service ได้ ควรใช้ทั้งสองอย่างร่วมกับ hardened SSH เพื่อให้แต่ละชั้นป้องกันในส่วนที่ชั้นอื่นทำไม่ได้
What files should the service user own?
เฉพาะไฟล์ที่ service จำเป็นต้องใช้เท่านั้นและไม่เกินกว่านั้น ให้สิทธิ์ความเป็นเจ้าของแก่ account สำหรับ working directory และข้อมูลของมันเอง และปล่อยให้ไฟล์อื่นๆ เป็นของ root รูปแบบที่ดีคือใช้ sudo chown -R svc-app:svc-app /opt/svc-app สำหรับ application directory ในขณะที่ไฟล์คอนฟิกภายใต้ /etc ควรเป็นของ root และให้ service อ่านได้เท่านั้น เป้าหมายคือหาก process ถูกเจาะระบบ ไฟล์ที่ผู้โจมตีสามารถแก้ไขได้จะถูกจำกัดอยู่เพียงข้อมูลของมันเอง ไม่ใช่ส่วนที่เหลือของระบบ