SSD Nodes Learn 🎉 VPS mula $5.50/buwan
Mga Gabay Matt ConnorNi Matt Connor · Na-update 2026-08-13

SELinux sa server: bakit nag-403 ang nginx

May 403 ang nginx kahit tama ang permissions? Basahin ang SELinux denial, ayusin ang label gamit ang semanage at restorecon, at panatilihing enforcing.

Bakit nagbabalik ng 403 ang nginx sa file na tama naman ang permissions

Ang pagbabalik ng nginx ng 403 sa file na tama ang permission bits ay halos palaging dahil sa SELinux (security-enhanced Linux) na tumatangging magbasa. Sinusuri ng SELinux ang ikalawang set ng rules matapos pumasa sa normal na permissions check. Pinapayagan lamang ang web server na magbasa ng mga file na may web content label. Iba ang label ng file mo, kaya nabibigo ang open at walang maipapadalang content ang nginx.

Suriin ang label, hindi lamang ang mode:

ls -ldZ /data/www /data/www/index.html
drwxr-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

Ipinapakita ng tuldok pagkatapos ng drwxr-xr-x na may SELinux label ang file. Ang default_t ang label na nakukuha ng isang path kapag hindi pa ito nakikilala ng policy. Walang rule sa web server na nagpapahintulot sa pagbasa ng ganoong type. Ordinary Unix error ang ipinapakita ng error log, kaya nagmumukha itong permissions bug:

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"

Ibinabalik ng kernel ang 13: Permission denied para sa parehong uri ng pagtanggi: ang ordinaryong pagtanggi at ang pagtanggi ng SELinux. Kaya ang unang hakbang ay tukuyin kung aling layer ang tumanggi. Huwag magsimula sa setenforce 0.

Ang bahaging kailangan mo sa model

Ang SELinux ay mandatory access control, na karaniwang isinusulat bilang MAC. Tumatakbo ang bawat proseso sa isang domain, gaya ng httpd_t para sa web server. May type ang bawat file at bawat network port, gaya ng httpd_sys_content_t. Ang policy ay listahan ng mga pinapayagang kombinasyon ng domain, type, at action. Ang anumang wala sa listahang ito ay dini-deny. Tumatakbo ito pagkatapos ng classic Unix check, kaya kailangan pa ring payagan muna ng permission bits sa drwxr-xr-x ang access. Kailangang parehong magbigay ng pahintulot ang dalawang layer.

Ang full context ay may apat na field na pinaghihiwalay ng mga colon, gaya ng system_u:system_r:httpd_t:s0: ang SELinux user, role, type, at level. Sa isang server, halos palagi kang nakatuon sa ikatlong field, ang type. Dalawang command ang nagpapakita ng mga live value:

ps -eZ | grep nginx
id -Z

Nagtatapos sa httpd_t ang context ng nginx workers. Nagpapakita naman ang login shell mo ng unconfined_u:unconfined_r:unconfined_t:s0, dahil kino-confine ng default na targeted policy ang mga serbisyo at hinahayaang hindi saklaw nito ang mga interactive user. Mahalagang malaman ito dahil hindi pinapalitan ng SELinux ang pagpapatakbo ng mga serbisyo sa ilalim ng mga user na may pinakamababang pribilehiyo. Nililimitahan nito ang maaabot ng isang serbisyo kapag may nakapasok dito.

Ang tatlong mode, at kung aling mga image ang may SELinux

sestatus
getenforce

Hinaharang at nilala-log ng Enforcing ang mga aksyon. Pinapahintulutan ng Permissive ang lahat at nilala-log kung ano ang haharangin sana nito. Walang policy na nilo-load ang Disabled. Ipinapakita ng getenforce ang kasalukuyang mode. Ipinapakita rin ng sestatus ang mode mula sa /etc/selinux/config, na siyang muling ginagamit pagkatapos ng reboot.

May SELinux na naka-enforce gamit ang targeted policy ang Rocky Linux, AlmaLinux, Fedora, at RHEL. AppArmor naman ang gamit ng Ubuntu at Debian. Pareho ang layunin nito, pero iba ang mekanismo nito. Tatalakayin ito sa huling seksyon. Kaya maaaring ma-install nang maayos ang parehong application sa isa sa mga server mo, pero magbalik ito ng 403 sa isa pa.

