ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Nginx权限问题排查指南:告别403 Forbidden与Permission denied

Nginx权限问题排查指南:告别403 Forbidden与Permission denied

说实话,只要跑过一段时间Nginx,没人能躲过这个报错组合:浏览器里一个大白板写着403 Forbidden,翻到error.log看到一行open() "/xxx/index.html" failed (13: Permission denied)。尤其是当你确认文件存在、路径没错、配置也是从默认模板改的,结果页面死活打不开的时候,那种憋屈感我太懂了。

Nginx权限问题在排障里出镜率极高,但它本身并不复杂。这篇文章我会把底层逻辑讲明白,再按场景给可复现的修复步骤,最后附上我平时排查的固定流程和踩坑清单,争取让你下次碰到类似问题能在五分钟内定位。

1. 先搞清楚Nginx权限体系的底层逻辑

1.1 工作进程是谁在跑

很多人把Nginx的权限问题想得太玄,其实核心就一句话:运行Nginx进程的用户,必须对网页文件有合适的访问权限。

Nginx启动时有个master主进程,负责加载配置、管理worker进程。主进程通常以root启动(因为要监听80/443这种特权端口),但它干完活就会把worker进程降权。真正处理请求、读文件、写日志的,是那些worker进程。

worker进程跑在哪个用户下,看配置文件里的user指令。常见发行版默认值不一样:Debian/Ubuntu系常用www-data,CentOS/RHEL系常用nginx,一些编译安装的版本可能直接用nobody。

所以排查权限问题的第一步,永远是确认worker进程的实际身份。光看配置文件的user指令是不够的,因为某些发行版会在编译时覆盖默认值,或者配置里有include片段二次赋值。最可靠的办法是直接看进程:

ps -eo pid,user,comm | grep nginx

输出里以nginx: worker结尾的进程属于哪个用户,处理请求时就用哪个身份,这是铁律。顺便提一句,虽然user指令可以写成user root;来“一劳永逸”,但这是严重的生产事故隐患,后文会专门解释为什么不能这么干。

1.2 目录和文件权限到底卡在哪一层

Linux的权限模型很简单:文件或目录有owner、group、others三组身份,每组有r(读)、w(写)、x(执行)三种权限。但对Nginx这类web服务来说,判断标准要从“当前登录用户”换成“worker进程的用户身份”。

这里有个新手最容易栽跟头的地方:目录的权限含义和文件不一样。

  • 文件:读需要r,写需要w,执行/运行需要x。
  • 目录:r只代表“能列出目录里有哪些名字”,w代表“能在目录里创建/删除文件”,x才代表“能穿过这个目录”。

对Nginx而言,访问/data/www/site/index.html需要/data、/data/www、/data/www/site每一级目录都包含“其他用户可执行”(others的x)的权限。哪怕只有一级目录缺了x,Nginx就会上报permission denied。但注意,目录缺r并不会直接阻止访问已知文件名的文件,这就是为什么有些“目录没读权限”的站点,直接输入完整路径能打开,但浏览目录列表会403。

考虑到权限配置,标准安全的做法是:目录给755(rwxr-xr-x),文件给644(rw-r--r--),owner设为部署用户或站点管理员。这样owner全权控制,其他所有人(包括Nginx worker)能读能执行,但不能写。

1.3 SELinux这个隐藏拦路虎

在CentOS/RHEL及衍生发行版上,大量Nginx权限问题还有一个隐形元凶:SELinux。它的存在会让“普通的Linux权限”看起来完全正常,但访问依然被拒。

SELinux给每个进程和文件都打了安全标签。默认策略里,Nginx进程的域是httpd_t,而网页文件通常需要标记为httpd_sys_content_t。如果你的站点文件放在自定义路径(比如/data/www)而不是发行版默认的/usr/share/nginx/html,SELinux默认不带httpd_sys_content_t标签,Nginx就会在系统调用层被拦截。

最典型的现象是:ll看文件权限一切正常,nginx用户也有权限,但日志照样报Permission denied。这时候必须检查SELinux:

getenforce

输出Enforcing就说明SELinux在生效。再用ls -Z看文件标签:

ls -Zd /data/www

