SELinux 基礎:nginx 為何回傳 403?
nginx 對權限正確的檔案回傳 403,通常是 SELinux 標籤遭拒。學會讀取 denial,使用 semanage 與 restorecon 修正,並維持 Enforcing。
nginx 對權限正確的檔案回傳 403 的原因
nginx 對權限位元正確的檔案回傳 403,幾乎總是因為 SELinux(security-enhanced Linux)拒絕讀取。正常權限檢查通過後,SELinux 還會套用另一組規則。Web 伺服器只能讀取帶有 Web 內容標籤的檔案。您的檔案帶有不同的標籤,因此 open 失敗,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.htmldrwxr-xr-x 後方顯示的句點表示檔案帶有 SELinux 標籤。default_t 是政策從未識別該路徑時所使用的類型,Web 伺服器的規則不允許讀取這種類型。錯誤日誌會顯示一般 Unix 錯誤,因此看起來像是權限問題:
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"對一般拒絕和 SELinux 拒絕這兩種情況,核心都會回傳 13: Permission denied。因此,第一步是確認拒絕來自哪一層。不要先執行 setenforce 0。
您需要掌握的模型部分
SELinux 是強制存取控制,通常寫作 MAC。每個程序都在某個網域中執行,例如 Web 伺服器使用的 httpd_t。每個檔案與網路連接埠都具有類型,例如 httpd_sys_content_t。政策列出允許的網域、類型與動作組合;未列出的組合一律拒絕。SELinux 會在傳統 Unix 檢查之後執行,因此 drwxr-xr-x 中的權限位元仍必須先允許存取。兩層檢查都必須通過。
完整的 context 由冒號分隔的四個欄位組成,例如 system_u:system_r:httpd_t:s0:SELinux 使用者、角色、類型與層級。在伺服器上,您幾乎總是處理第三個欄位,也就是類型。以下兩個命令可顯示目前值:
ps -eZ | grep nginx
id -Znginx worker 的 context 會以 httpd_t 結尾。您的登入 shell 會顯示 unconfined_u:unconfined_r:unconfined_t:s0,因為預設的 targeted policy 會限制服務,並讓互動式使用者不受限制。這點很重要,因為 SELinux 不會取代讓服務以最小權限使用者執行。當有人入侵服務後,SELinux 會限制該服務可存取的內容。
3 種模式,以及哪些映像檔具備 SELinux
sestatus
getenforceEnforcing 會封鎖並記錄事件。Permissive 允許所有操作,但會記錄原本會封鎖的操作。Disabled 完全不載入任何政策。getenforce 會顯示目前模式。sestatus 也會顯示 /etc/selinux/config 中設定的模式;重新開機後會恢復為此模式。
Rocky Linux、AlmaLinux、Fedora 和 RHEL 預設以 Enforcing 模式搭配 targeted 政策發行。Ubuntu 和 Debian 則改用 AppArmor,功能相同,但採用不同機制(最後一節會介紹)。因此,同一個應用程式可能在其中一台伺服器上順利安裝,卻在另一台伺服器上回傳 403。
在需要之前先安裝工具
sudo dnf install -y policycoreutils-python-utils setroubleshoot-server在精簡映像上,semanage: command not found 表示缺少 policycoreutils-python-utils:該套件包含 semanage 與 audit2allow。setroubleshoot-server 會加入 sealert,並將每項拒絕的簡明英文摘要寫入 journal。請在新伺服器上同時安裝這兩者,因為真正需要它們時,通常已經有某些項目無法正常運作。
如何讀取 audit log 中的 SELinux 拒絕記錄
每次拒絕都會由 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 是來源 context,也就是程序執行所在的 domain。tcontext 是目標 context,也就是程序嘗試存取之物件上的 label。tclass 是物件類型;此處為檔案。合併解讀後,意思是:在 httpd_t 中執行的程序嘗試讀取標示為 default_t 的檔案,而 permissive=0 表示該請求確實遭到阻擋,不只是被記錄而已。
如果 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、不符合 policy 的 label,或根本沒有規則。sealert 會掃描整份 log,並為每次拒絕輸出一個建議指令。請將該建議視為參考。不同 release 的文字可能不同,而且 sealert 有時會建議建立自訂 policy module,但正確作法其實只是修正一行 label。
還有一點需要注意。policy 包含 dontaudit 規則,會隱藏被視為無害的拒絕記錄。因此,即使 log 沒有內容,程式仍可能發生異常。請在單次測試期間取消隱藏:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -B使用 semanage fcontext 與 restorecon 修正錯誤標記的路徑
只需執行 2 個命令,而且順序很重要。semanage fcontext -a會記錄路徑應有的標記。restorecon會將該記錄的預設值套用至磁碟上的檔案。
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.html此路徑使用正規表示式。(/.*)?涵蓋目錄本身及其下的所有內容,這正是文件根目錄所需的設定。修改前先查看可能的變更:sudo restorecon -Rvn /data/www會列出預計重新標記的項目,因為 -n表示不執行任何動作。實際執行 restorecon後,標記會顯示為 httpd_sys_content_t,403 錯誤也會消失,無須重新啟動服務。
chcon只能用於測試。chcon -t httpd_sys_content_t index.html會直接設定標記;下一次 restorecon、套件更新或完整重新標記時,該設定就會被重設,因為 policy 仍指定此路徑應使用其他標記。semanage fcontext才是能持續保留的設定。使用 sudo semanage fcontext -l | grep '^/data'列出已記錄的設定。
服務需要寫入的內容必須使用不同的類型。上傳目錄或快取請使用 httpd_sys_rw_content_t,並限制在這些路徑;將唯讀網站放在可寫入類型下,會提供超出應用程式需求的權限。
為什麼標記一開始會錯?幾乎總是因為檔案的來源方式。mv會保留檔案現有的標記,因此從 /root移出的網站會帶著 admin_home_t標記抵達,並維持該標記。一般的 cp會讓新檔案使用目的地目錄的預設標記,通常這正是需要的結果;但 cp -a與 rsync -X會連同來源標記一起複製。將檔案 git clone至新的頂層目錄時,會產生 default_t。如果頁面從 /usr/share/nginx/html載入正常,但從自己的目錄載入失敗,原因就在這裡。
以布林值修正一類行為
有些故障不是標籤問題。全新的 Rocky 或 AlmaLinux 伺服器上,反向代理回傳 502,錯誤日誌顯示:
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上游服務正常。httpd_t domain 預設不允許開啟對外網路連線,因此 connect() 呼叫在到達 loopback 介面前就遭到拒絕。只要一個設定即可控制整個行為:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P 是關鍵旗標,會將值寫入磁碟。若沒有 -P,變更會在下次重新開機時遺失,導致服務只能運作到機器重新啟動為止。使用 semanage boolean -l | grep httpd_can_network_connect 確認;它會在儲存值旁顯示目前執行中的值。
只要有可用的布林值,優先使用布林值,不要手動撰寫規則。布林值隨發行版政策提供,因此會持續維護、有文件說明,也方便下一位管理員查找。getsebool -a 會列出系統上的所有布林值。
讓服務監聽非標準埠
埠也有標籤。將 nginx 移至 8081 後,服務拒絕啟動:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t 可以繫結標記為 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 埠正常運作,也要使用相同的指令。journalctl -u sshd 中的 Bind to port 2222 on 0.0.0.0 failed: Permission denied 表示 2222 不在 ssh_port_t 內,因此請在重新啟動 daemon 並關閉工作階段前,先執行 sudo semanage port -a -t ssh_port_t -p tcp 2222。在 Red Hat 系列映像檔上,依照一般指南強化 VPS 上的 SSH時,這是人們常忽略的步驟。SELinux 也不是防火牆,因此仍必須開放該埠:此處使用 sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload,或在 Debian 或 Ubuntu 映像檔上使用ufw。
沒有可變更的布林值或標籤時
在一般伺服器上,這種情況很少見,也是最容易造成損害的情況。audit2allow 可以根據日誌中的拒絕事件建立政策模組:
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,會一次授予所有這些權限。不要安裝由無法解釋的拒絕事件建立的模組:允許 httpd_t 讀取伺服器上所有檔案的規則很容易產生,卻很難在數月後察覺。使用 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 1Permissive 模式會允許存取,並將事件寫入日誌。它的真正價值在於完整性。在 enforcing 模式下,服務會在遇到第一個拒絕時停止。因此,你修正該問題並重新啟動服務後,才會遇到第二個拒絕。Permissive 模式會讓執行流程繼續,並在單次執行中收集所有拒絕事件的日誌。接著切回 enforcing,再一次修正這些問題。
setenforce 不會修改 /etc/selinux/config,因此重新開機後系統會回到 enforcing。這是安全防護機制,也說明了為何只執行 setenforce 0 的「修正」會在最不適當的時機再次出現。如果你在處理某項服務時需要暫時放寬限制,請標記該網域,而不是整台機器:sudo semanage permissive -a httpd_t 會讓其他部分維持 enforcing,sudo semanage permissive -d httpd_t 則可還原設定。
停用 SELinux 的代價高於修正標籤
在 /etc/selinux/config 中設定 SELINUX=disabled,等於用一行標籤修正,換取永久較弱的伺服器安全性。當 Web 應用程式遭到入侵時,兩者的差異就會顯現。在 enforcing 模式下,攻擊者的程式會以 httpd_t 執行,因此可能讀取 Web 內容;但無論 Unix 使用者原本具備何種權限,依據政策,讀取 /etc/shadow 或寫入 systemd unit 都會被拒絕。若未載入政策,相同程式就能取得服務帳號擁有的全部權限。
停用 SELinux 也會產生延後支付的代價。未載入政策期間建立的新檔案不會有標籤,因此檔案系統會逐漸偏離政策狀態。之後重新啟用 SELinux 時,就必須完整重新標記,否則會導致大量服務同時失敗:
sudo fixfiles -F onboot
sudo reboot這會寫入 /.autorelabel,並在下一次開機期間重新標記每個檔案系統。大型磁碟需要很長時間,主控台看起來也像是卡住,因此請在能夠等待時執行。在 Rocky Linux 和 AlmaLinux 9 中,設定檔不再自行停用核心層的 SELinux;完整停用 SELinux 的文件化方式是使用核心參數(sudo grubby --update-kernel ALL --args selinux=0)。接手他人伺服器時,了解這個指令會很有幫助。但它不是修正 403 的方法。
容器會多加一個標籤
在 Red Hat 系列主機上,容器程序會在 container_t 中執行,且只能讀取標記為 container_file_t 的檔案。從主機繫結掛載的目錄,在容器內會出現 Permission denied,但主機上的 ls -l 看起來完全正常。:Z 後綴會要求執行環境重新標記掛載點:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z 會只為這個容器標記目錄。:z 會將目錄標記為可供多個容器共用。若將 :Z 指向其他服務使用的目錄,系統會遞迴重新標記該目錄,導致那些服務無法正常運作,因此應為容器提供專用路徑。其餘設定與其他映像檔相同,詳見在 VPS 上執行 Docker。
Ubuntu 與 Debian 提供 AppArmor
用途相同,但設計不同。AppArmor 會依據程式可執行檔的路徑限制程式,使用 /etc/apparmor.d/ 下的設定檔,而不是為磁碟上的檔案加上標籤。不需要重新標記,也沒有 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,但相關設定看似正確時,請先讀取安全性日誌,再修改權限。權限位元通常不會連續兩次造成問題。
FAQ
為什麼檔案權限正確時,nginx 仍回傳 403?
因為 SELinux 拒絕讀取,而不是權限位元有問題。Web 伺服器在 httpd_t domain 中執行,只能讀取標記為 Web 內容的檔案,因此標記為 default_t 或 admin_home_t 的檔案會遭拒絕,nginx 也就沒有內容可提供。使用 sudo ausearch -m AVC -ts recent 確認;該指令會顯示 scontext 以 httpd_t 結束,而 tcontext 保留了錯誤的類型。接著記錄正確的標籤並套用:sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?",然後執行 sudo restorecon -Rv /data/www。
執行 setenforce 0 讓服務恢復運作安全嗎?
setenforce 0 是診斷步驟,不是修正方法。使用它重現一次問題,讓日誌在單次檢查中收集所有拒絕事件,再以 sudo ausearch -m AVC -ts recent 讀取這些事件,然後執行 sudo setenforce 1 並修正原因。處於 permissive 模式的伺服器會記錄所有拒絕事件,但不會阻擋任何操作,因此只會保留大量雜訊,卻失去防護。如果某項服務在處理期間需要暫時放寬限制,請執行 sudo semanage permissive -a httpd_t,讓機器的其餘部分維持 enforcing。
如何在 SELinux enforcing 模式下,讓服務使用非標準連接埠?
將連接埠加入該服務允許繫結的類型。Web 伺服器使用 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 檢查,並在 sudo journalctl -k 中尋找 apparmor="DENIED" 行。Ubuntu 只會限制一部分套件服務,因此許多程式預設以 unconfined 模式執行。Rocky Linux 和 AlmaLinux 預設啟用 SELinux enforcing,Fedora 和 RHEL 也是如此。