I-install ang mga tool bago mo kailanganin ang mga ito

sudo dnf install -y policycoreutils-python-utils setroubleshoot-server

semanage: command not found sa isang minimal image ay nangangahulugang nawawala ang policycoreutils-python-utils: naglalaman ang package na iyon ng semanage at audit2allow. Nagdaragdag ang setroubleshoot-server ng sealert at nagsusulat ng malinaw na buod sa English para sa bawat denial sa journal. I-install ang dalawang ito sa isang bagong server, dahil kapag kailangan mo na ang mga ito, may sira na agad.

Paano basahin ang SELinux denial sa audit log

Itinatala ng audit daemon ang bawat pagtanggi bilang mensaheng AVC (access vector cache):

sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent
type=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

Apat na field ang naglalaman ng buong impormasyon. Ang comm ay ang program na hinarang. Ang scontext ay ang source context, o ang domain kung saan tumatakbo ang process. Ang tcontext ay ang target context, o ang label ng resource na sinubukan nitong ma-access. Ang tclass ay ang uri ng object; dito, file ito. Kapag pinagsama: sinubukan ng process sa httpd_t na basahin ang file na may label na default_t, at ipinapakita ng permissive=0 na talagang hinarang ang request sa halip na itala lamang.

Kung walang output ang ausearch, maaaring hindi tumatakbo ang audit daemon. Sa halip, napupunta ang mga denial sa kernel ring buffer:

sudo journalctl -k | grep -i avc

Ngayon, gawing pangungusap ang record:

sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.log

Binabasa ng audit2why ang parehong records at tinutukoy ang sanhi na nakilala nito: isang boolean na naka-off, isang label na hindi tumutugma sa policy, o kawalan ng rule. Sinusuri ng sealert ang buong log at nagpi-print ng iminungkahing command para sa bawat denial. Ituring na gabay lamang ang mungkahi. Nagbabago ang wording sa iba’t ibang release, at kung minsan ay nagmumungkahi ang sealert ng custom policy module kahit isang one-line label fix ang tamang solusyon.

May isa pang mahalagang bagay. May dontaudit rules ang policy na nagtatago ng mga denial na itinuturing na harmless, kaya maaaring magkaroon ng problema ang program habang walang laman ang log. Ipakita muli ang mga ito sa tagal ng isang test:

sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -B

Ayusin ang maling label ng path gamit ang semanage fcontext at restorecon

Dalawang command ito, at mahalaga ang pagkakasunod-sunod. Itinatala ng semanage fcontext -a kung ano dapat ang label ng isang path. Inia-apply naman ng restorecon ang naitalang default sa mga file sa disk.

sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.html

Regular expression ang path. Saklaw ng (/.*)? ang mismong directory at lahat ng nasa loob nito, na kailangan para sa document root. Tingnan muna kung ano ang magbabago bago ito baguhin: ipinapakita ng sudo restorecon -Rvn /data/www ang mga relabel na isasagawa, dahil nangangahulugang walang aksyon ang -n. Pagkatapos ng aktuwal na restorecon, mababasa ang label sa httpd_sys_content_t at mawawala ang 403 nang hindi nire-restart ang service.

Gamitin lamang ang chcon bilang test. Direktang itinatakda ng chcon -t httpd_sys_content_t index.html ang label, at nire-reset ito ng susunod na restorecon, package update, o full relabel dahil sinasabi pa rin ng policy na ibang label ang dapat gamitin sa path. Ang semanage fcontext ang bersyong nananatili. Ilista ang mga naitala mo gamit ang sudo semanage fcontext -l | grep '^/data'.

Ibang type ang kailangan para sa content na dapat sulatan ng service. Gamitin ang httpd_sys_rw_content_t para sa upload directory o cache, at limitahan ito sa mga path na iyon: ang read-only site na nasa ilalim ng writable type ay nagbibigay ng mas malawak na access kaysa sa kailangan ng application.