如果标签是default_t而不是httpd_sys_content_t,就可以实锤了。修SELinux的正确姿势不是直接setenforce 0关掉(虽然很多人这么干),而是按后文方案给文件打标签。后面我在解决方案里会展开讲。

2. 认识两类最典型的权限报错

2.1 403 Forbidden的两种截然不同的成因

我见过太多人一看403就急着调目录权限,结果越调越乱。其实403 Forbidden在Nginx里有两种完全不同的来源,必须靠error.log区分。

第一种是系统调用层错误。Nginx尝试用worker进程身份去open()文件,内核返回EACCES(errno 13)。这种错误在error.log里的特征是:包含Permission denied字样,并且会带上失败的操作路径。比如:

[error] 12345#0: *678 open() "/usr/share/nginx/html/index.html" failed (13: Permission denied)

这种才是真正的“权限”问题,需要按第3章的方案去修目录、owner、SELinux标签。

第二种是Nginx配置层的拒绝。文件本身读得到,但Nginx的配置逻辑决定不给你看。典型场景:

[error] 12345#0: *789 directory index of "/data/www/site/" is forbidden

这种情况通常是目录里没有index.html/index.php等索引文件,而配置里又没开autoindex on;。简单说,Nginx进得了目录、读得到文件列表,但不知道默认该显示哪个文件。这不太算权限问题,更像配置问题。还有一种配置层拒绝是location块里显式写了deny all;或做了IP封禁。

区分这两种403的方法很简单:去error.log看关键字。有“permission denied”的走系统权限排查;只有“directory index is forbidden”的检查索引文件和index指令。

2.2 error.log里第13号错误代表什么

系统调用错误后面跟的数字是从errno全局变量来的。13: Permission denied对应EACCES,意思是“权限不足,操作不被允许”。并不是只有文件权限会导致EACCES,像Unix socket连接、写日志、创建临时文件都可能触发。

常见的几个errno在Nginx日志里出现的频率也高,我整理了一下:

errno含义Nginx日志里的典型表现
13EACCES权限不足open() failed (13: Permission denied)
2ENOENT文件/目录不存在open() failed (2: No such file or directory)
21EISDIR路径是目录open() "/path" failed (21: Is a directory)
30EROFS文件系统只读open() failed (30: Read-only file system)
111ECONNREFUSED连接被拒connect() failed (111: Connection refused)

排查时不要只盯着13不放,有时候2号和13号是连锁反应:比如Nginx想访问某目录下的默认索引文件,但目录本身对worker进程没有x权限,内核可能返回的不是13而是2(当作不存在)。所以“文件明明存在但报No such file or directory”时,也要回头看权限。

3. 分场景破解权限问题

3.1 静态文件与站点目录的权限修复

最典型也最好修的场景:静态站或前端项目部署到服务器上,结果访问直接403。我习惯按这个顺序处理:

第一步,确认worker进程用户。

ps -eo user,comm | grep nginx

假设返回nginx,那所有判断都以nginx用户能否读文件为基准。

第二步,检查目录链路权限。

namei -l /data/www/site/index.html

这条命令会列出从根目录到目标文件的每一层权限,一眼就能看出哪级目录缺x或r。比如说:

f: /data/www/site/index.html drwxr-xr-x root root / drwxr-xr-x root root data drwxr-xr-x root root www drwx------ root root site -rw-r--r-- root root index.html

问题立刻就清楚了:site目录权限是700,只有root能进,nginx用户被拒之门外。修复方式是把目录改成755:

chmod 755 /data/www/site

如果不想用迭代的chmod命令,可以一条命令递推设置目录和文件权限:

find /data/www/site -type d -exec chmod 755 {} \; find /data/www/site -type f -exec chmod 644 {} \;

第三步,把owner调整为更适合协作的模式。如果部署用户是deploy,但它要把文件放在/data/www下给nginx读,有几种选择:

  • 直接把文件owner改成nginx:chown -R nginx:nginx /data/www/site
  • 更推荐的做法:owner保持deploy,group设成nginx,目录权限755,文件权限640以上,再把nginx用户加入deploy组或文件所在组。这样开发部署方便,Nginx也能读,权责边界清晰。

