เสริมความปลอดภัยให้บริการ systemd (Hardening)
(roguesecurity.dev)- systemd มี ความสามารถในการจัดการบริการ ที่ทรงพลัง แต่ค่าตั้งต้นถูกปรับให้เหมาะกับการใช้งานมากกว่าความปลอดภัย จึงควรมีการใช้ ตัวเลือก hardening เพิ่มเติม
- สามารถใช้คำสั่ง
systemd-analyze securityเพื่อวิเคราะห์ ตัวชี้วัดการเปิดเผยด้านความปลอดภัย ของทุกบริการหรือบริการเฉพาะ และใช้จัดลำดับความสำคัญได้ - มี ตัวเลือกด้านความปลอดภัย หลายแบบที่ใช้ได้ในระดับ service unit และสามารถแก้ไขแยกเป็นรายตัวผ่าน
/etc/systemd/system/ServiceName.service.d/override.confเป็นต้น - ตัวเลือกสำคัญมีทั้ง
ProtectSystem,PrivateTmp,NoNewPrivileges,SystemCallFilter,MemoryDenyWriteExecuteเป็นต้น ซึ่งใช้จำกัดสิทธิ์ของโปรเซสและการเข้าถึงทรัพยากร - แทนที่จะมุ่งเป้าไปที่ความปลอดภัยสมบูรณ์แบบ ควร harden บริการที่เปิดเผยสู่ภายนอก ก่อนเพื่อลดความเสี่ยง และยังให้ผลดีมากในสภาพแวดล้อมแบบ self-hosting
ภาพรวมของ systemd
- systemd มอบแนวทางการจัดการบริการที่สมบูรณ์และแข็งแกร่งมาก
- แต่เนื่องจากให้ความสำคัญกับความพร้อมใช้งานทันทีมากกว่าความปลอดภัย ค่าตั้งต้นจึงค่อนข้างผ่อนปรน
- เอกสารนี้แนะนำตัวเลือกเสริมความปลอดภัยหลายแบบที่ใช้กับ systemd service unit และ podman quadlet เพื่อลดโอกาสถูกเจาะ และลดขอบเขตความเสียหายหากเกิดการเจาะระบบ
- นี่ไม่ใช่คู่มือสำหรับนำไปใช้แบบเหมารวมกับทุกบริการ แต่ต้องมีการทดลอง ตรวจสอบล็อก และปรับแต่งเป็นรายบริการตามลักษณะและความต้องการของแต่ละตัว
- ความรับผิดชอบด้านความปลอดภัยของโครงสร้างพื้นฐานเป็นของผู้ดูแลทั้งหมด และเอกสารนี้เป็นเพียงเครื่องมืออ้างอิง
การวิเคราะห์ความปลอดภัยของ systemd
- ใช้คำสั่ง
systemd-analyze securityเพื่อตรวจสอบสถานะความปลอดภัยของทุกบริการ หรือวิเคราะห์การตั้งค่าอย่างละเอียดของบริการเฉพาะ (เช่นsshd.service) ได้- เอาต์พุตจะมีสถานะการตรวจสอบ ชื่อฟีเจอร์ คำอธิบาย และ คะแนน Exposure โดยยิ่ง Exposure สูง ความเสี่ยงก็ยิ่งมาก
- สามารถเพิ่มการตั้งค่าตัวเลือกความปลอดภัยในส่วน
[Service](systemd) หรือ[Container](podman quadlet) ได้ - แนะนำให้ใช้
systemctl edit ServiceName.serviceเพื่อสร้างไฟล์ override และหากล้มเหลวควรตรวจสอบสิทธิ์ที่จำเป็นแล้วปรับแก้
ตัวเลือกความปลอดภัยของบริการ
- systemd มี คีย์เวิร์ดตัวเลือกความปลอดภัย หลายแบบ และสามารถดูรายละเอียดได้จาก
man systemd.exec 5,man capabilities 7เป็นต้น - ตัวเลือกด้านความปลอดภัยที่พบได้บ่อย
ProtectSystem→ ตัวเลือกสำหรับจำกัดไฟล์ซิสเต็มให้อ่านได้อย่างเดียวProtectHome→ ตัวเลือกสำหรับบล็อกการเข้าถึง/home,/root,/run/userPrivateDevices→ ตัวเลือกสำหรับบล็อกการเข้าถึงอุปกรณ์จริง และอนุญาตเฉพาะอุปกรณ์เสมือนอย่าง/dev/nullProtectKernelTunables,ProtectKernelModules,ProtectKernelLogs→ ตัวเลือกสำหรับบล็อกการเข้าถึงทรัพยากรของเคอร์เนลNoNewPrivileges→ ตัวเลือกสำหรับป้องกันการได้สิทธิ์ใหม่ผ่าน setuid/setgid เป็นต้นMemoryDenyWriteExecute→ บล็อกการใช้หน่วยความจำที่เขียนและรันได้พร้อมกัน ซึ่งอาจมีปัญหากับภาษาแบบ JIT บางตัวSystemCallFilter→ ตัวเลือกสำหรับจำกัด system call ที่อนุญาต โดยเมื่อฝ่าฝืนอาจจบโปรเซสหรือคืนค่า EPERM ได้
คำอธิบายของแต่ละตัวเลือก
- ProtectSystem: หากตั้งเป็น
strictจะเมานต์ทั้งไฟล์ซิสเต็มเป็นแบบอ่านอย่างเดียว โดย/dev,/proc,/sysต้องใช้ตัวเลือกป้องกันเพิ่มเติมแยกต่างหาก - ReadWritePaths: ตั้งค่าให้บางพาธกลับมาเขียนได้อีกครั้ง
- ProtectHome: บล็อกการเข้าถึง
/home,/root,/run/user - PrivateDevices: ปิดการเข้าถึงอุปกรณ์จริง และอนุญาตเฉพาะอุปกรณ์ pseudo อย่าง
/dev/null - ProtectKernelTunables: ทำให้
/proc,/sysเป็นแบบอ่านอย่างเดียว - ProtectControlGroups: อนุญาตเฉพาะการเข้าถึง cgroup แบบอ่านอย่างเดียว
- ProtectKernelModules: ห้ามโหลดโมดูลเคอร์เนลแบบชัดแจ้ง
- ProtectKernelLogs: จำกัดการเข้าถึงบัฟเฟอร์ล็อกของเคอร์เนล
- ProtectProc: หากตั้งเป็น
invisibleจะซ่อนโปรเซสของผู้ใช้อื่นจาก/proc/ - ProcSubset: บล็อกเนื้อหาใน
/procนอกเหนือจากรายการที่เกี่ยวข้องกับ PID ที่กำหนด - NoNewPrivileges: บล็อกการยกระดับสิทธิ์ใหม่ผ่าน setuid, setgid และ file system capability
- ProtectClock: บล็อกการเขียนค่า system/hardware clock
- SystemCallArchitectures: หากตั้งเป็น
nativeจะอนุญาตเฉพาะ native syscall เช่น x86-64 - RestrictNamespaces: จำกัด namespace ที่ใช้กับคอนเทนเนอร์โดยเฉพาะ
- RestrictSUIDSGID: บล็อกการตั้งบิต setuid, setgid ให้กับไฟล์
- LockPersonality: ป้องกันการเปลี่ยน execution domain (มักจำเป็นเฉพาะกับแอปพลิเคชันรุ่นเก่า)
- RestrictRealtime: จำกัดการจัดตารางแบบ real-time (จำเป็นเฉพาะกับบริการเฉพาะทางบางตัว)
- RestrictAddressFamilies: จำกัดตระกูล socket address ที่อนุญาต (เช่น ระบุ AF_INET, AF_INET6, AF_UNIX เป็นต้น)
- MemoryDenyWriteExecute: บล็อกการสร้างพื้นที่หน่วยความจำใหม่ที่เขียนได้+รันได้ (บริการที่ใช้ JIT ต้องระวัง)
- ProtectHostname: ห้ามใช้ syscall
sethostname,setdomainname - SystemCallFilter: ตั้งค่าอนุญาต/บล็อก syscall รายบริการ และกรองได้ละเอียด
- ปรับได้ทั้งแบบกลุ่ม, syscall รายตัว, วิธีอนุญาต/บล็อก เป็นต้น
- รองรับการตั้งค่าให้คืนรหัสผิดพลาดอย่าง EPERM แทนการจบโปรเซสเมื่อมีการฝ่าฝืน
- ดูรายการทั้งหมดได้จาก
systemd-analyze syscall-filterหรือman systemd.exec(5) - ใช้คำนำหน้า
~เพื่อกลับความหมายของทั้งลิสต์ได้ (เช่นCapabilityBoundingSet=~CAP_SETUIDเป็นต้น)
ขั้นตอนการปรับข้อจำกัดของ SystemCallFilter
- ใช้
auditdเพื่อตรวจดูล็อกได้ว่าเมื่อบริการล้มเหลว มี syscall ใดถูกบล็อก- รัน
sudo ausearch -i -m SECCOMP -ts recentแล้วตรวจค่าของ syscall - จากนั้นเพิ่ม syscall นั้นหรือกลุ่มที่เกี่ยวข้องลงใน
SystemCallFilterเพื่อแก้ปัญหาไปทีละขั้น
- รัน
ลำดับความสำคัญในการทำ hardening และเคล็ดลับการใช้งาน
- ไม่จำเป็นต้องใช้กับทุกบริการทั้งหมด
- threat model และการจัดการความเสี่ยงคือหัวใจสำคัญ โดยเฉพาะ บริการที่เปิดเผยสู่ภายนอก (httpd, nginx, ssh เป็นต้น) ควรพิจารณาเป็นพิเศษ
- คำสั่งแบบกำหนดเอง, timer unit (ตัวแทน cron แบบเดิม) เป็นต้น ก็ให้ผลดีหากนำไปใช้เชิงรุก
- ยิ่งเป็นบริการที่ไม่ซับซ้อนมาก ก็ยิ่งมีโอกาสปรับจูนแบบละเอียดได้มาก
เช็กลิสต์: ชุดตัวเลือกความปลอดภัยที่แนะนำ (ลำดับความสำคัญเริ่มต้น)
ProtectSystem=strictPrivateTmp=yesProtectHome=yesหรือProtectHome=tmpfsProtectClock=yes,ProtectKernelLogs=yes,ProtectKernelModules=yesRestrictSUIDGUID=yesUMask=0077LockPersonality=yesRestrictRealtime=yesMemoryDenyWriteExecute=yesDynamicUser=yesหรือระบุUserเป็นผู้ใช้เฉพาะที่ไม่ใช่ root
- รายการข้างต้นโดยทั่วไปเป็นชุดที่มักใช้ได้โดยแทบไม่กระทบต่อบริการ
- หากต้องการเพิ่มการกรอง syscall (
SystemCallFilter) ด้วย จะต้องมีการทดสอบอย่างละเอียด
ตัวอย่างการตั้งค่า Traefik
- เป็นกรณีตัวอย่างของบริการ Traefik แบบคอนเทนเนอร์ที่รันด้วย systemd quadlet และใช้ตัวเลือกด้านความปลอดภัยหลายรายการ
- ใช้
ProtectSystem=full,ProtectHome=yes,SystemCallFilter=@system-service @mount @privilegedเป็นต้น - ลบสิทธิ์บางอย่างด้วย
CapabilityBoundingSet=~CAP_SETUID CAP_SETPCAP - ใช้ข้อจำกัดการเข้าถึงเครือข่าย เช่น
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
- ใช้
บทสรุป
- ตัวเลือกเสริมความปลอดภัยของ systemd เป็นวิธีที่ใช้งานได้จริงและควรมีติดกล่องเครื่องมือไว้สำหรับผู้ดูแลระบบสายยูนิกซ์
- ไม่ใช่มาตรการความปลอดภัยที่สมบูรณ์แบบ แต่ควรใช้เป็น เครื่องมือเพื่อลดความเสี่ยง และไม่จำเป็นต้องยัดการตั้งค่าความปลอดภัยใส่ทุกบริการแบบไม่เลือก
- โดยเฉพาะผู้ดูแลในสภาพแวดล้อมแบบ self-hosting จะได้ประโยชน์มากในการยกระดับความปลอดภัย
- ให้ความสำคัญกับ “ความใช้งานได้จริงมากกว่าความสมบูรณ์แบบ” และแนะนำให้นำไปใช้แบบบางส่วนตามงานและสภาพแวดล้อมที่เหมาะสม
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
คิดว่าน่าสนใจที่สามารถทำ automated systemd service hardening ได้ด้วยการทำ strace profiling
https://github.com/desbma/shh
มีวิธีที่ผมเจอแล้วค่อนข้างดี คือในตัวอย่างไม่ได้ใช้ ProtectSystem=
แต่ถ้าใช้ TemporaryFileSystem=/:ro, BindReadOnly=/usr/bin/binary /lib /lib64 /usr/lib usr/lib64
ก็จะใส่เฉพาะไบนารีที่ต้องการและพาธที่ต้องการให้อ่านได้เท่านั้น
ตอนนี้ ProtectSystem= ยังไม่รองรับการทำงานแบบนี้
รายละเอียดเพิ่มเติมดูได้ที่นี่
คิดว่าวิธีนี้อาจมีปัญหากับ service ที่ต้องทำงานเพิ่มเติมเวลาเกิด error เช่น ส่งอีเมล
เทียบกับโพสต์เรื่อง systemd hardening เมื่อวาน โพสต์นี้ใช้งานได้จริงกว่าเยอะและมีทิปที่เอาไปใช้ได้ทันทีหลายอย่าง
เมื่อวานผมพยายามยกตัวอย่างที่ practical กว่าไว้ในคอมเมนต์ของโพสต์นั้น แต่โพสต์วันนี้สรุปเนื้อหาเชิงปฏิบัติได้ดีมาก และอธิบายวิธีเสริม isolation กับ security ด้วย systemd ได้อย่างรวดเร็วและง่ายดาย
คิดว่าเป็นบทความที่ยอดเยี่ยม
แปะของเมื่อวานไว้เป็นข้อมูลอ้างอิงด้วย
https://us.jlcarveth.dev/post/hardening-systemd.md
https://news.ycombinator.com/item?id=44928504
บางเบราว์เซอร์เข้าไม่ได้เลยเพราะ certificate error
ขอบคุณที่แชร์
ถ้าใช้ systemd-analyze พร้อมแฟล็ก --user ก็สามารถตรวจความปลอดภัยของ systemd user unit ได้ ("systemd-analyze --user security")
ช่วงที่ย้าย container ไปใช้ Podman ผมเริ่มใช้ systemd มากขึ้น และคิดว่าเครื่องมือนี้จะช่วยยกระดับความปลอดภัยของ systemd unit/container service ได้มาก
สมัยก่อน init script ต่างคนต่างทำกันไป จึงทำ hardening แบบสม่ำเสมออย่างนี้ไม่ได้
ผมเข้ามาใช้ Linux ค่อนข้างช้าเลยนึกภาพระบบที่ไม่มี systemd แทบไม่ออก และระบบที่ไม่มี systemd ก็ใช้งานลำบากมาก
ช่วงหลังผมเพิ่งเจอเครื่องมือชื่อ "unshare" ทำให้ทดลองอย่าง remount ทั้ง /nix เป็น RW ได้โดยไม่กระทบ process อื่น
systemd อาจใช้งานไม่ค่อยลื่นนัก แต่พูดตรง ๆ สำหรับผม ทางเลือกอื่นมีแค่ Windows
สงสัยว่าทำไม Linux distro ถึงไม่เปิด security switch พวกนี้เป็นค่าเริ่มต้นให้มากกว่านี้
เลยคิดว่าการ harden แบบ conservative มันมีข้อเสียอะไรหรือเปล่า
สำหรับผู้ใช้จำนวนมาก การตั้งค่าอาจเยอะและซับซ้อนเกินไป
ถ้าปรับค่าแบบ aggressive เกินไป ก็อาจทำให้ค่าที่มีอยู่เดิมพังโดยไม่ตั้งใจ
ตัวอย่างเช่น ถ้า harden NetworkManager ก็ต้องไล่ตรวจเองว่าทั้ง IPv4 และ IPv6 ยังเชื่อมต่อได้ไหม, โหมด dns=systemd-resolved กับ dns=default ยังทำงานปกติไหม, การเชื่อมต่อกับ ModemManager และ cellular, ปลั๊กอิน openvpn หรือ cisco anyconnect, รวมถึง NetworkManager-dispatcher hook ทำงานครบหรือไม่
อีกประเด็นคือจะมี maintainer ของ distro สักกี่คนที่มั่นใจได้ว่าปรับ switch ของแพ็กเกจที่ตัวเองดูแลได้มากแค่ไหน โดยที่ไม่ทำให้ environment ของผู้ใช้เกิน 0.01% พัง
ถ้า distro เป็นคนดูแลแฟล็กพวกนี้เอง ก็ต้องแบก compatibility issue เพิ่มทุกครั้งที่มี upstream release และถ้า upstream เป็นคนตั้งค่าเอง ก็เลี่ยงไม่ได้ที่จะต้องระวังเรื่อง backward compatibility มากเป็นพิเศษ
คำถามนี้คล้ายกับคำถามว่า "ทำไม distro ถึงไม่เปิด MAC (อย่าง SELinux) แบบเข้มงวดเป็นค่าเริ่มต้น?"
แม้แต่ sshd เองก็ควรถูกจำกัดให้มากกว่านี้
ด้วยเหตุผลพวกนี้ distro หลัก ๆ เลยรับภาระค่อนข้างหนัก
SELinux กับ AppArmor ก็คล้ายกัน คือ maintainer หลายคนมองว่าผลตอบแทนต่อการลงทุนไม่ค่อยสูง
อีกเหตุผลใหญ่คือไม่มีศักยภาพหรือทรัพยากรพอจะทำ integration test ทีละพารามิเตอร์เพื่อให้มั่นใจว่า system service หลักยังทำงานปกติ
บทสนทนาที่เกี่ยวข้อง
https://news.ycombinator.com/item?id=29995566
ผลของ systemd-analyze security ก็แตกต่างกันไปในแต่ละ distro
desbma/shh จะสร้างกฎรายหน่วยอย่าง SyscallFilter เป็นต้นแบบอัตโนมัติจากข้อมูลที่เก็บด้วย strace ซึ่งคล้ายกับ audit2allow ของ SELinux
แต่การติดตั้ง strace ลงใน production environment ก็อาจเป็นเรื่องที่ถกเถียงกันได้
https://github.com/desbma/shh
ผมก็ไม่แน่ใจเหมือนกัน แต่บาง setting เป็นของที่เพิ่งเพิ่มเข้ามา ผู้ใช้จำนวนมากอาจยังไม่รู้จัก
ไม่ใช่ว่าทุกคนจะเป็น systemd power user และถ้าเปิด setting เหล่านี้ ก็มีความเสี่ยงว่า systemd เวอร์ชันเก่าอาจทำงานไม่ได้ตามปกติ
ถึงจะมีความสามารถอย่าง SELinux, AppArmor และอื่น ๆ อยู่ แต่ distro, นักพัฒนา และผู้ใช้จำนวนมากก็ไม่ได้รู้สึกว่าจำเป็นขนาดนั้น จึงทำให้เปิดใช้อัตโนมัติได้ยาก
มีตัวเลือกสำหรับ hardening เยอะมาก จนคิดว่าน่าจะมี repository ที่รวบรวมตัวอย่าง hardening ทั่วไปแยกตาม service ไว้ให้ดู
ผู้ใช้มักนำสคริปต์ hardening ที่คนอื่นใช้กันทั่วไปมาปรับใช้ แต่สุดท้ายกลับพบว่าต้องตั้งสิทธิ์กว้างกว่าที่คิดเพื่อไม่ให้เกิดเคสยกเว้น
วิธีที่มีประโยชน์ที่สุดคือดูว่า distro กระแสหลักแพ็กเกจและ harden กันอย่างไร
hardening เหล่านั้นมักผ่านการทดสอบมาค่อนข้างดีแล้ว ดังนั้นถ้าอยากดูตัวอย่างของ postgresql หรือบริการอื่น ๆ ก็ควรเริ่มจากแพ็กเกจของ Debian, Ubuntu, RHEL
หนึ่งในความสามารถด้าน security ที่ยอดเยี่ยมของ systemd คือการจัดการ credentials
มันช่วยส่ง credential ให้แอปพลิเคชันได้อย่างปลอดภัยกว่าการเก็บไว้ใน environment variable หรือไฟล์บน filesystem
ใน environment ที่ไม่มี Vault หรือระบบลักษณะนั้น เช่น โปรเจกต์ส่วนตัว ผมเลือกวิธีนี้ตลอด
ผมยังทำ Go package ที่เชื่อมกับความสามารถนี้ไว้เองด้วย
credentials ใน systemd
แพ็กเกจ credential-go
ฟังดูเหมือนวัฒนธรรมแบบ nodejs หรือ npm ที่ทำโค้ด 2 บรรทัดให้กลายเป็นแพ็กเกจ
จริง ๆ แล้วก็แค่
ไม่ได้ซับซ้อนไปกว่า left-pad เลย
เท่าที่ผมเข้าใจ ชุมชน Go เดิมทีถือว่าการลด dependency และหลีกเลี่ยง abstraction ที่ไม่จำเป็น (เช่น การเรียกฟังก์ชันเพิ่ม) เป็นสิ่งที่ดี
การทำงานง่าย ๆ แบบนี้เมื่อก่อนคนก็มักเขียนกันสด ๆ ตรงนั้นเลย
สงสัยว่ารูปแบบการส่ง credential แบบนี้ป้องกันไม่ให้ process ลูกที่ถูก fork สืบทอด credential ต่อได้อย่างไร
เป็นบทความที่มีประโยชน์มาก
ชอบทั้งรายการตัวเลือกต่าง ๆ ของ systemd และคำแนะนำแนว "เปิด man แล้วขอให้โชคดี"
systemd ยอดเยี่ยมมากจนผมอยาก deploy ใช้กับเซิร์ฟเวอร์ของตัวเองให้หนักขึ้น
ทิปเล็ก ๆ คือการเขียนชื่อ systemd ที่ถูกต้องคือ systemd
ไม่ใช่ SystemD, system D หรือ system d
เพราะมันย่อมาจาก system daemon จึงตั้งชื่อตามธรรมเนียม Unix/Linux ที่ลงท้ายด้วย d ตัวเล็ก
ปกติผมเห็นคนใช้ systemD บ่อยกว่า เลยสงสัยว่าทำไมรูปแบบนั้นถึงแพร่หลายมาก
ทิปการดีบักปัญหา syscall ใน systemd มีประโยชน์มากจริง ๆ