SELinux 造成 nginx 403:如何找出並修正標籤問題
nginx 回傳 403,但 Unix 權限看似正確?查看 SELinux denial,使用 semanage 設定檔案標籤,再以 restorecon 修正,維持 Enforcing 模式。
nginx 對權限正確的檔案回傳 403 的原因
nginx 對權限位元正確的檔案回傳 403,幾乎總是因為 SELinux(security-enhanced Linux)拒絕讀取。一般權限檢查通過後,SELinux 還會套用另一組規則。Web server 只能讀取帶有 Web 內容標籤的檔案。你的檔案帶有不同的標籤,因此 open 失敗,nginx 沒有內容可傳送。
請檢查標籤,不要只看 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.htmldrwxr-xr-x 後方顯示的點,表示檔案帶有 SELinux 標籤。default_t 是政策從未識別該路徑時所使用的類型,而 Web server 的規則不允許讀取這種類型。錯誤日誌會顯示一般 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"對這兩種拒絕,核心都會回傳 13: Permission denied:一般權限拒絕,以及 SELinux 拒絕。因此,第一步是確認拒絕來自哪一層。不要先執行 setenforce 0。
您需要了解的模型部分
SELinux 是強制存取控制,通常縮寫為 MAC。每個程序都在某個網域中執行,例如 Web 伺服器使用的 httpd_t。每個檔案與網路埠都帶有類型,例如 httpd_sys_content_t。政策列出網域、類型與動作的允許組合,未列入的組合一律拒絕。SELinux 會在傳統 Unix 檢查之後執行,因此 drwxr-xr-x 中的權限位元仍必須先允許存取。兩層檢查都必須允許。
完整的 context 有 4 個以冒號分隔的欄位,例如 system_u:system_r:httpd_t:s0:SELinux 使用者、角色、類型與層級。在伺服器上,您幾乎只會處理第三個欄位,也就是類型。以下 2 個命令可以顯示目前值:
ps -eZ | grep nginx
id -Znginx worker 的 context 會以 httpd_t 結尾。您的登入 shell 則會顯示 unconfined_u:unconfined_r:unconfined_t:s0,因為預設的 targeted 政策會限制服務,並讓互動式使用者不受限制。這點很重要,因為 SELinux 不會取代讓服務以最小權限使用者執行。當有人入侵服務後,SELinux 會限制該服務可以存取的資源。
3 種模式,以及哪些映像檔具備 SELinux
sestatus
getenforceEnforcing 會阻擋並記錄。Permissive 允許所有操作,並記錄原本會阻擋的項目。Disabled 完全不載入任何政策。getenforce 會顯示目前模式。sestatus 也會顯示來自 /etc/selinux/config 的模式;這是重新開機後會恢復的模式。
Rocky Linux、AlmaLinux、Fedora 和 RHEL 出廠時都會以 Enforcing 模式搭配 targeted 政策執行 SELinux。這項共用的預設值是繼承而來,並非巧合,因為這 4 個發行版都源自 相同的 Red Hat 系譜,該系譜在 Rocky Linux 和 AlmaLinux 出現前曾經歷 CentOS。使用哪一個對本頁內容都沒有影響,因為它們採用相同的政策與工具。因此,選擇 Rocky Linux 或 AlmaLinux 取決於兩者對相容性的承諾與對較舊 CPU 的支援,而不是安全性預設值。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四個欄位即可說明完整情況。comm 是遭封鎖的程式。scontext 是來源 context,也就是程序執行所在的 domain。tcontext 是目標 context,也就是該程序嘗試存取之物件上的 label。tclass 是物件類型,此處為 file。合併解讀後,意思是: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 修正標籤錯誤的路徑
需要執行兩個命令,而且順序很重要。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、套件更新或完整重新標籤都會將其重設,因為政策仍指定該路徑應使用其他標籤。在 dnf-automatic 依排程套用安全性更新 的伺服器上,這項重設會依自己的排程發生,而不是在你坐在機器前操作時發生,因此網站可能在你上次操作數小時後才中斷。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 網域不允許開啟對外網路連線,因此 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 埠時,也必須使用相同的指令才能讓 SSH 正常運作。journalctl -u sshd 中的 Bind to port 2222 on 0.0.0.0 failed: Permission denied 表示 ssh_port_t 缺少 2222,因此請在重新啟動 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。--permanent 旗標與布林值上的 -P 一樣,具有相同的重新開機陷阱;此外,決定規則套用哪些介面的 zone,也值得在Rocky 或 AlmaLinux VPS 的 firewalld 基礎中閱讀一次。
沒有可變更的 boolean 和 label 時
這種情況在一般伺服器上很少見,也是最容易造成損害的情況。audit2allow 可以根據日誌中的拒絕事件建立 policy module:
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 讀取系統上所有檔案的規則很容易產生,卻很難在數個月後察覺。使用 sudo semodule -r nginx_local 移除 module。
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,並在下次開機期間為所有檔案系統重新標籤。大型磁碟需要很長時間,主控台看起來也可能像是卡住,因此請在能夠等待時執行。既然主機即將關機,也應先查看是否已有其他重新啟動工作排入佇列;dnf 更新讓舊版核心與程式庫仍留在記憶體中後,needs-restarting 會回報這些資訊。在 Rocky Linux 和 AlmaLinux 9 上,設定檔不再自行停用核心層的 SELinux;完整停用 SELinux 的文件化方式是使用核心引數(sudo grubby --update-kernel ALL --args selinux=0)。瞭解這個命令有助於接手他人的伺服器。它不是修正 403 的方法。
容器會多出一個標籤
在 Red Hat 系列主機上,容器程序會在 container_t 中執行,且只能讀取標示為 container_file_t 的檔案。從主機建立的 bind mount 在容器內會以 Permission denied 失敗,但主機上的 ls -l 看起來完全正常。:Z 尾碼會指示執行環境重新標示該掛載:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z 會只為這個容器標示目錄。:z 會將目錄標示為可供多個容器共用。若將 :Z 指向其他服務正在使用的目錄,系統會遞迴重新標示該目錄,導致那些服務失效,因此應為容器提供專用路徑。若主機上尚未安裝容器引擎,請注意這些發行版上的 docker 指令通常是由 podman 以該名稱回應;Rocky 和 AlmaLinux 的安裝步驟會先處理這項差異,讓你不必等到遇到問題時才處理。其餘設定方式與其他映像檔相同,詳見在 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 server 在 httpd_t domain 中執行,只能讀取標記為 Web content 的檔案,因此標記為 default_t 或 admin_home_t 的檔案會遭拒絕,nginx 也就沒有可提供的內容。使用 sudo ausearch -m AVC -ts recent 確認;該指令會顯示以 httpd_t 結尾的 scontext,以及帶有錯誤 type 的 tcontext。接著記錄正確的 label 並套用:sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?",然後執行 sudo restorecon -Rv /data/www。
使用 setenforce 0 讓服務運作是否安全?
setenforce 0 是診斷步驟,不是修正方法。使用它重現一次問題,讓 log 在單次處理中收集所有拒絕事件,再以 sudo ausearch -m AVC -ts recent 讀取這些事件,然後執行 sudo setenforce 1 並修正原因。伺服器維持 permissive 時會記錄所有拒絕事件,但不會阻擋任何操作,因此只會留下大量雜訊,並失去防護。若某項服務在處理期間需要暫時放寬限制,請執行 sudo semanage permissive -a httpd_t,讓機器其餘部分維持 enforcing。
如何在 SELinux enforcing 模式下,讓服務使用非標準連接埠?
將連接埠加入該服務允許繫結的 type。若 Web server 使用 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;它會套用與 executable 路徑相關聯的 profile,而不是依據檔案上的 label 強制執行規則。使用 sudo aa-status 檢查,並在 sudo journalctl -k 中尋找 apparmor="DENIED" 行。Ubuntu 只會限制一部分已封裝的服務,因此許多程式預設不受限制。Rocky Linux 和 AlmaLinux 預設會啟用 SELinux enforcing,Fedora 和 RHEL 也是如此。