实操里我一般不用nginx作owner,因为后续程序要更新文件时,会很别扭。把项目交给专用的部署账户,然后把站点目录的group设成nginx更顺手。

第四步,检查SELinux标签。如果你在CentOS系上,文件权限改完还是403,立刻检查标签:

chcon -R -t httpd_sys_content_t /data/www/site

但chcon是临时生效,重启或执行restorecon后会恢复。正规做法是用semanage fcontext写持久化规则:

semanage fcontext -a -t httpd_sys_content_t "/data/www/site(/.*)?" restorecon -Rv /data/www/site

如果后续还会部署新文件到该目录,这个持久化规则会自动给新文件打正确的标签,一劳永逸。

3.2 PHP-FPM的Unix Socket权限冲突

PHP类站点(比如各种PHP框架、WordPress)跑Nginx时,静态文件可能一切正常,但一旦访问PHP文件,报错就变成了:

[error] connect() to unix:/run/php-fpm/php-fpm.sock failed (13: Permission denied)

这种和网页文件的Permission denied完全不是一回事。Nginx在这里是要把自己当成客户端,去连接一个Unix domain socket,而连接socket的权限由socket文件的权限和SELinux布尔值决定。

先看socket文件本身:

ls -l /run/php-fpm/php-fpm.sock

常见输出是:

srw-rw---- 1 root php-fpm 0 某日某时 php-fpm.sock

socket的owner是root,owner是php-fpm组。而Nginx worker进程是nginx用户,它不属于php-fpm组。srw-rw----表示除了root和php-fpm组成员,其他人都无法读也无法写,nginx自然连不上。

解决办法有四种,按推荐度排序:

方案A:改PHP-FPM的socket owner/group为nginx。在PHP-FPM的pool.d/www.conf里找:

listen.owner = nginx listen.group = nginx listen.mode = 0660

改完重启PHP-FPM,socket的ower/group就变成nginx了,Nginx就能连。这种最省心。

方案B:把nginx用户加进php-fpm组。

usermod -a -G php-fpm nginx

需要让Nginx重新加载用户组信息,通常得重启服务。效果也是让nginx用户获得socket访问权。

方案C:把socket权限放宽到0666。修改listen.mode = 0666。能用但不建议,因为这是把所有本地用户都敞开写入socket的权限,等于把后门打开给同机器的其他进程。

CenterOS系上还有第五个隐藏坑:即使socket权限看起来全对,可能还是连不上,因为SELinux会拦。这时要检查并打开布尔开关:

setsebool -P httpd_can_network_connect 1

httpd_can_network_connect控制Nginx是否能发起网络连接(包括访问socket)。在CentOS 7/8上,这个值默认往往是off。顺带一提,如果你用Nginx反代到本机某端口,遇到connection refused或13错误,很多时候也是这个布尔值的锅。

我自己踩过最深的坑是:改了socket权限之后,忘了检查SELinux,结果还报13错。后来把两个维度并成一套检查流程:ls -l看Unix权限,getsebool httpd_can_network_connect看SELinux布尔量,一气呵成。

3.3 反向代理与缓存目录的权限边界

Nginx做反向代理时,目录权限问题主要在缓存目录和临时目录。如果配置了proxy_cache,而缓存目录没有正确权限,会出现:

[error] mkdir() "/var/cache/nginx/proxy_temp" failed (13: Permission denied)

这里要理解Nginx对临时目录和缓存目录的权限要求:worker进程必须对它们有完全的读写执行权限。因为Nginx在代理大文件、响应体时,会先把内容写入临时文件(快满或超阈值时),再移动到目标位置。

默认临时目录配置在http块里:

proxy_temp_path /var/cache/nginx/proxy_temp; proxy_cache_path /var/cache/nginx/proxy_cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=on;

把这两类目录的owner统一给nginx是正确的做法:

mkdir -p /var/cache/nginx/proxy_temp /var/cache/nginx/proxy_cache chown -R nginx:nginx /var/cache/nginx chmod -R 700 /var/cache/nginx

权限给700就够,反正是Nginx自己的私有目录,越少对外开放越好。

如果你用Nginx反代到上游,比如负载均衡到多个后端节点,还要留意上游返回内容很大时Nginx写临时文件会迅速占满磁盘。这虽然不算权限问题,但经常和权限问题一起出现,排查时我建议顺手看下df -h和目录所属空间是否够用。

