วิธีแก้ปัญหา nginx ขึ้น 403 Forbidden จาก SELinux
nginx แสดงผล 403 ทั้งที่สิทธิ์ไฟล์ถูกต้อง มักเกิดจาก SELinux ปฏิเสธการเข้าถึง เรียนรู้วิธีตรวจสอบ log การใช้ semanage และ restorecon เพื่อแก้ไข label โดยไม่ต้องปิดระบบ
เหตุใด nginx จึงส่งค่า 403 สำหรับไฟล์ที่มีสิทธิ์เข้าถึงถูกต้อง
การที่ nginx ส่งค่า 403 สำหรับไฟล์ที่มีการตั้งค่า permission ถูกต้องแล้ว มักเกิดจาก SELinux (Security-Enhanced Linux) ปฏิเสธการอ่านไฟล์นั้น SELinux จะตรวจสอบกฎชุดที่สองหลังจากผ่านการตรวจสอบ permission ปกติไปแล้ว โดยเว็บเซิร์ฟเวอร์จะได้รับอนุญาตให้อ่านเฉพาะไฟล์ที่มีป้ายกำกับ (label) เป็นเนื้อหาสำหรับเว็บเท่านั้น หากไฟล์ของคุณมีป้ายกำกับอื่น การเปิดไฟล์จะล้มเหลวและ nginx จะไม่มีข้อมูลส่งให้ผู้ใช้งาน
ให้ตรวจสอบป้ายกำกับ ไม่ใช่ดูแค่โหมดของไฟล์:
ls -ldZ /data/www /data/www/index.htmldrwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.htmlจุดที่แสดงหลัง drwxr-xr-x หมายความว่าไฟล์นั้นมีป้ายกำกับ SELinux กำกับอยู่ default_t คือสิ่งที่พาธได้รับเมื่อนโยบาย (policy) ไม่รู้จักพาธนั้น และไม่มีกฎใดในเว็บเซิร์ฟเวอร์ที่อนุญาตให้อ่านไฟล์ประเภทดังกล่าว บันทึกข้อผิดพลาด (error log) จะแสดงข้อผิดพลาดแบบ Unix ทั่วไป ซึ่งเป็นเหตุผลว่าทำไมกรณีนี้จึงดูเหมือนปัญหาเรื่อง permission:
2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"เคอร์เนลจะส่งค่า 13: Permission denied สำหรับการปฏิเสธทั้งสองรูปแบบ ทั้งแบบปกติและแบบ SELinux ดังนั้นงานแรกที่ต้องทำคือการระบุว่าเลเยอร์ใดเป็นผู้ปฏิเสธ อย่าเพิ่งเริ่มด้วยการใช้ setenforce 0
ส่วนประกอบของโมเดลที่คุณต้องทราบ
SELinux คือระบบควบคุมการเข้าถึงแบบบังคับ หรือที่มักเรียกกันว่า MAC ทุกกระบวนการ (process) จะทำงานภายใต้โดเมนหนึ่งๆ เช่น httpd_t สำหรับเว็บเซิร์ฟเวอร์ ไฟล์และพอร์ตเครือข่ายทุกรายการจะมีประเภท (type) กำกับไว้ เช่น httpd_sys_content_t นโยบายของ SELinux คือรายการชุดค่าผสมที่อนุญาตระหว่างโดเมน ประเภท และการกระทำ หากไม่มีระบุไว้ในรายการนี้ การเข้าถึงจะถูกปฏิเสธ ระบบนี้จะทำงานต่อจากการตรวจสอบแบบ Unix ดั้งเดิม ดังนั้น บิตสิทธิ์การเข้าถึงในรูปแบบ drwxr-xr-x จะต้องอนุญาตการเข้าถึงก่อนเป็นอันดับแรก ทั้งสองชั้นต้องอนุญาตจึงจะเข้าถึงได้
บริบท (context) ที่สมบูรณ์ประกอบด้วย 4 ฟิลด์คั่นด้วยเครื่องหมายโคลอน เช่น system_u:system_r:httpd_t:s0 ได้แก่ ผู้ใช้ SELinux, บทบาท (role), ประเภท (type) และระดับ (level) ในการดูแลเซิร์ฟเวอร์ คุณจะใช้เวลาส่วนใหญ่ไปกับฟิลด์ที่สามซึ่งก็คือประเภท มีคำสั่งสองคำสั่งที่ใช้แสดงค่าที่ใช้งานจริง:
ps -eZ | grep nginx
id -Zกระบวนการทำงานของ nginx จะแสดงบริบทที่ลงท้ายด้วย httpd_t ส่วนเชลล์ที่คุณใช้ล็อกอินจะแสดงเป็น unconfined_u:unconfined_r:unconfined_t:s0 เนื่องจากนโยบาย targeted แบบมาตรฐานจะจำกัดเฉพาะบริการต่างๆ และปล่อยให้ผู้ใช้ทั่วไปทำงานได้ตามปกติ สิ่งนี้เป็นเรื่องที่ควรทราบเพราะ SELinux ไม่ได้เข้ามาแทนที่ การรันบริการภายใต้ผู้ใช้ที่มีสิทธิ์จำกัด (least-privilege) แต่ทำหน้าที่จำกัดขอบเขตสิ่งที่บริการนั้นสามารถเข้าถึงได้หลังจากที่มีผู้บุกรุกเข้ามาในระบบแล้ว
โหมดการทำงานทั้ง 3 รูปแบบ และการรองรับ SELinux ในอิมเมจต่างๆ
sestatus
getenforceโหมด Enforcing จะทำการบล็อกและบันทึก log การกระทำที่ละเมิดนโยบาย โหมด Permissive จะอนุญาตทุกการกระทำแต่ยังคงบันทึก log สิ่งที่ควรจะถูกบล็อกไว้ ส่วนโหมด Disabled จะไม่มีการโหลดนโยบายใดๆ เลย คำสั่ง getenforce ใช้สำหรับแสดงโหมดการทำงานปัจจุบัน ส่วน sestatus จะแสดงโหมดที่อ่านมาจากไฟล์ /etc/selinux/config ซึ่งเป็นค่าที่จะถูกนำมาใช้หลังจากรีบูตระบบ
Rocky Linux, AlmaLinux, Fedora และ RHEL มาพร้อมกับ SELinux ในโหมด Enforcing โดยใช้ policy แบบ targeted ในขณะที่ Ubuntu และ Debian จะใช้ AppArmor แทน ซึ่งทำหน้าที่ในลักษณะเดียวกันแต่ใช้กลไกที่แตกต่างกัน (เนื้อหาในส่วนสุดท้ายจะอธิบายเรื่องนี้) ด้วยเหตุนี้ แอปพลิเคชันเดียวกันจึงอาจติดตั้งบนเซิร์ฟเวอร์เครื่องหนึ่งได้สำเร็จ แต่กลับแสดงข้อผิดพลาด 403 บนอีกเครื่องหนึ่งได้
ติดตั้งเครื่องมือที่จำเป็นไว้ก่อนใช้งาน
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found บนอิมเมจแบบ minimal หมายความว่า policycoreutils-python-utils ยังไม่ได้ถูกติดตั้ง เนื่องจากแพ็กเกจดังกล่าวประกอบด้วย semanage และ audit2allow ส่วน setroubleshoot-server จะเพิ่ม sealert และบันทึกสรุปเหตุการณ์การปฏิเสธการเข้าถึงเป็นภาษาที่เข้าใจง่ายลงใน journal ให้ติดตั้งทั้งสองรายการนี้บนเซิร์ฟเวอร์ที่เพิ่งติดตั้งใหม่ เพราะช่วงเวลาที่คุณจำเป็นต้องใช้เครื่องมือเหล่านี้ มักเป็นช่วงเวลาที่ระบบเกิดปัญหาขึ้นแล้ว
วิธีอ่านข้อความปฏิเสธการเข้าถึงของ SELinux ใน audit log
การปฏิเสธการเข้าถึงแต่ละครั้งจะถูกบันทึกโดย audit daemon ในรูปแบบข้อความ AVC (access vector cache):
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=AVC msg=audit(1754896442.881:412): avc: denied { read } for pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0ข้อมูล 4 ส่วนจะบอกรายละเอียดทั้งหมด comm คือโปรแกรมที่ถูกบล็อก scontext คือ source context หรือโดเมนที่โพรเซสกำลังทำงานอยู่ tcontext คือ target context หรือป้ายกำกับของสิ่งที่โพรเซสพยายามเข้าถึง tclass คือประเภทของออบเจกต์ ในที่นี้คือไฟล์ เมื่ออ่านรวมกันจะได้ความว่า: โพรเซสใน httpd_t พยายามอ่านไฟล์ที่ติดป้ายกำกับว่า default_t และ permissive=0 ระบุว่าคำขอนั้นถูกบล็อกจริง ไม่ใช่แค่การบันทึก log ไว้เท่านั้น
หาก ausearch ไม่แสดงผลลัพธ์ใดๆ เป็นไปได้ว่า audit daemon อาจไม่ได้ทำงานอยู่ ข้อความปฏิเสธการเข้าถึงจะไปปรากฏใน kernel ring buffer แทน:
sudo journalctl -k | grep -i avcจากนั้นให้แปลงบันทึกดังกล่าวเป็นประโยคภาษาอังกฤษ:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why จะอ่านบันทึกเดียวกันและระบุสาเหตุที่ตรวจพบ เช่น boolean ที่ถูกปิดอยู่, ป้ายกำกับที่ไม่ตรงกับนโยบาย หรือไม่มีกฎรองรับเลย sealert จะไล่ดู log ทั้งหมดและแสดงคำสั่งที่แนะนำสำหรับการปฏิเสธแต่ละรายการ ให้ถือว่าคำแนะนำเป็นเพียงแนวทางเท่านั้น ข้อความอาจเปลี่ยนแปลงไปตามเวอร์ชันของซอฟต์แวร์ และบางครั้ง sealert อาจเสนอให้สร้าง custom policy module ทั้งที่วิธีแก้ไขที่ถูกต้องคือการปรับป้ายกำกับเพียงบรรทัดเดียว
อีกเรื่องที่ควรทราบคือนโยบายของ SELinux มีกฎ dontaudit ที่ซ่อนการปฏิเสธการเข้าถึงที่ถือว่าไม่เป็นอันตรายไว้ ทำให้โปรแกรมอาจทำงานผิดพลาดได้ในขณะที่ log ยังคงว่างเปล่า คุณสามารถยกเลิกการซ่อนข้อความเหล่านี้ชั่วคราวเพื่อการทดสอบได้ด้วยคำสั่ง:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -Bแก้ไข path ที่ระบุ label ผิดพลาดด้วย semanage fcontext และ restorecon
ต้องใช้สองคำสั่งและลำดับมีความสำคัญ semanage fcontext -a ทำหน้าที่บันทึกว่า label ของ path นั้นควรเป็นอย่างไร ส่วน restorecon จะนำค่าเริ่มต้นที่บันทึกไว้ไปปรับใช้กับไฟล์บนดิสก์
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlpath ที่ระบุเป็นนิพจน์ทั่วไป (regular expression) โดย (/.*)? จะครอบคลุมถึงตัวไดเรกทอรีเองและทุกสิ่งที่อยู่ภายใน ซึ่งเป็นสิ่งที่ document root จำเป็นต้องมี ให้ตรวจสอบสิ่งที่กำลังจะเปลี่ยนแปลงก่อนดำเนินการจริง: sudo restorecon -Rvn /data/www จะแสดงรายการการเปลี่ยน label ที่วางแผนไว้ เนื่องจาก -n หมายถึงการไม่ดำเนินการใดๆ หลังจากใช้ restorecon จริงแล้ว label จะเปลี่ยนเป็น httpd_sys_content_t และข้อผิดพลาด 403 จะหายไปโดยไม่ต้องรีสตาร์ท service
ใช้ chcon เพื่อการทดสอบเท่านั้น chcon -t httpd_sys_content_t index.html จะตั้งค่า label โดยตรง แต่เมื่อมีการใช้ restorecon, การอัปเดตแพ็กเกจ หรือการทำ relabel ทั้งระบบ ค่าจะถูกรีเซ็ตกลับไปตามนโยบายที่กำหนดไว้เดิม semanage fcontext คือวิธีที่ทำให้ค่าคงอยู่ถาวร คุณสามารถตรวจสอบรายการที่บันทึกไว้ได้ด้วย sudo semanage fcontext -l | grep '^/data'
เนื้อหาที่ service จำเป็นต้องเขียนข้อมูลลงไปต้องการ type ที่แตกต่างออกไป ให้ใช้ httpd_sys_rw_content_t สำหรับไดเรกทอรีอัปโหลดหรือแคช และจำกัดการใช้งานไว้เฉพาะ path เหล่านั้นเท่านั้น เพราะการวางเว็บไซต์แบบอ่านอย่างเดียวไว้ภายใต้ type ที่เขียนได้ จะเป็นการเปิดสิทธิ์การเข้าถึงเกินความจำเป็นของแอปพลิเคชัน
เหตุใด label จึงผิดพลาด? สาเหตุส่วนใหญ่มาจากวิธีการย้ายไฟล์เข้ามา mv จะคง label เดิมของไฟล์ไว้ ดังนั้นหากย้ายเว็บไซต์ออกจาก /root ไฟล์จะติด label admin_home_t และคงค่านั้นไว้ การใช้ cp ปกติจะทำให้ไฟล์ใหม่ได้รับ label เริ่มต้นของไดเรกทอรีปลายทาง ซึ่งมักเป็นสิ่งที่ต้องการ ในขณะที่ cp -a และ rsync -X จะคัดลอก label จากต้นทางมาพร้อมกับไฟล์ การใช้ git clone ไปยังไดเรกทอรีระดับบนสุดแห่งใหม่จะทำให้เกิด default_t เมื่อหน้าเว็บโหลดจาก /usr/share/nginx/html ได้ปกติแต่กลับล้มเหลวในไดเรกทอรีของคุณเอง นี่คือสาเหตุของปัญหาดังกล่าว
แก้ไขพฤติกรรมด้วยค่าบูลีน
ความล้มเหลวบางอย่างไม่ใช่ปัญหาเรื่อง label โดย reverse proxy บน Rocky หรือ AlmaLinux ที่ติดตั้งใหม่มักจะส่งค่า 502 กลับมา และ log แสดงข้อผิดพลาดดังนี้:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamupstream ของคุณทำงานปกติ แต่โดเมน httpd_t ไม่ได้รับอนุญาตให้เปิดการเชื่อมต่อเครือข่ายขาออกโดยค่าเริ่มต้น ดังนั้นการเรียก connect() จึงถูกปฏิเสธก่อนที่จะถึง loopback interface มีสวิตช์ตัวเดียวที่ควบคุมพฤติกรรมทั้งหมดนี้:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P คือ flag ที่สำคัญ เพราะมันจะเขียนค่าลงในดิสก์ หากไม่มี -P การเปลี่ยนแปลงจะหายไปเมื่อรีบูตเครื่องครั้งถัดไป ซึ่งจะทำให้บริการของคุณทำงานได้จนกว่าจะรีสตาร์ทเครื่องเท่านั้น ให้ยืนยันด้วย semanage boolean -l | grep httpd_can_network_connect ซึ่งจะแสดงค่าที่กำลังทำงานอยู่ควบคู่ไปกับค่าที่บันทึกไว้
ควรเลือกใช้ค่าบูลีนแทนการเขียนกฎขึ้นมาเองหากมีตัวเลือกนั้นอยู่ ค่าบูลีนมาพร้อมกับนโยบายของ distribution จึงมีการดูแลรักษา มีเอกสารประกอบ และง่ายต่อผู้ดูแลคนถัดไปที่จะค้นหา getsebool -a จะแสดงรายการค่าทั้งหมดที่มีอยู่ในระบบ
การตั้งค่าให้ service ฟังพอร์ตที่ไม่ใช่พอร์ตมาตรฐาน
พอร์ตต่างๆ จะมีการระบุประเภท (label) ไว้ หากคุณย้าย Nginx ไปที่พอร์ต 8081 แล้ว service ไม่ยอมเริ่มทำงาน:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t จะอนุญาตให้ bind ได้เฉพาะพอร์ตที่ระบุประเภทเป็น http_port_t เท่านั้น ซึ่งพอร์ต 8081 ไม่ได้ถูกระบุไว้ ให้เพิ่มพอร์ตดังกล่าวเข้าไป:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081ให้ตรวจสอบรายการพอร์ตก่อนเสมอ พอร์ตระดับสูงหลายพอร์ตได้รับอนุญาตไว้แล้ว เช่น 8008 และ 8443 หากพยายามเพิ่มพอร์ตเดิมซ้ำจะเกิดข้อผิดพลาด ValueError: Port tcp/8081 already defined หากพอร์ตนั้นถูกกำหนดประเภทอื่นไว้แล้ว ให้ใช้ semanage port -m -t http_port_t -p tcp 8081 เพื่อแก้ไขประเภทแทนการเพิ่มใหม่
คำสั่งเดียวกันนี้คือสิ่งที่ทำให้การย้ายพอร์ต SSH ใช้งานได้จริง การตั้งค่า Bind to port 2222 on 0.0.0.0 failed: Permission denied ใน journalctl -u sshd หมายความว่าพอร์ต 2222 ยังไม่ได้อยู่ใน ssh_port_t ดังนั้นให้รันคำสั่ง sudo semanage port -a -t ssh_port_t -p tcp 2222 ก่อนที่คุณจะรีสตาร์ท daemon และปิด session ของคุณ นี่คือขั้นตอนที่ผู้คนมักข้ามไปเมื่อทำตามคู่มือทั่วไปสำหรับการ hardening SSH on a VPS บน image ตระกูล Red Hat ทั้งนี้ SELinux ไม่ใช่ firewall ดังนั้นคุณยังคงต้องเปิดพอร์ตผ่าน firewall ด้วยคำสั่ง sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload หรือใช้ ufw on a Debian or Ubuntu image
เมื่อไม่มีค่า boolean หรือ label ให้ปรับเปลี่ยน
กรณีนี้พบได้น้อยบนเซิร์ฟเวอร์ทั่วไป และเป็นจุดที่ผู้ใช้งานมักทำให้เกิดความเสียหาย audit2allow สามารถสร้าง policy module จากรายการปฏิเสธการเข้าถึง (denials) ใน log ได้:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppโปรดอ่าน nginx_local.te ก่อนทำการติดตั้ง มีนิสัยสองประการที่ช่วยให้การดำเนินการนี้ปลอดภัย ประการแรก ให้กรองข้อมูลขาเข้าเฉพาะโปรแกรมที่คุณกำลังแก้ไขด้วย -c เนื่องจากการส่งข้อมูลรายการปฏิเสธการเข้าถึงที่สะสมมาตลอดสัปดาห์เข้าสู่ audit2allow จะเป็นการอนุญาตสิทธิ์ทั้งหมดนั้นในคราวเดียว ประการที่สอง ห้ามติดตั้ง module ที่สร้างจากรายการปฏิเสธการเข้าถึงที่คุณไม่สามารถอธิบายที่มาได้ เพราะกฎที่อนุญาตให้ httpd_t อ่านไฟล์ทุกไฟล์บนเครื่องนั้นสร้างได้ง่าย แต่ตรวจสอบพบได้ยากในอีกหลายเดือนต่อมา คุณสามารถลบ module ออกได้ด้วย sudo semodule -r nginx_local
Permissive เป็นโหมดสำหรับการวินิจฉัย ไม่ใช่การแก้ไขปัญหา
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1โหมด Permissive จะอนุญาตให้เข้าถึงและบันทึกเหตุการณ์ไว้ใน log ประโยชน์ที่แท้จริงของมันคือความครบถ้วน ในโหมด Enforcing บริการจะหยุดทำงานทันทีที่พบการปฏิเสธการเข้าถึงครั้งแรก ทำให้คุณต้องแก้ไขทีละจุดแล้วรีสตาร์ทเพื่อไปพบจุดถัดไป แต่ในโหมด Permissive ระบบจะทำงานต่อไปและบันทึกทุกการปฏิเสธการเข้าถึงไว้ในรอบเดียว ซึ่งช่วยให้คุณสลับกลับมาแก้ไขปัญหาทั้งหมดได้พร้อมกัน
setenforce ไม่ได้ไปเปลี่ยนแปลงค่า /etc/selinux/config ดังนั้นการรีบูตจะทำให้เครื่องกลับไปอยู่ในโหมด Enforcing ตามเดิม นี่คือกลไกป้องกันความปลอดภัย และเป็นเหตุผลว่าทำไมการ "แก้ไข" ด้วยวิธี setenforce 0 ถึงกลับมาสร้างปัญหาในเวลาที่ไม่คาดคิด หากบริการใดบริการหนึ่งต้องการพื้นที่ทำงานในระหว่างที่คุณกำลังจัดการ ให้กำหนดค่าเฉพาะโดเมนนั้นแทนการเปลี่ยนทั้งเครื่อง: sudo semanage permissive -a httpd_t จะคงสถานะ Enforcing ไว้สำหรับส่วนอื่นทั้งหมด และ sudo semanage permissive -d httpd_t จะเป็นการยกเลิกการตั้งค่าดังกล่าว
เหตุผลที่การปิด SELinux มีราคาที่ต้องจ่ายสูงกว่าการแก้ไข label
การตั้งค่า SELINUX=disabled ใน /etc/selinux/config เป็นการแลกเปลี่ยนการแก้ไข label เพียงบรรทัดเดียวกับความปลอดภัยของเซิร์ฟเวอร์ที่ลดลงอย่างถาวร ความแตกต่างนี้จะปรากฏชัดในวันที่เว็บแอปพลิเคชันถูกเจาะระบบ ภายใต้โหมด enforcing โค้ดของผู้โจมตีจะทำงานอยู่ใน httpd_t ดังนั้นมันอาจอ่านเนื้อหาบนเว็บได้ แต่การอ่าน /etc/shadow หรือการเขียนไฟล์ systemd unit จะถูกปฏิเสธโดยนโยบายไม่ว่า Unix user จะอนุญาตให้ทำได้หรือไม่ก็ตาม แต่หากไม่มีการโหลดนโยบายใดๆ โค้ดชุดเดียวกันนั้นจะได้รับสิทธิ์ทุกอย่างที่ service account นั้นมี
การปิดใช้งานยังมาพร้อมกับภาระที่คุณต้องจ่ายในภายหลัง ในขณะที่ไม่มีการโหลดนโยบาย ไฟล์ใหม่จะถูกสร้างขึ้นโดยไม่มี label ทำให้ระบบไฟล์ไม่สอดคล้องกับนโยบายอีกต่อไป การเปิด SELinux กลับมาใช้งานจึงจำเป็นต้องทำ relabel ทั้งระบบ มิฉะนั้นบริการจำนวนมากจะล้มเหลวพร้อมกัน:
sudo fixfiles -F onboot
sudo rebootคำสั่งดังกล่าวจะเขียน /.autorelabel และทำ relabel ระบบไฟล์ทั้งหมดในระหว่างการบูตครั้งถัดไป บนดิสก์ขนาดใหญ่กระบวนการนี้ใช้เวลานานและคอนโซลอาจดูเหมือนค้าง ดังนั้นควรเริ่มทำเมื่อคุณสามารถรอได้ สำหรับ Rocky Linux และ AlmaLinux 9 ไฟล์ config ไม่สามารถปิดส่วนของ kernel ได้ด้วยตัวเองอีกต่อไป และวิธีที่ถูกต้องตามเอกสารในการปิด SELinux อย่างสมบูรณ์คือการใช้ kernel argument (sudo grubby --update-kernel ALL --args selinux=0) การทราบคำสั่งนี้มีประโยชน์เมื่อคุณต้องรับช่วงดูแลเซิร์ฟเวอร์ต่อจากผู้อื่น แต่นี่ไม่ใช่การแก้ไขสำหรับปัญหา 403
การเพิ่ม label ให้กับคอนเทนเนอร์
บนโฮสต์ตระกูล Red Hat กระบวนการทำงานของคอนเทนเนอร์จะรันภายใต้ container_t และสามารถอ่านได้เฉพาะไฟล์ที่ติด label เป็น container_file_t เท่านั้น การทำ bind mount จากโฮสต์จะล้มเหลวด้วยข้อผิดพลาด Permission denied ภายในคอนเทนเนอร์ ทั้งที่สถานะ ls -l บนโฮสต์ดูเป็นปกติทุกประการ ส่วนต่อท้าย :Z จะสั่งให้รันไทม์ทำการเปลี่ยน label ของ mount นั้นใหม่:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z จะทำการติด label ไดเรกทอรีสำหรับคอนเทนเนอร์นี้เพียงตัวเดียว ส่วน :z จะติด label เพื่อให้แชร์ระหว่างคอนเทนเนอร์ได้ หากระบุ :Z ไปยังไดเรกทอรีที่บริการอื่นใช้งานอยู่ ระบบจะทำการเปลี่ยน label ไดเรกทอรีนั้นแบบ recursive ซึ่งจะทำให้บริการเหล่านั้นใช้งานไม่ได้ ดังนั้นควรจัดสรร path เฉพาะสำหรับคอนเทนเนอร์แต่ละตัว ส่วนรายละเอียดอื่น ๆ ในการตั้งค่าจะเหมือนกับอิมเมจอื่น ๆ ซึ่งได้อธิบายไว้ใน การรัน Docker บน VPS
Ubuntu และ Debian มาพร้อมกับ AppArmor
ทำงานเหมือนกันแต่ใช้การออกแบบที่ต่างกัน AppArmor จำกัดการทำงานของโปรแกรมโดยอ้างอิงจาก path ของไฟล์ executable โดยใช้โปรไฟล์ภายใต้ /etc/apparmor.d/ แทนการติดป้ายกำกับ (label) ไฟล์บนดิสก์ จึงไม่มีความจำเป็นต้องทำ relabel และไม่มี restorecon ให้ใช้งาน เริ่มต้นได้ที่นี่:
sudo aa-status
sudo journalctl -k | grep -i apparmorการปฏิเสธการเข้าถึงจะปรากฏเป็น apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r" กระบวนการทำงานมีรูปแบบเดียวกัน คืออ่านบันทึกการปฏิเสธ ค้นหาโปรไฟล์ที่เกี่ยวข้อง และแก้ไขกฎ sudo apt install apparmor-utils ช่วยให้คุณใช้ aa-complain (โหมด permissive สำหรับโปรไฟล์เดียว) และ aa-enforce เพื่อเปลี่ยนกลับเป็นโหมดปกติ Ubuntu จะจำกัดการทำงานเฉพาะบริการที่ติดตั้งผ่านแพ็กเกจบางรายการเท่านั้น ส่วนบริการอื่นจะไม่มีการจำกัด ดังนั้นให้อ่าน aa-status เพื่อตรวจสอบว่ามีอะไรทำงานอยู่จริงแทนการคาดเดา
มีนิสัยหนึ่งที่ใช้ได้กับทั้งสองระบบ เมื่อบริการแจ้งข้อผิดพลาด Permission denied ในสิ่งที่ดูเหมือนจะถูกต้อง ให้ตรวจสอบ log ความปลอดภัยก่อนที่จะไปแก้ไขสิทธิ์การเข้าถึง เพราะปัญหาเรื่องสิทธิ์มักไม่ได้เกิดจากตัวเลข permission ซ้ำซ้อนกันบ่อยนัก
FAQ
ทำไม Nginx ถึงคืนค่า 403 ทั้งที่สิทธิ์ของไฟล์ถูกต้องแล้ว?
เพราะ SELinux ปฏิเสธการอ่าน ไม่ใช่เพราะสิทธิ์ของไฟล์ (permission bits) เว็บเซิร์ฟเวอร์ทำงานภายใต้โดเมน httpd_t และอ่านได้เฉพาะไฟล์ที่ถูกติดป้ายกำกับ (label) สำหรับเนื้อหาเว็บเท่านั้น ดังนั้นไฟล์ที่ติดป้ายกำกับเป็น default_t หรือ admin_home_t จะถูกปฏิเสธและ Nginx จึงไม่มีไฟล์ให้แสดงผล ให้ตรวจสอบด้วยคำสั่ง sudo ausearch -m AVC -ts recent ซึ่งจะแสดง scontext ที่ลงท้ายด้วย httpd_t และ tcontext ที่มีประเภท (type) ไม่ถูกต้อง จากนั้นให้บันทึกป้ายกำกับที่ถูกต้องและนำไปใช้ด้วยคำสั่ง sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" ตามด้วย sudo restorecon -Rv /data/www
การรัน setenforce 0 เพื่อให้เซอร์วิสทำงานได้นั้นปลอดภัยหรือไม่?
setenforce 0 เป็นเพียงขั้นตอนการวินิจฉัย ไม่ใช่การแก้ไขปัญหา ให้ใช้เพื่อจำลองปัญหาให้เกิดขึ้นเพื่อให้ log บันทึกการปฏิเสธทั้งหมดไว้ในรอบเดียว จากนั้นอ่าน log ด้วย sudo ausearch -m AVC -ts recent แล้วรัน sudo setenforce 1 เพื่อแก้ไขสาเหตุที่แท้จริง หากปล่อยให้เซิร์ฟเวอร์อยู่ในโหมด permissive ระบบจะบันทึกการปฏิเสธทุกรายการแต่ไม่บล็อกสิ่งใดเลย ทำให้คุณได้รับข้อมูลขยะจำนวนมากและสูญเสียการป้องกัน หากมีเซอร์วิสหนึ่งที่ต้องการพื้นที่ทำงานในระหว่างที่คุณกำลังแก้ไข ให้รัน sudo semanage permissive -a httpd_t เพื่อให้ส่วนที่เหลือของเครื่องยังคงอยู่ในโหมด enforcing
ฉันจะรันเซอร์วิสบนพอร์ตที่ไม่ใช่พอร์ตมาตรฐานในขณะที่ SELinux เปิดใช้งานอยู่ได้อย่างไร?
ให้เพิ่มพอร์ตนั้นเข้าไปในประเภท (type) ที่เซอร์วิสนั้นได้รับอนุญาตให้ bind สำหรับเว็บเซิร์ฟเวอร์บนพอร์ต 8081 ให้ใช้: sudo semanage port -a -t http_port_t -p tcp 8081 สำหรับ SSH บนพอร์ต 2222 ให้ใช้: sudo semanage port -a -t ssh_port_t -p tcp 2222 ให้ตรวจสอบรายการปัจจุบันก่อนด้วย sudo semanage port -l | grep -w http_port_t เพราะหากพอร์ตนั้นมีอยู่ในรายการอยู่แล้ว คำสั่งจะล้มเหลวด้วยข้อความ ValueError: Port tcp/8081 already defined หากไม่ทำตามขั้นตอนนี้ daemon จะหยุดทำงานทันทีที่เริ่มระบบด้วยข้อความ bind() ... Permission denied แม้ว่าจะไม่มีกระบวนการอื่นใช้งานพอร์ตนั้นอยู่ก็ตาม
Ubuntu มี SELinux หรือไม่?
ไม่มี Ubuntu และ Debian ใช้ AppArmor ซึ่งบังคับใช้โปรไฟล์ที่ผูกกับพาธของไฟล์ปฏิบัติการแทนการใช้ป้ายกำกับบนไฟล์ ให้ตรวจสอบด้วย sudo aa-status และมองหาบรรทัด apparmor="DENIED" ใน sudo journalctl -k โดยปกติ Ubuntu จะจำกัดการทำงานเฉพาะเซอร์วิสที่ติดตั้งผ่านแพ็กเกจบางรายการเท่านั้น ดังนั้นโปรแกรมจำนวนมากจึงรันโดยไม่มีการจำกัด (unconfined) โดยค่าเริ่มต้น คุณจะพบ SELinux ที่เปิดใช้งานมาตั้งแต่ต้นใน Rocky Linux และ AlmaLinux รวมถึง Fedora และ RHEL