Bakit nagkamali ang label? Halos palagi itong dahil sa paraan ng pagdating ng mga file. Pinananatili ng mv ang kasalukuyang label ng file, kaya ang site na inilipat mula sa /root ay dumarating na may label na admin_home_t at nananatiling ganoon. Binibigyan ng simpleng cp ang bagong file ng default label ng destination directory, na karaniwang siyang kailangan mo, samantalang kinokopya ng cp -a at rsync -X ang source label kasama ng file. Ang git clone papunta sa bagong top-level directory ay lumilikha ng default_t. Kapag maayos na naglo-load ang page mula sa /usr/share/nginx/html ngunit nagfa-fail mula sa sarili mong directory, ito ang dahilan.

Ayusin ang isang klase ng behavior gamit ang boolean

Hindi lahat ng failure ay problema sa label. Sa isang bagong Rocky o AlmaLinux server, nagbabalik ng 502 ang reverse proxy, at sinasabi ng error 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 upstream

Maayos ang upstream mo. Bilang default, hindi pinapayagan ng httpd_t domain na magbukas ng outbound network connections, kaya tinatanggihan ang connect() call bago ito makarating sa loopback interface. Isang switch ang kumokontrol sa buong behavior na ito:

getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on

Ang -P ang mahalagang flag: isinusulat nito sa disk ang value. Kung wala ang -P, mawawala ang pagbabago sa susunod na reboot. Magreresulta ito sa service na gumagana hanggang mag-restart ang machine. Kumpirmahin gamit ang semanage boolean -l | grep httpd_can_network_connect, na nagpi-print ng kasalukuyang value kasama ng naka-store na value.

Mas mainam na gumamit ng boolean kaysa sumulat ng sariling rule kung may available na boolean. Kasama ang mga boolean sa policy ng distribution, kaya pinapanatili at dini-document ang mga ito, at madali silang mahanap ng susunod na mag-aasikaso. Inililista ng getsebool -a ang lahat ng boolean sa system.

Pahintulutan ang service na makinig sa non-standard na port

May label din ang mga port. Ilipat ang nginx sa 8081 at tatanggi itong magsimula:

nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)

Maaaring mag-bind sa mga port na may label na http_port_t ang httpd_t, at hindi kabilang dito ang 8081. Idagdag ito:

sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081

Suriin muna ang listahan. Marami nang high port ang pinapahintulutan, kabilang ang 8008 at 8443, at mabibigo ang pagdaragdag nang dalawang beses gamit ang ValueError: Port tcp/8081 already defined. Kung pagmamay-ari na ng ibang type ang port, baguhin ito gamit ang semanage port -m -t http_port_t -p tcp 8081 sa halip na idagdag ito.

Ang parehong command ang kailangan para gumana ang inilipat na SSH port. Ibig sabihin ng Bind to port 2222 on 0.0.0.0 failed: Permission denied sa journalctl -u sshd, nawawala ang 2222 sa ssh_port_t, kaya patakbuhin ang sudo semanage port -a -t ssh_port_t -p tcp 2222 bago mong i-restart ang daemon at isara ang iyong session. Ito ang hakbang na nalalaktawan kapag sinusunod ang generic na gabay sa pag-hardening ng SSH sa isang VPS sa Red Hat family image. Hindi rin firewall ang SELinux, kaya kailangan pa ring buksan ang port: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload dito, o ufw sa isang Debian o Ubuntu image.

Kapag walang boolean at walang label na mababago

Bihira itong mangyari sa isang karaniwang server, at dito nagkakaroon ng pinsala kapag nagkamali. Maaaring bumuo ang audit2allow ng policy module mula sa mga denial sa log:

sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.pp

Basahin ang nginx_local.te bago ito i-install. May dalawang gawi para manatiling ligtas ito. I-filter ang input para sa isang program lang na inaayos mo gamit ang -c, dahil kapag ipinasa mo sa audit2allow ang isang linggong mga denial na walang kaugnayan, sabay-sabay nitong bibigyan ng pahintulot ang lahat ng iyon. Huwag kailanman mag-install ng module na binuo mula sa denial na hindi mo maipaliwanag: madaling bumuo ng rule na nagpapahintulot sa httpd_t na magbasa ng bawat file sa server, pero mahirap mapansin ito pagkalipas ng ilang buwan. Mag-alis ng module gamit ang sudo semodule -r nginx_local.