3.4 日志文件写不进去怎么办

Nginx自身有两个日志相关的大坑:一是access.log/error.log本身写不了导致启动失败,二是站点的业务日志目录权限不对导致应用报错。

Nginx日志写不了的场景:error.log里大概率会直接出现:

[emerg] open() "/var/log/nginx/error.log" failed (13: Permission denied)

这种往往是/var/log/nginx目录的owner或者父目录权限被动过,worker进程(和master进程在启动阶段)没权限创建或追加日志文件。解决办法:

mkdir -p /var/log/nginx chown -R nginx:nginx /var/log/nginx

用nginx -t验证配置没问题后,再启动服务。要是还不行,注意看一下/var/log自身是不是权限异常,某些加固脚本会把所有日志目录都改成700 root:root,那Nginx是真的写不进去。

业务日志目录的坑没法用Nginx配置解决。比如PHP应用要写/data/www/site/runtime日志目录,而Nginx worker用户的上下文是nginx,应用进程(PHP-FPM)也是nginx用户,如果该目录owner是别的用户且不可写,应用就会报错。这类问题排查时要区分“给Nginx的权限”和“给应用运行用户的权限”,别全部套用Nginx逻辑。

我经历过一次所有改动都正确但仍写不进去的情况,最后发现是/data/www/site/runtime被chattr +i加了不可变标志。这个冷门问题值得留意,排查时可执行:

lsattr /data/www/site/runtime

如果看到----i--------,移除标志后用chattr -i就行。

4. 一套可落地的排查流程

4.1 从进程用户到目录链路的五步检查

烂熟于心的排查顺序能帮你节省大量时间。我自己总结的五步法,基本覆盖了90%的Nginx权限问题:

第一步,最小化确认worker身份。执行:

ps -eo user,comm | grep nginx

第二步,确认报错路径与文件。从error.log里拿到具体路径,不要凭感觉猜目录。日志里的路径可能有前缀(比如/usr/share/nginx/html)和实际站点目录不是一回事,务必以日志为准。

第三步,逐级查看目录链路权限。用namei -l或手动ls -ld每个目录,确认每一级都有“其他人可执行”权限。

第四步,确认文件权限。文件至少要644,至少不能对worker用户封锁读权限。

第五步,查SELinux状态和标签。特别在CentOS/RHEL系上,这一步不能省。

这五步执行下来,基本能定位“是普通Linux权限还是SELinux标签”的问题。如果五步全对还是报Permission denied,我才会考虑是不是有chattr加固、只读挂载或其他文件系统层因素。

4.2 模拟用户访问定位权限盲区

有一个特别高效的验证思路:切换到worker用户身份,真实模拟一次Nginx访问。

先确认worker用户,比如是nginx,然后执行:

sudo -u nginx cat /data/www/site/index.html

如果cat读得出来,说明普通Linux权限没问题。如果提示permission denied,那就可以直接定位到具体是哪个目录的哪个环节缺权限:

sudo -u nginx ls /data/www/site/

对比两条命令的结果,就能判断到底卡在目录穿越(x权限)还是文件读取(r权限)上。

PHP-FPM socket场景也能模拟:

sudo -u nginx curl --unix-socket /run/php-fpm/php-fpm.sock http://localhost/

如果curl访问socket返回权限错误,说明socket的owner/group或SELinux布尔量有问题;如果curl返回HTTP响应,那Nginx配置或PHP进程本身可能是主要嫌疑。

这个模拟法的好处是能区分“文件系统的状态”和“Nginx的配置行为”。很多时候文件权限没问题、SELinux也正常,但Nginx依然403,原因就跑到Nginx配置层去了。通过模拟命令,可以直接把配置层问题从权限问题中摘出来。

5. 常见问题速查与避坑笔记

5.1 高频问题对照表

我把这几年在群里、社区里见过的高频Nginx权限问题浓缩成一张速查表,实战时可对照着用:

