SELinux sa Server: Bakit 403 ang nginx
Nakikita ang 403 kahit tama ang permissions? Basahin ang SELinux denial, ayusin ang label gamit ang semanage at restorecon, at panatilihing enforcing.
Bakit nagbabalik ang nginx ng 403 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 ang 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 maipadalang content ang nginx.
Suriin ang label, hindi lamang ang mode:
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.htmlAng tuldok na naka-print pagkatapos ng drwxr-xr-x ay nangangahulugang may SELinux label ang file. Ang default_t ang nakukuha ng isang path kapag hindi pa ito nakikilala ng policy, at walang rule ng web server na nagpapahintulot sa pagbasa ng ganoong type. Nagpapakita ang error log ng karaniwang Unix error, kaya nagmumukha itong problema sa permissions:
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 karaniwang pagtanggi at ang pagtanggi ng SELinux. Kaya ang unang dapat gawin 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 tinatawag na MAC. Tumatakbo ang bawat process 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. Listahan ang policy 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 mga permission bit sa drwxr-xr-x ang access. Dapat parehong magbigay ng pahintulot ang dalawang layer.
May apat na field ang buong context na pinaghiwalay ng mga colon, gaya ng system_u:system_r:httpd_t:s0: ang SELinux user, role, type, at level. Sa isang server, halos palagi mong ginagamit ang ikatlong field, ang type. Dalawang command ang nagpapakita ng kasalukuyang values:
ps -eZ | grep nginx
id -ZAng nginx workers ay may context na nagtatapos sa httpd_t. Ang iyong login shell ay may unconfined_u:unconfined_r:unconfined_t:s0, dahil kino-confine ng default na targeted policy ang mga service at hindi nito kino-confine ang mga interactive user. Mahalagang malaman ito dahil hindi pinapalitan ng SELinux ang pagpapatakbo ng mga service gamit ang mga user na may pinakamababang pribilehiyo. Nililimitahan nito ang mga maaabot ng isang service matapos itong mapasok ng attacker.
Ang tatlong mode, at kung aling mga image ang may SELinux
sestatus
getenforceAng Enforcing ay nagba-block at nagla-log. Pinapayagan ng Permissive ang lahat at nilo-log kung ano ang bina-block sana nito. Hindi naglo-load ng anumang policy ang Disabled. Ipinapakita ng getenforce ang kasalukuyang mode. Ipinapakita rin ng sestatus ang mode mula sa /etc/selinux/config, na siyang bumabalik pagkatapos ng reboot.
Ang Rocky Linux, AlmaLinux, Fedora, at RHEL ay may SELinux na naka-enforce gamit ang targeted policy. Pareho ang default na ito dahil nagmula ang apat sa iisang Red Hat lineage na dumaan sa CentOS bago lumitaw ang Rocky Linux at AlmaLinux. Walang pinagkaiba kung alin sa dalawa ang gagamitin mo para sa anumang nasa pahinang ito, dahil pareho ang policy at mga tool na kasama sa mga ito. Kaya ang pagpili sa pagitan ng Rocky Linux at AlmaLinux ay nakabatay sa mga compatibility promise ng mga ito at suporta sa mas lumang CPU, hindi sa security default. AppArmor naman ang kasama ng Ubuntu at Debian. Pareho ang layunin nito, pero iba ang mekanismo nito (saklaw ito ng huling section). Kaya maaaring malinis na ma-install ang parehong application sa isa sa mga server mo pero magbalik ng 403 sa isa pa.
I-install ang mga tool bago mo kailanganin ang mga ito
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found sa isang minimal image ay nangangahulugang wala ang policycoreutils-python-utils: naglalaman ang package na iyon ng semanage at audit2allow. Idinaragdag ng setroubleshoot-server ang sealert at nagsusulat ito ng buod sa payak na English tungkol sa bawat denial sa journal. I-install ang dalawa sa isang bagong server, dahil kapag kailangan mo na ang mga ito, may nasira na.
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 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=0Apat na field ang naglalaman ng buong detalye. 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 i-access. Ang tclass ay ang uri ng object, na file dito. Kapag pinagsama, ganito ang ibig sabihin: 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 avcGawin ngayong isang pangungusap ang record:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logBinabasa ng audit2why ang parehong mga record at tinutukoy ang cause na nakikilala nito: isang boolean na naka-off, isang label na hindi tugma sa policy, o kawalan ng rule. Sinusuri ng sealert ang buong log at nagpi-print ng isang iminungkahing command para sa bawat denial. Ituring na hint lamang ang suggestion. 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 dapat malaman. May dontaudit rule ang policy na nagtatago ng mga denial na itinuturing na harmless, kaya maaaring magkaroon ng problema ang isang program kahit walang laman ang log. Ipakita pansamantala ang mga ito habang isinasagawa ang isang test:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BAyusin ang maling label ng path gamit ang semanage fcontext at restorecon
Dalawang command ito, at mahalaga ang pagkakasunod. Itinatala ng semanage fcontext -a kung ano dapat ang label ng isang path. Inilalapat 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.htmlRegular expression ang path. Sinasaklaw ng (/.*)? ang mismong directory at lahat ng nasa loob nito, na kailangan ng document root. Tingnan muna kung ano ang magbabago bago ito ilapat: ipinapakita ng sudo restorecon -Rvn /data/www ang mga nakaplanong relabel, dahil walang ginagawang pagbabago 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 iba dapat ang label ng path. Sa isang box na ang dnf-automatic ay nag-aapply ng security updates ayon sa timer, mangyayari ang reset sa sarili nitong schedule sa halip na habang nasa harap ka ng machine. Dahil dito, maaaring masira ang site ilang oras pagkatapos ng huli mong binago. Ang semanage fcontext ang bersyong nananatili. Ilista ang mga naitala mo gamit ang sudo semanage fcontext -l | grep '^/data'.
Kailangan ng ibang type ang 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. Kapag read-only ang site ngunit nasa writable type, nagbibigay ito ng mas malawak na access kaysa sa kailangan ng application.
Bakit naging mali ang label? Halos palaging 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. Ang simpleng cp ay nagbibigay sa bagong file ng default label ng destination directory, na karaniwang siyang gusto mo. Samantala, kinokopya ng cp -a at rsync -X ang source labels kasama ng file. Ang git clone papunta sa bagong top-level directory ay nagreresulta sa 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 box, 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 upstreamMaayos ang upstream mo. Bilang default, hindi pinapayagan ng httpd_t domain na magbukas ng outbound network connections, kaya nire-refuse 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 onAng -P ang flag na mahalaga: isinusulat nito sa disk ang value. Kung wala ang -P, mawawala ang pagbabago sa susunod na reboot, kaya gagana ang service hanggang sa mag-restart ang machine. Kumpirmahin gamit ang semanage boolean -l | grep httpd_can_network_connect, na nagpi-print ng kasalukuyang value kasabay ng naka-store na value.
Mas mainam gumamit ng boolean kaysa magsulat ng sariling rule kapag may available na boolean. Kasama ang booleans 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.
Hay mga 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 ang httpd_t sa mga port na may label na http_port_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 8081Suriin muna ang listahan. Pinapayagan na ang ilang high port, kabilang ang 8008 at 8443, at mabibigo ang pagdaragdag nang dalawang beses na may ValueError: Port tcp/8081 already defined. Kung may ibang type na 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 nagpapagana sa 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 session mo. Ito ang hakbang na nalalaktawan kapag sinusunod ang generic na gabay sa pagpapatibay ng SSH sa isang VPS na gumagamit ng Red Hat family image. Hindi rin firewall ang SELinux, kaya kailangan pa ring bukas ang port: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload dito, o ufw sa Debian o Ubuntu image. Ang --permanent flag ay may kaparehong reboot trap gaya ng -P sa isang boolean, at makabubuting basahin kahit isang beses ang mga zone na tumutukoy kung aling interface ang saklaw ng isang rule sa mga batayan ng firewalld para sa Rocky o AlmaLinux VPS.
Kapag walang boolean at walang label na babaguhin
Bihira itong mangyari sa isang normal na server, at dito madalas nagdudulot ng pinsala ang mga tao. 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.ppBasahin ang nginx_local.te bago ito i-install. May dalawang gawi na nakatutulong para manatiling ligtas ito. I-filter ang input sa iisang program na inaayos mo gamit ang -c, dahil kapag nag-pipe ka ng isang linggong mga denial na walang kaugnayan sa audit2allow, sabay-sabay nitong ibinibigay ang pahintulot para sa 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 itong mapansin pagkalipas ng ilang buwan. Mag-alis ng module gamit ang sudo semodule -r nginx_local.
Ang permissive ay diagnostic mode, hindi pag-aayos
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1Pinapahintulutan ng permissive mode ang access at nire-record ito sa log. Ang tunay na pakinabang nito ay ang pagiging kumpleto ng diagnosis. Sa enforcing mode, humihinto ang serbisyo sa unang denial. Aayusin mo iyon, magre-restart, at saka mo makikita ang pangalawang denial. Sa permissive mode, nagpapatuloy ang run at kinokolekta ng log ang lahat ng denial sa isang pass. Pagkatapos, ibabalik mo sa enforcing mode at aayusin ang mga ito nang sabay-sabay.
Hindi binabago ng setenforce ang /etc/selinux/config, kaya ibinabalik ng reboot ang system sa enforcing mode. Safety net ito. Dahil dito, muling lumilitaw sa pinakamasamang oras ang isang “fix” na binubuo lamang ng setenforce 0. Kung kailangang bigyan ng luwag ang isang serbisyo habang inaayos mo ito, ang domain nito ang markahan sa halip na ang buong machine: iniiwan ng sudo semanage permissive -a httpd_t na enforcing ang lahat ng iba pa, at binabawi ito ng sudo semanage permissive -d httpd_t.
Bakit mas malaking gastos ang pag-disable ng SELinux kaysa sa pag-aayos ng label
Ang pagtatakda ng SELINUX=disabled sa /etc/selinux/config ay ipinagpapalit ang isang linyang pag-aayos ng label sa permanenteng mas mahinang seguridad ng server. Makikita ang pagkakaibang ito kapag na-compromise ang isang web application. Kapag enforcing ang mode, tumatakbo ang code ng attacker sa httpd_t, kaya maaari nitong basahin ang web content, pero tatanggihan ng policy ang pagbasa sa /etc/shadow o pagsusulat sa isang systemd unit, anuman ang pinapahintulutan ng Unix user. Kapag walang naka-load na policy, makukuha ng parehong code ang lahat ng access ng service account.
May karagdagang epekto rin ang pag-disable na lalabas sa hinaharap. Habang walang naka-load na policy, nalilikha ang mga bagong file nang walang label, kaya hindi na tugma ang filesystem sa policy. Kapag muling ini-enable ang SELinux, kailangan ng buong relabel, kung hindi ay maaaring sabay-sabay bumagsak ang maraming serbisyo:
sudo fixfiles -F onboot
sudo rebootIsinusulat nito ang /.autorelabel at nire-relabel ang bawat filesystem sa susunod na boot. Sa malaking disk, matagal ito at maaaring magmukhang natigil ang console, kaya simulan ito kapag makapaghihintay ka. Dahil magda-down din ang machine, sulit na tingnan muna kung ano pa ang nakapila para sa restart, na siyang iniuulat ng needs-restarting matapos mag-iwan ang dnf update ng mga lumang kernel at library sa memory. Sa Rocky Linux at AlmaLinux 9, hindi na awtomatikong dini-disable ng config file ang bahagi nito 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). Makakatulong ang kaalaman sa command na iyon kapag nagmana ka ng server ng ibang tao. Hindi ito ang solusyon sa 403.
Mga container, may dagdag na label
Sa 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 myimageNilalagyan ng :Z ang directory para sa container na ito lamang. Nilalagyan naman ito ng :z para maibahagi sa maraming container. Kapag itinuro ang :Z sa directory na ginagamit ng iba pang serbisyo, nire-relabel nito nang recursive ang directory at nasisira ang mga serbisyong iyon. Kaya gumamit ng sariling path ang mga container. Kung wala pa sa machine ang engine, tandaan na ang command na docker sa mga distribution na ito ay kadalasang podman na sumasagot sa pangalan nito. Inaayos ng mga hakbang sa pag-install sa Rocky at AlmaLinux ang detalyeng ito bago mo ito kailanganin. Ang iba pang bahagi ng setup ay kapareho ng sa anumang image, gaya ng ipinaliwanag sa pagpapatakbo ng Docker sa isang VPS.
Binibigyan ka ng Ubuntu at Debian ng AppArmor
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 apparmorLumilitaw ang isang pagtanggi bilang apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Pareho ang daloy ng trabaho: basahin ang denial, hanapin ang profile, at baguhin ang rule. Ibinibigay ng sudo apt install apparmor-utils ang aa-complain (permissive para sa isang profile), at ginagamit ang aa-enforce upang ibalik ito. Kino-confine ng Ubuntu ang piling packaged services at hinahayaang unconfined ang iba, kaya basahin ang aa-status upang makita kung ano talaga ang active sa halip na manghula.
May isang nakasanayang hakbang na pareho sa dalawang system. Kapag nag-ulat ang isang service ng Permission denied sa isang bagay na mukhang tama, basahin muna ang security log bago baguhin ang permissions. Bihirang dalawang beses na permissions ang problema.
FAQ
Bakit nagbabalik ang nginx ng 403 kahit tama ang file permissions?
Dahil hinarangan ng SELinux ang pagbasa, hindi ng permission bits. Tumatakbo ang web server sa httpd_t domain at maaari lamang bumasa 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, na nagpapakita ng scontext na nagtatapos sa httpd_t at ng tcontext na may maling type. Itala ang tamang label at ilapat ito: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" na sinundan ng sudo restorecon -Rv /data/www.
Ligtas bang patakbuhin ang setenforce 0 para gumana ang isang serbisyo?
Ang setenforce 0 ay hakbang para sa diagnosis, hindi pag-aayos. Gamitin ito upang isang beses na i-reproduce ang problema para maitala ng log ang lahat ng denial sa iisang pass. Basahin ang mga ito gamit ang sudo ausearch -m AVC -ts recent, pagkatapos ay patakbuhin ang sudo setenforce 1 at ayusin ang mga sanhi. Kapag naiwan sa permissive mode ang server, itinatala nito ang bawat denial ngunit wala itong bina-block. Naiiwan sa iyo ang ingay sa log at nawawala ang proteksiyon. Kung isang serbisyo lamang ang kailangang bigyan ng puwang habang nagtatrabaho ka, patakbuhin ang sudo semanage permissive -a httpd_t upang manatiling enforcing ang natitirang bahagi ng machine.
Paano ako magpapatakbo ng serbisyo sa non-standard na port habang 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 mabibigo ang port na nakalista na kapag ginamit ang ValueError: Port tcp/8081 already defined. Kung lalaktawan ang hakbang na ito, lalabas ang daemon sa pagsisimula nito na may 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 nakabatay 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. Kinokontrol ng Ubuntu ang piling packaged services, kaya maraming program ang default na tumatakbo nang unconfined. Sa Rocky Linux at AlmaLinux mo karaniwang makikita ang SELinux na enforcing out of the box, pati sa Fedora at RHEL.