Ang permissive mode ay para sa diagnosis, hindi sa pag-aayos

sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1

Pinapayagan ng permissive mode ang access at nire-record ito sa log. Ang tunay na halaga nito ay ang pagiging kumpleto ng nakokolektang impormasyon. Sa enforcing mode, humihinto ang serbisyo sa unang denial. Kaya inaayos mo iyon, nagre-restart, at saka mo nakikita ang pangalawang denial. Sa permissive mode, nagpapatuloy ang proseso at kinokolekta ng log ang lahat ng denial sa isang pass. Pagkatapos, ibinabalik mo sa enforcing mode at inaayos ang mga ito nang sabay-sabay.

Hindi binabago ng setenforce ang /etc/selinux/config, kaya kapag nag-reboot, bumabalik ang system sa enforcing mode. Safety net ito. Ito rin ang dahilan kung bakit bumabalik sa pinakamasamang oras ang isang “fix” na binubuo lang ng setenforce 0. Kung isang serbisyo lang ang nangangailangan ng mas maluwag na policy habang inaayos mo ito, ang domain na iyon ang markahan sa halip na ang buong machine: iniiwan ng sudo semanage permissive -a httpd_t na enforcing ang lahat ng iba pa, at binabaligtad ito ng sudo semanage permissive -d httpd_t.

Bakit mas malaking gastos ang pag-disable ng SELinux kaysa sa pag-aayos ng label

Ang pag-set ng SELINUX=disabled sa /etc/selinux/config ay nagpapalit ng isang linyang pag-aayos ng label para sa server na permanenteng mas mahina ang proteksiyon. Makikita ang epekto nito kapag na-breach ang isang web application. Kapag enforcing ang mode, tumatakbo ang code ng attacker sa httpd_t, kaya maaari nitong basahin ang web content, ngunit tatanggihan ng policy ang pagbasa sa /etc/shadow o pagsusulat ng systemd unit, kahit pinahihintulutan sana ito ng Unix user. Kapag walang naka-load na policy, makukuha ng parehong code ang lahat ng pahintulot ng service account.

May kapalit din ang pag-disable na babayaran mo sa hinaharap. Habang walang naka-load na policy, ginagawa ang mga bagong file nang walang label, kaya unti-unting hindi naaayon ang filesystem sa policy. Kapag ibinalik ang SELinux, kailangan ng full relabel; kung hindi, sabay-sabay na maaaring mag-fail ang maraming serbisyo:

sudo fixfiles -F onboot
sudo reboot

Isinusulat nito ang /.autorelabel at nire-relabel ang bawat filesystem sa susunod na boot. Sa malaking disk, matagal itong matapos at maaaring magmukhang naka-stuck ang console, kaya simulan ito kapag makakapaghintay ka. Sa Rocky Linux at AlmaLinux 9, hindi na awtomatikong dini-disable ng config file ang bahagi ng SELinux sa kernel, at ang dokumentadong paraan para ganap na i-disable ang SELinux ay isang kernel argument (sudo grubby --update-kernel ALL --args selinux=0). Kapaki-pakinabang ang command na iyon kapag nagmana ka ng server ng ibang tao. Hindi ito ang solusyon sa 403.

Magdagdag ng isang label sa Containers

Sa isang host na kabilang sa Red Hat family, tumatakbo ang mga proseso ng container sa container_t at maaari lamang magbasa ng mga file na may label na container_file_t. Nabibigo ang bind mount mula sa host na may Permission denied sa loob ng container, kahit mukhang normal ang ls -l sa host. Sinasabi ng suffix na :Z sa runtime na i-relabel ang mount:

docker run -d -v /data/appdata:/var/lib/app:Z myimage

Nilalagyan ng :Z ang directory para sa container na ito lamang. Nilalagyan naman ito ng :z para sa pagbabahagi sa pagitan ng mga container. Kapag itinuro mo ang :Z sa directory na ginagamit ng iba pang serbisyo, nire-relabel nito ang directory nang recursive at naaapektuhan ang mga serbisyong iyon, kaya gumamit ng sariling path para sa mga container. Ang lahat ng iba pang bahagi ng setup ay kapareho ng sa anumang ibang image, gaya ng nakasaad sa pagpapatakbo ng Docker sa isang VPS.

