1. 403到底想告诉你什么:先别慌,先看错误本质
我见过不少人一碰到 nginx 403 错误,第一反应就是chmod -R 777,然后发现没用,再换chown -R nobody,还是没用,最后开始怀疑人生。其实方向没错,但缺了个关键前置步骤——你得先搞清楚 403 到底是被谁拒绝的,拒绝发生在哪个环节。
先说个基本认知:HTTP 状态码里,403 和 404 表达的是两种完全不同的“不行”。404 是服务器找遍了也没找到文件,403 是服务器找到了,但看了一眼权限和政策,明确告诉你“这个请求我不处理”。同样是失败,403 背后的信息量要大得多——它至少证明你的请求成功到达了 nginx,nginx 也正确解析了配置,只是在某个策略判断节点上把你拦了下来。这个“拦截点”可以是文件系统权限、配置指令、安全模块、上游应用返回,任何一处出现拦截指令都会表现为 403。
所以第一步永远不是改配置,而是先回答一个问题:这个 403 是谁返回的?我用一句话总结排查顺序:从 nginx 的访问日志和错误日志里找线索,而不是靠猜。
具体操作如下:
# 查看错误日志,建议直接看最近的几十行 tail -n 50 /var/log/nginx/error.log # 如果找不到日志路径,先看 nginx 配置里的 error_log 指令 grep -E "error_log" /etc/nginx/nginx.conf错误日志是排障最重要的数据来源。我根据本地实践经验总结,常见的 403 相关日志基本就这几类:
| 日志关键词 | 含义 | 后续动作 |
|---|---|---|
Permission denied | 文件系统权限不够 | 检查 nginx 工作进程用户对文件/目录的读、执行权限 |
directory index is forbidden | 目录下没有 index 文件,且禁用了目录列表 | 补 index 文件或配置 index 指令 |
access forbidden by rule | 被访问规则拒绝 | 查 location 块里的 allow/deny、防盗链模块等 |
UPSTREAM ... refused或no live upstreams | 反代场景下上游不可用 | 排查后端应用状态和反代配置 |
看完日志,再去访问日志里确认一下那条 403 请求的细节:访问的是根路径还是具体文件、用的是 IP 还是域名、用户代理是什么浏览器。这些信息组合起来,基本上能把问题框定到两三个方向之内。
2. 权限问题:最常见,但也最容易看走眼
2.1 nginx 用什么身份跑,决定了谁能访问文件
权限问题的核心,不是“你当前登录用户有没有权限”,而是“nginx 的工作进程是以什么用户身份运行的”。
nginx 启动后,master 进程会 fork 出多个 worker 进程来真正处理请求,这些 worker 进程运行在哪个系统用户下,决定了它能读取哪些文件。查法很简单:
ps aux | grep nginx输出里你会看到类似这样的行:
root 12345 0.0 0.1 ... nginx: master process /usr/sbin/nginx www-data 12346 0.0 0.1 ... nginx: worker process第二列那个www-data(也可能是nginx、nobody,取决于发行版)就代表 worker 进程的用户身份。接下来你要保证这个用户对你托管的目录有所有父级目录的执行权限,对文件本身有读权限。
这里有一个新手特别容易踩的坑:目录的权限逻辑和文件不一样。文件只需要r(读)权限,目录则至少需要r-x,x权限表示“能否进入这个目录并访问其中的条目”。很多人把目录设成644(rw-r--r--),文件本身权限完全正确,但 nginx 根本无法进入目录,自然会得到 403。正确姿势是:文件用644、目录用755,如果涉及需要修改的场景再额外调整。
我用一句口诀总结:“文件看读,目录看进”。
2.2 明明改好了权限还是 403?父目录和挂载点在捣乱
有一次帮同事排查问题,他在/var/www/html下放了静态页面,chmod 755、chown www-data全套做足了,nginx 照样 403。我让他跑了一条命令:
namei -om /var/www/html/index.html这条命令会把路径上每一级目录和文件的所有者、权限完整列出来。结果一看,问题出在// 路径下www-data对.这一级没有写权限——不是,我说反了,实际是/var下面的某一级目录权限是700,只允许 root 进入,任何其他用户都到不了下一层。你光看/var/www/html本身没有用,得顺着路径一层一层看,只要中间某一级缺x权限,整条链就是断的。
还有一类情况:网站目录挂在独立的数据盘上,/etc/fstab里写了挂载配置。如果文件系统类型或者挂载参数有问题(比如某些文件系统默认不支持权限位变更),你chmod表面上成功,实际完全不生效,nginx 怎么都读不了。这种情况直接看mount输出的挂载选项,大概率能找到原因。
2.3 SELinux 和 AppArmor:那些“权限全对但就是 403”的元凶
如果你用的是 CentOS、RHEL、Fedora 这类带 SELinux 的发行版,权限检查的正确顺序应该是:先查 SELinux,再查文件权限。
我第一次在 CentOS 7 上部署 nginx 时就被这玩意坑惨了。文件权限、所有者、目录结构全都无懈可击,nginx 就是 403,错误日志里还只有一句干巴巴的Permission denied。查了两小时,最后发现 SELinux 把 nginx 对网站目录的访问拦截了。
判断方法:
# 查看 SELinux 状态 getenforce # 查看文件的安全上下文 ls -Z /var/www/html/index.html如果输出是Enforcing,且上下文不是httpd_sys_content_t,那基本可以锁定问题。临时解决和永久解决:
# 临时赋予 nginx 访问该目录的权限(重启后失效) chcon -Rt httpd_sys_content_t /var/www/html # 永久修改,需要写进策略,不推荐用 chcon restorecon -Rv /var/www/html如果是反代场景,还要额外注意另一个 SELinux 布尔值:nginx 作为反向代理去连接后端时,可能会被httpd_can_network_connect拦截,表现为 502 或 403。用setsebool -P httpd_can_network_connect 1开启即可。Ubuntu/Debian 系用的是 AppArmor,机制类似,排查思路一样。
提示:
getenforce输出Disabled的话,问题大概率不在 SELinux,回到普通文件权限继续排查。
3. 配置文件里的两个坑:index 和 root/alias
3.1 访问根路径 403?多半是 index 忘了配
还有一种极其常见的情况:文件权限完全正常,curl -I http://localhost/却返回 403,但直接curl http://localhost/index.html能正常访问。这叫“目录索引被禁止”。
nginx 处理目录请求时,会根据index指令在目录里找索引文件(比如index.html、index.php)。如果没找到,而你又没有开启autoindex on的话,nginx 就直接回 403——它不会像 Apache 那样默认给你列出目录内容。
我见过太多人把静态站部署上去之后,访问域名得到 403,吓出一身冷汗,其实只是网站的首页文件名是home.html而不是index.html,或者文件放在了多级子目录下没注意。解决方法有两种:
# 方案一:把实际的首页文件写进 index server { location / { index home.html; } } # 方案二:如果你确实需要展示目录结构,开启目录列表 server { location / { autoindex on; } }生产环境我个人建议用方案一。开放目录列表在公网场景下几乎等于把服务器文件结构公开给所有人,安全隐患很大,尤其是图片、备份这类敏感目录。
3.2 root 和 alias 搞混,目录直接变成“禁区”
root和alias是 nginx 静态文件服务里最容易被混淆的两个指令。两者的差异一句话说透:root会把完整的 URI 拼到指定路径后面,而alias会用指定路径替换掉location匹配的那部分前缀。
看一个典型例子:
# 有问题的写法 location /static/ { root /var/www/html/; } # 请求 /static/img/logo.png 时,nginx 找的是: # /var/www/html/static/img/logo.png # 如果这个路径不存在,且对应目录权限也不对,可能表现为 403 # 正确写法 location /static/ { alias /data/upload/; } # 请求 /static/img/logo.png 时,nginx 找的是: # /data/upload/img/logo.png为什么root写错会变 403 而不是 404?主要有两种情况:一是 nginx 找到了目录但无法读取其中的文件(回到权限问题);二是应用系统(比如 Java 的 Spring Boot)把请求再转发了一层,上游返回了 403,nginx 原样透传给客户端。
我一直以来的建议是:能用root的地方尽量用root,只有路径映射需求复杂的场景才用alias,减少心智负担,也避免踩“多了一段路径”或“少了一段路径”的坑。改完配置别忘了nginx -t先做语法检查,再nginx -s reload热加载。
4. 其他触发 403 的隐藏场景
4.1 autoindex 的附加参数,值得多花一分钟了解
我上面提到了autoindex on,但这个指令其实还有两个经常被忽略的参数:autoindex_exact_size和autoindex_localtime。
autoindex_exact_size on时,目录列表显示的文件大小是精确字节数;改成off则会显示为更易读的 KB、MB 单位。autoindex_localtime on则让最后修改时间显示为服务器本地时间,off时显示 UTC 时间。
其实这两个参数本身不会引发 403,但值得讲是因为——你开启了autoindex之后,实际浏览目录列表时如果文件大小和时间显示得莫名其妙,十有八九就是这两个参数没有配合。这是面试题里经常带的“隐藏考点”,也是实际排障时容易忽略的细节:
location /download/ { autoindex on; autoindex_exact_size off; autoindex_localtime on; }4.2 安全模块和请求限制:看起来像权限问题,其实是策略拦截
如果你确认文件权限、SELinux、index 配置都没问题,但还是 403,那就要考虑 nginx 里的“规则拦截”了。常见的有这么几类:
1)allow/deny访问控制
location /admin/ { deny all; }这种配置在多层 location 嵌套时很容易“误伤”——你以为只拦了/admin/,实际上某个子路径也被继承规则覆盖了。排查方法是在错误日志里看有没有access forbidden by rule字样。
2)防盗链模块
location ~ \.(gif|jpg|png)$ { valid_referers none blocked *.example.com; if ($invalid_referer) { return 403; } }这个配置的本意是防别人直接引用你的图片,但如果你本地测试时用了空的Referer,none通常是放行的,可如果来源域名写错了,或者curl测试时带了奇怪的 Referer,也会莫名其妙 403。这是我踩过的坑——本地明明是合法的来源,结果因为 Referer 里带着http://localhost而配置里只写了正式域名,直接给拦了。
3)auth_basic导致的重定向循环
配置了 Basic Auth 后如果认证文件路径写错,nginx 会直接 403,错误日志里能看到类似user "xxx" was not found的信息。
4)上游应用返回的 403
nginx 做反向代理时,它自己可能一切正常,但后端的 Tomcat、Node.js、Spring Boot 应用出于自身的安全策略返回了 403 页面,nginx 透传回来。这类问题必须去查后端应用日志,你在 nginx 这一侧怎么折腾都没用。判断方法很简单:看 nginx 错误日志里有没有内容,如果 nginx 日志干干净净、access log 也只有一条普通的 403 记录,优先怀疑上游。
5)代理场景下的请求头限制
有些后端网关对请求头大小有限制(比如个别 WAF 产品),请求头超限时返回 403。这需要对比正常请求和异常请求的头部差异,再配合后端文档调整client_max_body_size等参数。这部分要区分好:请求体太大通常报 413,请求头异常更可能报 400 或 403,需要根据实际日志判断,不要头脑一热把所有 4xx 都归结到 nginx 配置上。
4.3 那些看起来像 403 其实不是 403 的“意外”
还有一类情况比较奇葩:你访问http://你的服务器IP/得到了 403,但用域名访问却一切正常。这不是 nginx 配置问题,多半是浏览器或运营商层面给“裸 IP 访问”加了一些限制,或者你的服务器上其他安全组件(如云安全组)对 IP 直连做了策略。属于环境干扰,别在 nginx 配置上死磕。
另一种“假 403”:你抓包看返回的是 403,但响应体不是 nginx 的默认错误页,而是某个中间设备的拦截页面。这种情况建议先确认响应头里的Server字段,如果Server不是你熟悉的nginx/1.20.1这类信息,那可能流量经过了第三方网关。
5. 排障速查表:把 403 分成三种颜色,对症下药
为了让你以后碰到 403 时不用翻这篇文章,我整理了一份“现象分类速查表”。遇到 403,先看日志,再对着表格找方向:
| 现象特征 | 优先检查项 | 常见处理方式 |
|---|---|---|
错误日志有Permission denied | worker 进程用户、父目录权限、SELinux | 检查目录755/ 文件644,chown给对用户,必要时调整 SELinux 上下文 |
| 访问根路径 403,直接访问具体文件正常 | index 指令、autoindex 配置 | 补index指令或打开autoindex |
| 特定路径 403,其他路径正常 | location 匹配规则、root/alias 写法 | 核对路径拼接结果,调整相关指令 |
错误日志有access forbidden by rule | allow/deny、防盗链、auth_basic | 检查对应规则的匹配范围,逐层放宽验证 |
| nginx 日志干净,但客户端 403 | 反代场景、上游应用 | 查后端应用日志和 WAF 拦截记录 |
| 请求头很大时 403 | 上游网关注入的请求限制 | 调整请求头大小限制,联系网关侧确认 |
| IP 访问 403、域名访问正常 | 安全组、浏览器插件、网关策略 | 换网络环境验证,排查中间层限制 |
这个表是我自己排障时一直沿用的框架。实际工作中,80% 的 403 都落在前三行里,剩下 20% 需要靠日志里的蛛丝马迹去追。别上来就chmod -R 777,也别一上来就怀疑 nginx 被入侵了——按“先日志、再权限、后配置、最后环境”的顺序走,一小时之内基本能定位。
最后再说一句:改完任何配置后,nginx -t永远是必做的一步,它能在你最不想出问题的时刻提前暴露语法错误。至于那个“最不想出问题的时刻”什么时候来,你永远不知道——但你知道的是,多一条检查,就少一次半夜被叫起来的可能。