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.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 中的权限位仍必须先允许访问。两层检查都必须通过。
完整上下文包含由冒号分隔的 4 个字段,例如 system_u:system_r:httpd_t:s0:SELinux 用户、角色、类型和级别。在服务器上,您几乎始终关注第三个字段,即类型。以下两个命令可以显示当前值:
ps -eZ | grep nginx
id -Znginx 工作进程显示的上下文以 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 模式运行 SELinux,并使用 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 日志中的 SELinux 拒绝记录
每次拒绝都会由 audit daemon 记录为一条 AVC(访问向量缓存)消息:
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=04 个字段包含完整信息。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.logaudit2why 会读取相同的记录,并指出它识别出的原因:某个 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 -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() 调用在到达回环接口之前就被拒绝了。一个开关即可控制整个行为:
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_t 或 admin_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 也是如此。