May AppArmor ang Ubuntu at Debian

Pareho ang layunin, pero magkaiba ang disenyo. Kinokontrol ng AppArmor ang isang program batay sa path ng executable nito, gamit ang profile sa ilalim ng /etc/apparmor.d/, sa halip na lagyan ng label ang mga file sa disk. Walang kailangang i-relabel at walang restorecon. Magsimula rito:

sudo aa-status
sudo journalctl -k | grep -i apparmor

Lumilitaw ang isang pagtanggi bilang apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Pareho ang anyo ng workflow: basahin ang denial, hanapin ang profile, at baguhin ang rule. Ibinibigay sa iyo ng sudo apt install apparmor-utils ang aa-complain (permissive para sa isang profile), at ang aa-enforce upang ibalik ito. Kino-confine ng Ubuntu ang isang piling set ng packaged service at iniiwang unconfined ang iba, kaya basahin ang aa-status upang makita kung ano talaga ang active sa halip na manghula.

May isang nakasanayan na naaangkop sa parehong system. Kapag nag-ulat ang isang service ng Permission denied sa isang bagay na mukhang tama, basahin muna ang security log bago galawin ang permissions. Bihirang dalawang beses na ang permission bits ang problema.

FAQ

Bakit nagbabalik ng 403 ang nginx kahit tama ang file permissions?

Dahil hinarang ng SELinux ang pagbasa, hindi ng permission bits. Tumatakbo ang web server sa httpd_t domain at maaari lamang magbasa ng mga file na may label para sa web content. Kaya tinatanggihan ang file na may label na default_t o admin_home_t, at walang maihahatid ang nginx. Kumpirmahin ito gamit ang sudo ausearch -m AVC -ts recent. Ipinapakita nito ang scontext na nagtatapos sa httpd_t at ang tcontext na may maling type. Itala ang tamang label at ilapat ito: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" na sinusundan ng sudo restorecon -Rv /data/www.

Ligtas bang patakbuhin ang setenforce 0 para gumana ang isang serbisyo?

Ang setenforce 0 ay diagnostic step, hindi fix. Gamitin ito upang isang beses na i-reproduce ang problema para makolekta ng log ang lahat ng denial sa iisang pass. Basahin ang mga ito gamit ang sudo ausearch -m AVC -ts recent. Pagkatapos, patakbuhin ang sudo setenforce 1 at ayusin ang mga sanhi. Ang server na naka-permissive mode ay nagla-log ng bawat denial ngunit walang hinaharang. Naiiwan sa iyo ang ingay sa log at nawawala ang proteksiyon. Kung kailangan ng isang serbisyo ng puwang habang nagtatrabaho ka, patakbuhin ang sudo semanage permissive -a httpd_t upang manatiling enforcing ang iba pang bahagi ng machine.

Paano ako magpapatakbo ng serbisyo sa non-standard port kapag enforcing ang SELinux?

Idagdag ang port sa type na pinapayagang pag-bind-an ng serbisyo. Para sa web server sa 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Para sa SSH sa 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Suriin muna ang kasalukuyang listahan gamit ang sudo semanage port -l | grep -w http_port_t, dahil magfa-fail ang port na nakalista na gamit ang ValueError: Port tcp/8081 already defined. Kung hindi ito gagawin, lalabas ang daemon sa startup dahil sa bind() ... Permission denied kahit walang ibang process na gumagamit ng port.

May SELinux ba ang Ubuntu?

Wala. Ang Ubuntu at Debian ay may AppArmor, na nagpapatupad ng profile na nakatali sa path ng executable sa halip na sa mga label ng file. Suriin ito gamit ang sudo aa-status at hanapin ang mga linyang apparmor="DENIED" sa sudo journalctl -k. Kino-confine ng Ubuntu ang piling packaged service, kaya maraming program ang default na tumatakbong unconfined. Sa Rocky Linux at AlmaLinux mo karaniwang makikita ang SELinux na enforcing out of the box, gayundin sa Fedora at RHEL.