SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

SELinux导致nginx 403怎么排查和修复

文件权限正确但 nginx 返回 403?检查 SELinux 拒绝日志,使用 semanage 设置正确标签,再用 restorecon 恢复上下文,整个过程无需关闭 Enforcing 模式。

文件权限正确但 nginx 返回 403

如果文件的权限位正确,但 nginx 仍返回 403,几乎总是因为 SELinux(安全增强型 Linux)拒绝了读取请求。通过常规权限检查后,SELinux 还会检查另一组规则。Web 服务器只能读取带有 Web 内容标签的文件。您的文件带有其他标签,因此打开文件失败,nginx 没有可发送的内容。

查看标签,而不只是查看权限模式:

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

drwxr-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 中的权限位仍必须先允许访问。两层检查都必须通过。

完整上下文包含由冒号分隔的 4 个字段,例如 system_u:system_r:httpd_t:s0:SELinux 用户、角色、类型和级别。在服务器上,您几乎始终关注第三个字段,即类型。以下两个命令可以显示当前值:

ps -eZ | grep nginx
id -Z

nginx 工作进程显示的上下文以 httpd_t 结尾。您的登录 shell 显示 unconfined_u:unconfined_r:unconfined_t:s0,因为默认的 targeted 策略会限制服务,而不会限制交互式用户。了解这一点很重要,因为 SELinux 不会取代让服务以最小权限用户运行。当有人攻破服务后,它会限制该服务可以访问的内容。

SELinux 的 3 种模式,以及哪些镜像启用 SELinux

sestatus
getenforce

Enforcing 模式会阻止操作并记录日志。Permissive 模式允许所有操作,并记录原本会被阻止的操作。Disabled 模式完全不加载策略。getenforce 可显示当前模式。sestatus 还会显示 /etc/selinux/config 中的模式;重启后系统会恢复为该模式。

Rocky Linux、AlmaLinux、Fedora 和 RHEL 默认以 Enforcing 模式运行 SELinux,并使用 targeted 策略。Ubuntu 和 Debian 使用 AppArmor,其作用相同,但机制不同(最后一节会介绍)。因此,同一个应用可能在一台服务器上正常安装,却在另一台服务器上返回 403。

在需要之前安装工具

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

在精简镜像中,semanage: command not found 表示缺少 policycoreutils-python-utils:该软件包包含 semanageaudit2allowsetroubleshoot-server 会添加 sealert,并将每条拒绝记录的英文概要写入 journal。在新服务器上安装这两个软件包,因为真正需要它们时,通常已经有部分功能出现故障。

如何读取 audit 日志中的 SELinux 拒绝记录

每次拒绝都会由 audit daemon 记录为一条 AVC(访问向量缓存)消息:

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

4 个字段包含完整信息。comm 是被阻止的程序。scontext 是源上下文,即进程运行所在的域。tcontext 是目标上下文,即进程尝试访问的对象标签。tclass 是对象类型,此处为文件。合起来看:运行在 httpd_t 中的进程尝试读取标记为 default_t 的文件,而 permissive=0 表示该请求确实被阻止,而不只是被记录。

如果 ausearch 没有输出,audit daemon 可能没有运行。此时,拒绝记录会写入内核环形缓冲区:

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.log

audit2why 会读取相同的记录,并指出它识别出的原因:某个 boolean 已关闭、标签与策略不匹配,或根本没有对应规则。sealert 会遍历整个日志,并为每条拒绝记录输出一条建议命令。请将该建议作为参考。不同发行版的措辞可能不同,而且 sealert 有时会建议创建自定义策略模块,而正确做法可能只是修复一行标签。

还需要注意一点。策略包含 dontaudit 规则,这些规则会隐藏被认为无害的拒绝记录。因此,即使日志为空,程序仍可能运行异常。可以在一次测试期间暂时显示这些记录:

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、软件包更新或完整重新标记都会重置该标签,因为策略仍规定该路径应使用其他标签。semanage fcontext才是可持久保存的方式。使用 sudo semanage fcontext -l | grep '^/data'列出已记录的规则。

