1. 先看懂报错:403 与 (13: Permission denied) 各自代表什么
我最早被这个报错折磨的时候,还是刚接手公司一台 CentOS 7 服务器。当时搭了个静态博客,浏览器打开直接一片红字:403 Forbidden。我第一反应是去改 Nginx 配置,以为是 deny all 之类写错了,结果翻遍配置文件也没找到问题。直到打开 /var/log/nginx/error.log,才看到真正有价值的线索:
code复制[error] 12345#0: *678 open() "/usr/share/nginx/html/index.html" failed (13: Permission denied), client: 1.2.3.4, server: _, request: "GET / HTTP/1.1", host: "example.com"
这一行至少透露了三件事:Nginx 确实尝试打开 index.html 这个文件、服务器返回了 403、而操作系统在 open() 这个系统调用上给了个 13: Permission denied 的反馈。
要理解这个报错,得先把两个层面的东西分开:
- 403 Forbidden 是 HTTP 协议层的状态码,翻译过来就是“服务器理解请求,但拒绝处理”。它可以是 Nginx 主动拒绝(比如
allow/deny规则),也可以是 Nginx 尝试读取文件失败后向上层反馈的最终结果。 - (13: Permission denied) 是
errno错误码,13 对应的是EACCES。内核在open()、stat()、access()等系统调用时发现当前进程没有足够的权限,就会把这个错误码返回给 Nginx。
所以,看到日志里带 (13: Permission denied) 时,基本可以断定问题出在操作系统权限检查这一层,而不是 Nginx 的业务配置。换句话说,请求已经顺利到达了 Nginx 的静态文件处理阶段,Nginx 也确实找到了文件路径,但在真正打开文件那一刻被系统拦下来了。
这里顺带说一个排查思路:如果日志里只有 403 而没有 (13: Permission denied),那问题很可能不是文件权限,而是 Nginx 配置规则本身——比如 deny all、index 指令指向了不存在的文件、或者缺少默认索引文件。反过来说,只要出现了 13 这个错误码,就值得把注意力集中到 Linux 文件权限和 SELinux 这两条线上,这是整篇排查指南的核心。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. worker 进程身份与文件权限的“隔空对决”
很多新手在排查权限问题时,习惯性先 ls -l 看一眼文件属主,然后得出结论:文件是 root 的,我当前用户是 root,那 Nginx 肯定也能读。这个思路错得很典型,因为 Nginx 真正读文件的时候,用的是 worker 进程的身份,而不是你 SSH 登录时那个身份。
2.1 master 进程和 worker 进程的身份差异
Nginx 启动时会先创建一个 master 进程,这个进程以 root 身份运行,负责加载配置、绑定 80/443 端口、管理 worker 进程。绑定低端口需要 root 权限,所以 master 必须以 root 启动。但真正的请求处理、文件读取、日志写入,全部由 worker 进程完成。
worker 进程的身份由配置文件里的 user 指令决定。比如:
nginx复制user nginx;
worker_processes auto;
这个 nginx 用户是安装 Nginx 时自动创建的,很多发行版默认是 nginx,也有的是 www-data 或者 nobody。如果配置文件里没写 user 指令,那 worker 会继承编译时的默认值,通常是 nobody。
要确认当前 worker 进程到底是什么身份,直接看进程列表:
bash复制ps -eo user,group,pid,ppid,cmd | grep nginx
输出大概长这样:
code复制root 1234 1 0 10:00 ? 00:00:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx 1235 1234 0 10:00 ? 00:00:00 nginx: worker process
nginx 1236 1234 0 10:00 ? 00:00:00 nginx: worker process
看到没有,worker 进程都是 nginx 用户。所以 Nginx 访问文件时,等价于你用 nginx 这个系统用户去执行 ls /usr/share/nginx/html/index.html。如果这个用户没有读文件的权限,内核就会返回 EACCES,也就是 13: Permission denied。
2.2 为什么要用独立用户而不是 root
每次讲到这里都会有人问:那我直接把 user root; 写进配置,worker 不就拥有最高权限了吗?技术上确实可以,但这是运维事故的高发点。Nginx 一旦被攻击者利用漏洞拿到 worker 进程的控制权,攻击者就拥有了 root 权限,后果不用我多说。
正确的做法是遵守最小权限原则:worker 进程只保留处理请求所必需的权限,文件权限不匹配时,应该调整文件属主或权限,而不是反向提高进程权限。这个思路贯穿全文——我们是在“让 Nginx 能读到该读的文件”,而不是“让 Nginx 什么都能读”。
在权限排查时,记住一个等价关系:Nginx 读文件 = nginx 用户读文件。所有检查,都可以用 sudo -u nginx cat /path/to/file 来验证,这一步能直接复现 Nginx 遇到的问题:
bash复制sudo -u nginx cat /srv/www/example.com/html/index.html
如果这个命令报 Permission denied,那 Nginx 百分百也会报同样的错误,问题出在系统层面;如果这个命令能正常输出内容,那 Nginx 还报 403 就要换个方向查了——比如 SELinux、路径中某个特殊设备的挂载选项、或者符号链接指向位置的问题。
3. 文件权限逐层排雷:从根目录到目标文件,执行权限一个都不能少
确认了 worker 进程身份之后,下一步就是文件权限检查。这里最容易被忽略的是:路径上的每一层目录都必须有执行(x)权限,而不只是目标文件本身有读权限。
3.1 目录权限的一个常见误解
Linux 的目录权限有三个位,含义和普通文件完全不同:
r(读):允许列出目录内容w(写):允许在目录中创建、删除文件x(执行):允许进入目录、访问目录中已知文件名的文件
对于 Nginx 读取静态文件来说,路径上每一级目录都需要 x 权限。如果目录没有 x 权限,即使文件本身是 644,Nginx 也无法打开文件,因为它在“进入目录”这一步就卡住了,内核同样会返回 13: Permission denied。
举个例子,假设站点文件放在 /srv/www/example.com/html/index.html,那么从 / 开始,/srv、/srv/www、/srv/www/example.com、/srv/www/example.com/html 每一层目录,都要允许 nginx 用户进入。
你可能会想:/ 目录权限默认是 755,/srv 也是 755,肯定不会出问题。真正出问题的往往是一些“特殊”路径:
/home/user/www:home 目录默认权限是 700,外部用户进不去,Nginx 自然也无法访问/root/www:root 的 home 目录位于/root,权限通常也是 700 甚至 750- 挂载在
/mnt/data、/media/xxx下的数据盘,有时挂载点权限很保守 - 通过 FTP/SFTP 上传文件时,传输工具创建的目录可能默认是 700
- Windows 迁移过来的目录,权限继承可能很混乱
3.2 用 namei 一条命令看穿整条路径
手动逐层 ls -ld 太慢了,推荐直接用 namei 命令:
bash复制namei -l /srv/www/example.com/html/index.html
输出会列出从根目录到目标文件每一层的权限、属主、属组:
code复制f: /srv/www/example.com/html/index.html
dr-xr-xr-x root root /
drwxr-xr-x root root srv
drwxr-xr-x root root www
drwxr-xr-x root root example.com
drwxr-xr-x nginx nginx html
-rw-r--r-- nginx nginx index.html
一眼就能看出哪一层目录缺少 x 权限。如果看到某个目录权限是 drwx------,那大概率就是问题所在。
3.3 最小可行权限建议
我不建议一上来就 chmod -R 777。虽然 777 能解决绝大多数权限问题,但它意味着任何本地用户都能读写你的网站文件,一旦服务器上有其他高危漏洞,攻击者可以轻易篡改站点内容。
我通常建议这样设置:
| 对象 | 推荐权限 | 说明 |
|---|---|---|
| 站点根目录 | 755 | 属主 root 或 nginx 均可 |
| 静态资源文件 | 644 | 普通文件只需读权限 |
| 需要写入的目录(上传、缓存、日志) | 755 或 775 | 视属主而定,nginx 用户需要写权限时用 775 或 750 |
| 可执行脚本/二进制 | 755 | 既要读又要执行 |
| 配置类敏感文件 | 640 | 减少不必要的读取面 |
调整权限时,建议按需最小化,比如:
bash复制chown -R nginx:nginx /srv/www/example.com/html
find /srv/www/example.com/html -type d -exec chmod 755 {} \;
find /srv/www/example.com/html -type f -exec chmod 644 {} \;
如果你的站点目录里还有需要 PHP-FPM 写入的缓存目录,单独拿出来提升权限即可,不要整个目录一刀切 777。
3.4 符号链接带来的隐藏问题
还有一种容易被忽视的情况:站点的某个文件或目录是符号链接。比如:
bash复制ln -s /data/storage/files /srv/www/example.com/html/files
这个时候,Nginx 不仅需要目标文件路径上每一层目录的权限,还需要符号链接真实指向位置每一层目录的权限。也就是说,/data、/data/storage、/data/storage/files 每一层都必须允许 nginx 用户进入。很多时候主目录权限没问题,但符号链接指向的挂载盘权限不对,照样报 13: Permission denied。
检查符号链接指向的完整路径,同样可以用 namei -l,它会自动追踪链接的真实路径。
4. SELinux:当权限全对却依然 403 时,多半是它在拦
文件权限全部调好之后,网站很可能已经恢复正常。但如果你遇到的是 RHEL、CentOS、Fedora 这些默认开启 SELinux 的系统,另一个拦截层就会出现:SELinux 的强制访问控制(MAC)机制。
我第一次被 SELinux 折腾到怀疑人生,是部署一个运行在 /home/alice/website 下的应用。权限按上面方法全部调了一遍,namei 也没问题,sudo -u nginx cat 也能读出内容,但浏览器访问还是 403。折腾到最后,才发现是 SELinux 把 Nginx 访问文件的行为给拦了。
4.1 先确认 SELinux 的状态
排查 SELinux 之前,先确认它到底有没有开启:
bash复制getenforce
输出有三种可能:
Enforcing:SELinux 正在强制拦截,需要继续排查Permissive:SELinux 只记录不拦截,如果在这种状态下网站正常、Enforcing 下异常,基本可以锁定是 SELinuxDisabled:SELinux 已关闭,直接跳过本章
如果服务器上明确开启了 SELinux,就要检查 Nginx 相关文件的 SELinux 上下文标签。查看文件标签:
bash复制ls -Z /srv/www/example.com/html/index.html
输出会带一段类似这样的标签:
code复制system_u:object_r:httpd_sys_content_t:s0
其中 httpd_sys_content_t 是我们关注的类型。SELinux 的工作原理是:worker 进程运行在 httpd_t 这个域中,这个域被允许访问的类型是预定义好的。如果目标文件的类型不在允许列表里,即使 DAC 权限全部放行,SELinux 依然会拒绝。
4.2 常见场景:网站文件被放在了“错误”的位置
SELinux 预置了很多默认策略,比如 /var/www/html 下的文件自动被标记为 httpd_sys_content_t,Nginx 可以正常读取。但你把站点放在 /home/xxx、/mnt/data、/opt/website 这些非标准位置时,文件的默认标签往往是 home_root_t、usr_t、default_t 等,Nginx 的 httpd_t 域没有权限访问这些类型,于是 403 就出现了,而且看起来毫无理由。
检查一下文件标签是否符合预期:
bash复制ls -Z /path/to/site
如果看到类型不是 httpd_sys_content_t 开头,那基本就是 SELinux 在拦。
4.3 修复 SELinux 标签的两种方式
方法一:临时修改标签(最常用)
bash复制chcon -R -t httpd_sys_content_t /srv/www/example.com/html
这种方法立即生效,但有个坑:如果之后执行了 restorecon,或者文件被重新复制覆盖,标签会被还原成原始值,问题会再次出现。
方法二:写入 SELinux 策略(推荐,持久生效)
用 semanage 把路径规则固化下来:
bash复制semanage fcontext -a -t httpd_sys_content_t "/srv/www/example.com(/.*)?"
restorecon -Rv /srv/www/example.com
第一行给路径添加一条持久规则,第二行让系统根据规则重新打标签。这样以后即使执行 restorecon,标签也不会丢失。
如果 Nginx 还需要向某个目录写入文件(比如上传目录、FastCGI 缓存目录),那这个目录需要允许读写:
bash复制semanage fcontext -a -t httpd_sys_rw_content_t "/srv/www/example.com/html/uploads(/.*)?"
restorecon -Rv /srv/www/example.com/html/uploads
4.4 怎么快速确认是不是 SELinux 干的
看 AVC 日志:
bash复制ausearch -m avc -ts recent
如果输出里有 denied 相关的 AVC 记录,并且路径指向你的站点文件,那基本可以断定是 SELinux 拦截。也可以直接查看 /var/log/audit/audit.log 末尾的拒绝记录:
bash复制grep "nginx" /var/log/audit/audit.log | tail -20
还有一种临时验证方式:把 SELinux 切到 Permissive 模式,看网站是否恢复正常:
bash复制setenforce 0
恢复正常后,setenforce 1 切回去,然后用上面的方法做持久修复。需要注意,这只是在测试环境用的手段,生产服务器不要长期保持 Permissive,毕竟 SELinux 是重要的安全防线。
4.5 布尔值开关:Nginx 反向代理时可能踩到的另一类 SELinux 问题
如果你是拿 Nginx 做反向代理,访问后端端口(比如本机的 8080 服务)时也遇到 permission denied,记得检查一个布尔值:
bash复制getsebool -a | grep httpd
重点看:
code复制httpd_can_network_connect --> off
如果它是 off,Nginx 默认不允许作为代理去连接网络端口。开启它:
bash复制setsebool -P httpd_can_network_connect on
-P 参数表示持久化,重启不丢失。同理,如果 Nginx 需要连接数据库,可能需要开启 httpd_can_network_connect_db 之类的布尔值。
5. 被误认成权限问题的其他 403 场景
排查时间长了你会发现,凡是到了 403 这一步,很多人第一反应都是“权限问题”,于是机器似的 chmod -R 777 一顿操作,问题却纹丝不动。我总结了几类特别容易跟真正的权限问题混淆的场景,遇到 403 时先对照排除。
5.1 目录索引规则导致的无 index 文件
如果请求访问的是一个目录,而目录下没有 index.html、index.php 之类的索引文件,同时目录的 autoindex 又是关闭状态,Nginx 会返回 403。这种情况的错误日志里通常不会出现 (13: Permission denied),而是类似:
code复制[error] directory index of "/srv/www/example.com/html/" is forbidden
这和权限完全无关,属于 Nginx 配置层面的拒绝。解决办法是添加上 index 指令指向真实存在的文件,或者开启 autoindex on; 允许目录列表展示。
5.2 allow/deny 规则限制
Nginx 配置里如果写了:
nginx复制location /admin {
deny all;
}
或者只允许特定 IP 访问:
nginx复制location /internal {
allow 192.168.1.0/24;
deny all;
}
不符合规则来源的请求会直接返回 403,错误日志里会显示 access forbidden by rule,而不是文件权限错误。
5.3 PHP-FPM 的 open_basedir 限制
当 Nginx 通过 FastCGI 将 PHP 请求转发给 PHP-FPM 时,PHP 自身还有一个安全配置叫 open_basedir,它限制了 PHP 脚本能访问的文件路径范围。如果脚本试图访问 open_basedir 之外的路径,PHP 会拒绝执行,最终返回 403。这种情况错误日志出现在 PHP-FPM 的日志里,会出现类似:
code复制PHP Warning: file_get_contents(): open_basedir restriction in effect. File(/tmp/xxx) is not within the allowed path(s): /srv/www/example.com:/tmp/
这跟 Linux 文件权限无关,是应用层 PHP 的路径白名单限制。
5.4 Nginx 的 disable_symlinks 指令
有些高安全的环境会在 Nginx 配置中开启:
nginx复制disable_symlinks on;
开启之后,如果请求的文件路径中包含符号链接,Nginx 会直接拒绝访问。这种配置出现在共享目录、防目录穿越的安全场景里,容易造成“文件明明存在、权限也正确,但一直 403”的情况。
5.5 文件系统挂载选项
如果网站文件存放在单独挂载的数据盘上,要检查挂载选项。比如 /etc/fstab 里给数据盘加了 noexec 选项时,不影响静态文件读取,但如果目录里包含需要执行的脚本,就会受影响。还有极少数情况:挂载时加了 nosuid、nodev 等选项,对 Nginx 读取静态文件影响不大,但如果你在目录里跑 CGI 程序,就要留意。
检查当前挂载选项:
bash复制mount | grep /data
如果看到 ro(只读),那问题就直接多了,Nginx 试图写缓存或日志的时候会失败。
5.6 Web 应用自身的 403 逻辑
有时 403 根本不是你 Nginx 返回的,而是后端应用自己抛出的。比如 Laravel、Django、WordPress 等框架如果检测到非法的请求来源、缺少 CSRF Token、或者 IP 被应用层封禁,都会返回 403。判断方法很简单:看响应头里的 Server 字段,或者看请求是否经过 PHP-FPM 之后才返回 403。
6. 一套能直接照着跑的排查命令清单
说了这么多理论,最后落成一套可以直接在服务器上执行的排查顺序。每次遇到 Nginx 403 我基本按这个流程走,从定位到解决一般不超过十分钟。
6.1 手动排查五步法
第一步,定位真实报错:
bash复制tail -n 100 /var/log/nginx/error.log
重点看有没有 (13: Permission denied)。如果没有,回到第五章排查配置类问题。
第二步,确认 Nginx 进程身份:
bash复制ps -eo user,group,pid,cmd | grep nginx
记住 worker 进程的用户,记为 $NGINX_USER。
第三步,检查文件路径每一层权限:
bash复制namei -l /srv/www/example.com/html/index.html
找出缺 x 权限的目录,按需修正:
bash复制chmod o+x /home /home/alice # 谨慎操作,home 目录开放对外执行权限要评估风险
# 或者把站点目录移到 Nginx 默认能访问的位置,比如 /srv/www 或 /var/www
第四步,用 Nginx 用户身份实际测试读取:
bash复制sudo -u nginx cat /srv/www/example.com/html/index.html
能读出来,说明 DAC 权限 OK;读不出来,说明权限还需要调整。
第五步,检查 SELinux:
bash复制getenforce
ls -Z /srv/www/example.com/html/index.html
ausearch -m avc -ts recent
标签不对就按第四章的方法修复。
6.2 一个免安装的排查脚本
不想一步步敲,可以把下面这段存成 nginx_403_check.sh,直接跑:
bash复制#!/bin/bash
# Nginx 403 Permission Denied 快速排查脚本
# 用法: bash nginx_403_check.sh /srv/www/example.com/html/index.html
FILE_PATH="$1"
if [ -z "$FILE_PATH" ]; then
echo "用法: $0 <文件完整路径>"
exit 1
fi
echo "========== 1. 定位 Nginx 进程身份 =========="
ps -eo user,group,pid,cmd | grep nginx || echo "未找到 nginx 进程"
echo ""
echo "========== 2. 检查路径每一层权限 =========="
namei -l "$FILE_PATH"
echo ""
echo "========== 3. 检查 SELinux 状态 =========="
getenforce
echo ""
echo "========== 4. 检查文件 SELinux 标签 =========="
ls -Z "$FILE_PATH"
echo ""
echo "========== 5. 最近 30 分钟 AVC 拒绝记录 =========="
ausearch -m avc -ts recent 2>/dev/null | tail -30 || echo "无 AVC 日志或 ausearch 不可用"
echo ""
echo "========== 6. 模拟 nginx 用户读取测试 =========="
sudo -u nginx cat "$FILE_PATH" > /dev/null 2>/tmp/nginx_user_read.err
if [ $? -eq 0 ]; then
echo "nginx 用户可以正常读取该文件"
else
echo "nginx 用户读取失败,错误信息:"
cat /tmp/nginx_user_read.err
fi
这个脚本适合在初次排查时快速生成一份“体检报告”,看完输出基本能锁定问题在哪一层。
6.3 常见命令速查表
| 命令 | 作用 | 典型用法 |
|---|---|---|
namei -l <路径> |
查看路径每一层权限 | namei -l /var/www/html/index.html |
ps -eo user,pid,cmd | grep nginx |
查看 Nginx 进程身份 | 确认 worker 用户名 |
ls -Z <路径> |
查看文件 SELinux 标签 | ls -Z /var/www/html/index.html |
getenforce |
查看 SELinux 模式 | 确认是否为 Enforcing |
ausearch -m avc -ts recent |
查看最近 SELinux 拒绝记录 | 定位被拦截的具体文件 |
sudo -u nginx cat <路径> |
模拟 Nginx 读取 | 复现权限问题 |
semanage fcontext -a -t httpd_sys_content_t "<路径>(/.*)?" |
给路径写持久 SELinux 规则 | 解决非标准目录的 403 |
restorecon -Rv <路径> |
按策略恢复 SELinux 标签 | 应用 fcontext 规则 |
7. 一次真实的 403 排查案例复盘
最后分享一个我印象比较深的实战案例,把上面所有内容串成一条完整的排查链。
当时有个朋友托管了一个 WordPress 站点,他把站点压缩包上传到了自己的家目录 /home/alice/site,解压之后访问,报 403。他自己试了 chmod -R 777 /home/alice/site,没有改善,于是来找我。
我先看了 Nginx 错误日志,明确看到 (13: Permission denied),确认是系统层问题。
接着 ps 确认 worker 是 nginx 用户。然后 namei -l /home/alice/site/index.php,输出显示:
code复制f: /home/alice/site/index.php
dr-xr-xr-x root root /
drwxr-xr-x root root home
drwx------ alice alice alice <-- 问题所在
drwxrwxrwx alice alice site
-rw-r--r-- alice alice index.php
/home/alice 的权限是 700,只有 alice 自己能进,nginx 用户被挡在了门外。虽然 site 目录已经是 777,但进不去家目录,一切都是白搭。
修复方案有两个方向:一是给 /home/alice 加 o+x 权限,让它允许其他用户穿过;二是干脆把站点移到标准目录,比如 /srv/www/alice-site,然后把属主改成 nginx。我选了第二种,因为第一种等于给 alice 的家目录开了个口子,不太安全。
迁完目录、改好权限之后,我顺手看了眼 getenforce,发现这台机器是 Enforcing,于是又 ls -Z 确认了标签。新目录在 /srv/www 下,但之前 chcon 的标签不一定被继承,我直接用:
bash复制semanage fcontext -a -t httpd_sys_content_t "/srv/www/alice-site(/.*)?"
restorecon -Rv /srv/www/alice-site
把 SELinux 的规则也固化了下来。最后访问,站点恢复。这案例里有几个关键点:日志先定位、路径逐层排查、SELinux 补一刀,缺一个都可能绕圈子。
在这之后我又踩过几次类似的坑,印象最深的是 /mnt/data 这种数据盘场景,DAC 权限完全正确、namei 也 OK,但文件标签是 default_t,SELinux 就是不让你读。这种问题用肉眼很难察觉,不看 AVC 日志的话,你可能连方向都找不对。
所以我的最终建议是:遇到 Nginx 403 不要急着 chmod 777,先把 error.log 里那行报错看清楚,有 (13: Permission denied) 就往权限链路上查,没有就往配置和业务层查。文件权限、进程身份、SELinux 这三板斧轮下来,绝大多数问题都能在几分钟内定位到根因。