报错特征可能原因首选修复
open() failed (13: Permission denied),文件在站点目录目录链路缺x或文件缺r;SELinux标签不对namei -l定位缺权限层;chmod 755/644;SELinux下用restorecon -Rv
directory index of "/xxx" is forbidden目录下无index文件且未开autoindex放index文件或加autoindex on;(慎用)
connect() to unix:/xxx.sock failed (13: Permission denied)socket权限不对调listen.owner/group或在PHP-FPM配置中指定nginx用户
mkdir() "xxx" failed (13: Permission denied)缓存目录/临时目录owner不对chown -R nginx:nginx缓存目录
access.log/error.log open() failed (13)日志目录owner不对或日志目录被锁chown -R nignx:nginx /var/log/nginx;查lsattr
反代到本机端口报13或111SELinux布尔量关闭setsebool -P httpd_can_network_connect 1
文件存在但报2号No such file某级目录权限缺x,内核“看不见”补目录的x权限

光看表不一定覆盖所有场景,但排障的时候先对照一遍,大概率能省去你从零摸索的时间。

5.2 几个容易忽略的细节

这一节分享几个我在实际操作中容易踩到的细节,常规文档基本不提但影响实打实。

细节一:Nginx对目录的权限要求比想象中严格。目录权限至少要711才能让Nginx正常穿越,750也能穿越(owner可读写执行、group可读执行、others没有权限)。这意味着如果你把站点目录放在某用户home目录下(比如/home/deploy/www),而home目录权限是700,Nginx就永远进不去。很多人把站点放在home下,文件权限全对,就是403,原因就在home目录没有“其他人可执行”。这种场景要么把目录挪到/var/www或/data/www,要么调整home目录权限(一般不建议)。

细节二:不要轻易用user root;。我看到过几次有人为了省事直接把Nginx worker设成root,这样的后果是任何漏洞都直接以root权限执行。一旦PHP或其他模块被攻击,影响是灾难性的。正规做法永远是给worker一个低权限账号,并把站点文件通过合理的方式(group、ACL、sudo部署)交给这个账号访问。

细节三:改完权限记得测试重载。修改配置文件或权限后,先跑:

nginx -t

确认语法没问题再用:

nginx -s reload

平滑重载。很多时候权限问题不是没修好,而是改了配置没生效,或者语法报错导致根本没加载。

细节四:ACL权限也可能参与。当ls普通权限看着没问题但Nginx还是13错时,检查ACL:

getfacl /data/www/site

如果ACL里有mask或者某条对特定用户拒绝的记录,会直接影响Nginx的访问判断。用setfacl -R -m u:nginx:rX /data/www/site可以追加ACL授权,这个方案特别适合那些不方便改owner的共享目录场景。

细节五:日志轮转也会制造权限问题。logrotate轮转日志时,如果新创建的日志文件owner是root,而Nginx接下来要写入就会遇到13错。排查时如果看到日志报错时间点集中在轮转前后,大概率就是轮转脚本创建的文件权限不对。解决方式是在logrotate配置里加create 640 nginx adm这类参数,确保轮转后文件owner正确。

结尾:说点个人体会

处理Nginx权限问题这几年,我最深的体会是:这问题根本不算难,难的是养成系统性排查习惯。很多人一看到403就直接把手头文件chmod 777,改完了还不行就开始怀疑配置、怀疑防火墙、怀疑网络,绕一大圈回来才知道是目录上一级少了个x权限。与其这样,不如按前面那五步来:确认worker身份、看日志拿路径、查目录链路、查文件权限、查SELinux。每一步都用一个命令验证,基本上两分钟就能定位。

另外一个让我吃过亏的细节是,修改完权限后务必要重新加载Nginx和PHP-FPM。Nginx的root权限配置改了要重载,PHP-FPM的listen.owner改了要重启,SELinux布尔量改了最好也重启一下相关服务。很多人改完不重启,就在那儿反复测,浪费时间不说还容易误判。

如果你按照这个流程排完之后发现权限完全正常但问题依旧,最后再分享一个小技巧:去看error.log的完整上下文,别只盯着最后一行。有时候权限问题不是根因而是连锁反应。比如后端PHP进程崩溃了,Nginx连不上socket,日志先报connect失败,后报permission denied,你把全部精力花在处理socket权限上,结果后端一启动就全好了。前后文结合着看,才能避免白忙一场。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进