服务必须写入的内容需要使用其他类型。对上传目录或缓存使用 httpd_sys_rw_content_t,并将其限制在这些路径上:将只读站点放在可写类型下,会授予应用超出实际需要的访问权限。

为什么标签会出错?几乎总是因为文件的来源方式。mv会保留文件现有的标签,因此从 /root移出的站点会带着 admin_home_t标签到达,并保持该标签不变。普通的 cp会让新文件继承目标目录的默认标签,这通常正是所需的行为;而 cp -arsync -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() 调用在到达回环接口之前就被拒绝了。一个开关即可控制整个行为:

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 端口时,也要使用同一个命令才能使其正常工作。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。在 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 删除模块。

宽松模式用于诊断,不是修复手段

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

宽松模式允许访问并记录日志。它的真正价值在于完整性。在强制模式下,服务会在遇到第一个拒绝时停止,因此您修复这一项、重启服务,然后才会遇到第二项。宽松模式会让运行继续,并在一次运行中收集所有拒绝记录。之后切回强制模式,再统一修复这些问题。

setenforce不会修改/etc/selinux/config,因此重启后系统会恢复为强制模式。这是一道安全保障,也说明为什么仅执行setenforce 0的“修复”会在最不合适的时刻再次出现。如果您只需要在处理某个服务时放宽限制,应仅标记该域,而不是整台机器:sudo semanage permissive -a httpd_t会让其他部分继续处于强制模式,sudo semanage permissive -d httpd_t则撤销该设置。

禁用 SELinux 的代价高于修复标签

/etc/selinux/config 中设置 SELINUX=disabled,等于用永久削弱服务器的安全性,换取一次一行命令即可完成的标签修复。Web 应用遭到入侵时,这种差异就会体现出来。在 enforcing 模式下,攻击者的代码运行在 httpd_t 中,因此可能读取 Web 内容,但无论 Unix 用户权限允许什么,策略都会拒绝读取 /etc/shadow 或写入 systemd 单元。未加载策略时,同一段代码将获得服务账户拥有的全部权限。

禁用 SELinux 还会产生后续成本。未加载策略期间,新文件会以无标签状态创建,文件系统会逐渐偏离策略要求。之后重新启用 SELinux 时,必须完整重新标记文件系统,否则会有大量服务同时失败:

sudo fixfiles -F onboot
sudo reboot

该命令会写入 /.autorelabel,并在下次启动期间重新标记每个文件系统。对于大型磁盘,这一过程需要很长时间,控制台看起来可能像是卡住了,因此应在有足够等待时间时执行。在 Rocky Linux 和 AlmaLinux 9 中,配置文件不再自动关闭内核部分;完整禁用 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(仅对一个配置文件启用宽松模式),使用 aa-enforce 将其恢复。Ubuntu 会限制一组选定的打包服务,其他服务则不受限制。因此,应阅读 aa-status 以确认当前实际启用的内容,不要凭假设判断。

有一个习惯适用于这两类系统。当服务报告 Permission denied,但相关内容看起来正确时,应先读取安全日志,再修改权限。权限位通常不会连续两次成为问题。

FAQ

如果文件权限正确,nginx 为什么仍返回 403?

因为 SELinux 拒绝了读取操作,而不是权限位有问题。Web 服务器运行在 httpd_t 域中,只能读取标记为 Web 内容的文件。因此,标记为 default_tadmin_home_t 的文件会被拒绝,nginx 没有可提供的内容。使用 sudo ausearch -m AVC -ts recent 确认,该命令会显示以 httpd_t 结尾的 scontext,以及带有错误类型的 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。如果不执行此步骤,守护进程会在启动时因 bind() ... Permission denied 退出,即使没有其他进程占用该端口。

Ubuntu 是否有 SELinux?

没有。Ubuntu 和 Debian 提供 AppArmor。AppArmor 将配置文件绑定到可执行文件的路径,而不是绑定到文件标签。使用 sudo aa-status 检查,并在 sudo journalctl -k 中查找 apparmor="DENIED" 行。Ubuntu 只限制一组经过选择的已打包服务,因此许多程序默认不受限制。Rocky Linux 和 AlmaLinux 默认启用 SELinux enforcing 模式,Fedora 和 RHEL 也是如此。