SELinux基础:nginx权限正确仍返回403怎么办
nginx 返回403但权限位正确?本文解释 SELinux 如何拒绝读取,教您用 ausearch 读取拒绝记录,再用 semanage 和 restorecon 修复标签,同时保持 Enforcing 模式。
文件权限正确时,为什么 nginx 仍返回 403
如果文件的权限位正确,但 nginx 仍返回 403,几乎总是因为 SELinux(Security-Enhanced Linux,安全增强型 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
sestatus
getenforceEnforcing 会阻止并记录操作。Permissive 允许所有操作,并记录本应被阻止的操作。Disabled 完全不加载策略。getenforce 会显示当前模式。sestatus 还会显示 /etc/selinux/config 中的模式;该模式会在重启后恢复。
Rocky Linux、AlmaLinux、Fedora 和 RHEL 默认以 Enforcing 模式运行 SELinux,并使用 targeted 策略。这一共享默认设置并非偶然,而是因为这四个发行版都源自同一 Red Hat 系谱;该系谱在 Rocky Linux 和 AlmaLinux 出现之前曾延续至 CentOS。Rocky Linux 和 AlmaLinux 共享同一 Red Hat 系谱。无论使用其中哪一个,本页内容都不会有任何差异,因为它们提供相同的策略和工具。因此,选择 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,并将每条拒绝记录的通俗英文摘要写入日志。应在新服务器上同时安装这两个软件包,因为真正需要它们时,通常已经有组件发生故障。
如何读取 audit 日志中的 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 是源上下文,即进程运行所在的域。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 会读取相同的记录,并指出它识别出的原因:某个布尔值已关闭、标签与策略不匹配,或根本没有相应规则。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、软件包更新或完整重新标记都会将其重置,因为策略仍然规定该路径应使用其他标记。对于配置了 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 端口时,也要使用相同的命令才能使其正常工作。Bind to port 2222 on 0.0.0.0 failed: Permission denied 在 journalctl -u sshd 中表示 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。--permanent 标志与布尔值上的 -P 一样,会带来相同的重启陷阱;决定规则适用于哪些接口的 zone 也值得阅读一次,参见Rocky 或 AlmaLinux VPS 的 firewalld 基础知识。
没有可调整的布尔值或标签
在普通服务器上,这种情况很少见,也是最容易造成损害的情况。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,并在下一次启动期间为每个文件系统重新标记。磁盘较大时,这一过程会持续很长时间,控制台看起来可能像是卡住了,因此应在能够等待时启动。既然机器本来就要停机,不妨先查看还有哪些任务已排队等待重启;needs-restarting 可报告 dnf 更新后仍驻留内存的旧内核和库。在 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 指向其他服务正在使用的目录时,它会递归地重新标记该目录,从而导致这些服务失效,因此应为容器分配独立路径。如果系统尚未安装容器引擎,请注意,在这些发行版中,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(仅对一个配置文件启用宽容模式),使用 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 模式下让服务使用非标准端口?
将端口添加到该服务获准绑定的类型中。对于使用 8081 的 Web 服务器:sudo semanage port -a -t http_port_t -p tcp 8081。对于使用 2222 的 SSH: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,它根据可执行文件的路径关联配置文件,而不是根据文件标签执行限制。使用 sudo aa-status 检查,并在 sudo journalctl -k 中查找 apparmor="DENIED" 行。Ubuntu 只限制一组指定的打包服务,因此许多程序默认以不受限制的方式运行。Rocky Linux 和 AlmaLinux 默认启用 SELinux enforcing,Fedora 和 RHEL 也是如此。