上一篇 分享链接 返回 返回顶部

Nginx 502 Bad Gateway 排查中,如何核对服务身份与后端目录权限?

发布人:Minchunlin 发布时间:2026-09-29 19:25 阅读量:31
Nginx 502 Bad Gateway 排查中,如何核对服务身份与后端目录权限?

一次请求从客户端发出后,通常会依次经过 Nginx 监听端口、虚拟主机和 location,再由 Nginx 通过 TCP 或 Unix Socket 连接后端服务。后端进程接到请求后,还要读取代码、配置和依赖,并写入日志、缓存、上传或临时目录。链路中的任意一环失败,客户端都可能看到 502 Bad Gateway。

要找出 如何找出 Nginx 502 Bad Gateway 的根本原因,不能先执行 chmod -R 777。正确顺序是:先确认生效配置和错误日志,再确定 Nginx worker 与后端进程的实际用户、用户组,随后检查 TCP 端口或 Unix Socket,最后沿着从根目录到目标文件的完整路径核对目录权限,并用同一条请求链路复测。需要特别区分:Nginx worker 主要需要访问 Socket、静态文件或脚本路径;后端进程则需要读取应用代码,并对实际运行目录拥有必要的写权限。

先确认 502 发生在哪一跳

查看生效配置和实际日志

不要只打开某个配置文件判断,因为它可能没有被当前 Nginx 加载。先查看完整生效配置:

sudo nginx -T 2>/dev/null | grep -nE 'server_name|location|proxy_pass|fastcgi_pass|uwsgi_pass|scgi_pass'

根据结果确认 Nginx 使用的后端连接方式:

proxy_pass http://127.0.0.1:8080;

表示通过 TCP 连接后端。

proxy_pass http://unix:/run/example/app.sock:;

表示通过 Unix Socket 连接后端。

fastcgi_pass unix:/run/php/php-fpm.sock;

通常表示通过 FastCGI Unix Socket 连接 PHP-FPM。

fastcgi_pass 127.0.0.1:9000;

表示通过 TCP 连接 FastCGI 服务。uwsgi_pass 和 scgi_pass 则分别使用对应协议,不能简单使用普通 HTTP 请求判断。

如果同一个域名配置了多个 server 或 location,还要确认实际请求命中了哪一段配置。查看访问日志和错误日志:

sudo tail -n 100 /var/log/nginx/access.log
sudo tail -n 100 /var/log/nginx/error.log

日志路径可能因发行版和自定义配置不同而变化,可以从生效配置中查找:

sudo nginx -T 2>/dev/null | grep -nE 'access_log|error_log'

根据错误日志建立初步判断

错误日志可以缩小范围,但不能脱离实际路径、服务身份和后端状态直接下结论。

日志片段或现象常见含义下一步检查
connect() failed (111: Connection refused)目标端口没有服务监听,或后端刚好退出检查后端进程、监听端口和应用日志
connect() failed (2: No such file or directory)Unix Socket 不存在,或后端尚未创建 Socket检查 Socket 路径、后端启动状态和配置是否一致
connect() failed (13: Permission denied)Nginx worker 无权访问 Socket,或无法进入路径中的某一级目录检查 Nginx 用户、用户组、Socket 和每一级父目录权限
upstream timed out后端未在超时时间内返回,可能是应用阻塞或资源不足检查后端日志、进程状态和请求处理情况
upstream prematurely closed connection后端提前关闭连接,可能是应用异常退出或处理请求时崩溃检查后端服务日志和进程状态
upstream sent no valid HTTP/1.0 header上游协议或端口配置不匹配核对 proxy_pass、FastCGI 或其他协议配置
浏览器显示 502,但错误日志没有对应记录请求可能没有到达当前 Nginx,或查看了错误的日志文件核对访问入口、虚拟主机和实际日志路径

Permission denied 直接指向权限链路,但 No such file or directory 也可能是后端因权限不足启动失败,最终没有创建 Socket。因此,502 本身不是修改权限的充分依据。

先确认两个服务分别以谁的身份运行

权限判断的关键不是“哪个登录用户可以访问”,而是“实际执行访问动作的进程以哪个用户和用户组运行”。至少要区分:

  1. Nginx 主进程启动时的身份;
  2. 真正处理请求的 Nginx worker 身份;
  3. 后端主进程和实际处理请求的后端 worker 身份。

检查 Nginx 主进程和 worker

在使用 systemd 的 Linux 环境中,可以先查看服务单元和进程:

sudo systemctl show nginx -p User -p Group -p MainPID
sudo systemctl cat nginx
ps -eo user,group,pid,ppid,args | grep '[n]ginx'

再查看 Nginx 配置中的工作进程用户:

sudo nginx -T 2>/dev/null | grep -nE '^[[:space:]]*user[[:space:]]'

很多发行版中,Nginx 主进程可能以 root 启动,再按照配置降权为 www-data、nginx 或其他用户。主进程能够读取某个路径,并不代表 worker 也有权限。

如果需要确认某个 worker 的补充用户组,可以先找到 worker PID,再读取进程状态:

WORKER_PID=$(pgrep -f 'nginx: worker process' | head -n 1)
sudo awk '/^(Uid|Gid|Groups):/ {print}' /proc/"$WORKER_PID"/status

ps 中显示的主用户组不一定包含所有补充组。Socket 或目录通过组权限开放时,应使用 id 和 getent group 检查组关系:

id www-data
getent group appgroup

将 www-data 和 appgroup 替换成实际用户和用户组。

检查后端服务身份

先根据实际环境确定服务名称,不要直接猜测:

systemctl list-units --type=service --state=running | grep -Ei 'php|fpm|node|gunicorn|uwsgi|app'

确定服务后查看单元配置和进程:

sudo systemctl show <后端服务名> -p User -p Group -p MainPID
sudo systemctl cat <后端服务名>
ps -eo user,group,pid,ppid,args | grep -E '[p]hp-fpm|[n]ode|[g]unicorn|[u]wsgi'

如果服务单元没有显式设置 User=,还要检查启动脚本、启动参数或后端自身的 worker 配置。后端主进程可能与处理请求的子进程使用不同身份,不能只看 MainPID 对应的一个进程。

以 PHP-FPM 为例,还要继续核对 Pool 配置中的 user、group 和 listen:

sudo grep -RInE '^[[:space:]]*(user|group|listen|listen.owner|listen.group|listen.mode)[[:space:]]*=' \
  /etc/php /etc/php-fpm.d 2>/dev/null

至少需要确认:

  • Nginx 的 fastcgi_pass 与 PHP-FPM 的 listen 路径完全一致;
  • Socket 的所有者、用户组和模式允许 Nginx worker 访问;
  • PHP-FPM worker 能够读取应用代码和依赖;
  • PHP-FPM worker 能够写入应用实际需要写入的日志、缓存、上传或临时目录。

只修改 Nginx 的 user,却没有同步检查 Socket 和后端目录,502 可能不会消失,甚至会从一个权限错误变成另一个权限错误。

按 TCP 或 Unix Socket 检查后端连接

TCP 后端:监听端口不等于应用正常

如果配置中的后端是 127.0.0.1:8080 或其他 TCP 地址,先查看监听状态:

sudo ss -lntp

检查指定端口时,可以使用:

sudo ss -lntp | grep ':8080'

将 8080 替换为生效配置中的实际端口。结果应这样理解:

  • 能看到对应端口监听:说明至少有进程占用了该端口,但不代表协议正确、应用健康或响应正常;
  • 没有监听:后端可能未启动、启动后退出、监听地址不一致,或者 Nginx 配置中的端口写错;
  • 后端只监听 127.0.0.1,而 Nginx 连接的是其他地址:需要核对监听地址与 proxy_pass 是否匹配;
  • 监听端口属于其他服务:端口开放不能证明目标后端正常。

如果后端提供 HTTP,可以从服务器本机发起只读请求:

curl -v --max-time 10 http://127.0.0.1:8080/

如果后端不是 HTTP 服务,不要用这个命令判断应用是否正常,应使用该服务支持的健康检查方式,或直接查看服务日志。

后端直连成功,只能说明该端口上的服务能够处理这次直连请求。它不能证明 Nginx worker 使用的身份可以访问 Unix Socket,也不能证明实际虚拟主机、协议配置和脚本路径正确。

Unix Socket:文件、父目录和身份必须同时满足

如果配置使用 Unix Socket,先查看 Socket 本身及其路径:

sudo stat /run/example/app.sock
sudo namei -l /run/example/app.sock

重点检查:

  • Socket 文件是否存在;
  • Socket 的所有者和所属组;
  • Socket 的权限模式;
  • /run、/run/example 等父目录是否允许 Nginx worker 进入;
  • 后端重启后 Socket 是否会被删除并重新创建。

namei -l 不能省略。即使 Socket 文件本身是 0660,只要上级目录缺少执行权限,Nginx 仍然无法访问。对目录而言:

  • r 表示读取目录项;
  • x 表示进入目录并访问其中已知名称的对象;
  • w 表示在目录中创建、删除或重命名目录项;
  • 访问文件时,路径上的每一级目录通常都需要相应的 x 权限。

因此,Socket 权限排查应同时看文件权限和完整路径,而不是只执行一次 ls -l app.sock。

沿完整路径检查代码、静态文件和运行目录

从根目录开始检查每一级路径

假设应用入口文件为 /srv/www/example/public/index.php,先使用只读命令检查,不要递归修改权限:

TARGET=/srv/www/example/public/index.php

sudo namei -l "$TARGET"
sudo stat "$TARGET"
sudo stat -c '%A %a %U:%G %n' \
  /srv \
  /srv/www \
  /srv/www/example \
  /srv/www/example/public \
  "$TARGET"

需要确认:

  • 实际服务用户能进入应用路径中的每一级目录;
  • 后端用户对代码和依赖具备所需的读取权限;
  • Nginx worker 对静态文件具备读取权限;
  • 目录所有者和所属组符合部署设计;
  • 目标文件不是软链接到另一个权限不正确的路径。

检查软链接的真实目标:

readlink -f /srv/www/example/public/index.php
sudo namei -l "$(readlink -f /srv/www/example/public/index.php)"

如果应用通过软链接指向发布目录、共享目录或挂载目录,必须继续检查真实目标路径,而不是停留在软链接本身。

用实际服务用户进行低风险测试

确定服务用户后,可以模拟该用户执行只读的 test 命令:

SERVICE_USER=www-data
APP_FILE=/srv/www/example/public/index.php
APP_DIR=/srv/www/example/public

sudo -u "$SERVICE_USER" -- test -x "$APP_DIR" \
  && echo "目录可进入" \
  || echo "目录不可进入"

sudo -u "$SERVICE_USER" -- test -r "$APP_FILE" \
  && echo "文件可读取" \
  || echo "文件不可读取"

将 SERVICE_USER 替换成从 ps、服务配置和进程状态中确认的实际用户。应用需要写入运行目录时,再单独检查:

SERVICE_USER=appuser
RUNTIME_DIR=/srv/www/example/var

sudo -u "$SERVICE_USER" -- test -d "$RUNTIME_DIR" \
  && echo "运行目录存在" \
  || echo "运行目录不存在"

sudo -u "$SERVICE_USER" -- test -w "$RUNTIME_DIR" \
  && echo "运行目录可写" \
  || echo "运行目录不可写"

上述命令只测试目录和文件属性,不会创建文件。不要为了测试直接执行 touch,因为它会改变目录内容。如果必须验证真实写入能力,应先确认目录用途、备份现有状态,并在明确的临时测试目录中操作。

这些测试主要验证传统 Unix 权限和部分 ACL,不一定能够复现 systemd 服务的完整安全上下文。若命令显示可访问而服务日志仍显示拒绝,还要检查 SELinux 或 AppArmor。

不同目录应采用不同权限

代码、静态文件和运行目录的职责不同,不能全部设置成同一种权限:

目录类型通常需要的权限不应默认授予的权限
应用代码目录后端用户可读,目录按路径需要具备执行权限Nginx 或后端用户的写权限
静态文件目录Nginx worker 可读,父目录可进入Nginx 对整个站点目录的写权限
上传目录应用用户可写,必要时 Nginx 可读对所有用户开放写权限
缓存、日志、临时目录对应应用用户可写对整个应用根目录开放写权限
Unix Socket 所在目录Nginx 和后端按需进入对所有用户开放读写

例如,应用代码只需要读取时,可以让代码由部署用户或 root 拥有,再通过专用用户组给后端提供读取权限。但 750、640 等模式不是所有环境的固定答案,必须根据实际用户、用户组、ACL 和目录用途确认。

排除安全策略和后端启动失败

Unix 权限正常时检查 SELinux 或 AppArmor

如果 namei、stat 和模拟用户测试都显示权限正常,但错误日志仍出现 Permission denied,检查强制访问控制策略。

SELinux 环境可以执行:

getenforce
ls -Zd /srv/www/example /srv/www/example/public
sudo ausearch -m avc -ts recent 2>/dev/null

SELinux 强制模式下,文件安全上下文也会影响 Nginx 或后端访问。不要直接关闭安全策略来验证问题,应确认目标目录的正确上下文,并按照系统管理规范修复。

如果系统启用了 AppArmor,可以查看状态:

sudo aa-status 2>/dev/null

结果可以这样处理:

  • Unix 权限测试失败:先修复用户、用户组、目录或文件权限;
  • Unix 权限通过,但 SELinux 或 AppArmor 日志明确拒绝:修复安全策略或文件上下文;
  • 两类策略都没有拒绝:继续检查后端进程、Socket、协议和应用日志。

后端启动失败也可能表现为 502

后端可能因为无法读取配置、无法写入日志、无法创建缓存目录而启动失败。对 Nginx 来说,最终可能只是端口无人监听或 Socket 不存在。

查看服务状态和近期日志:

sudo systemctl status <后端服务名> --no-pager
sudo journalctl -u <后端服务名> --since "15 minutes ago" --no-pager

如果服务反复重启,重点检查:

  • 配置文件是否可读;
  • 应用代码、依赖目录是否可读;
  • 日志、缓存、临时目录是否可写;
  • Socket 父目录是否存在;
  • 服务启动后是否因权限异常立即退出。

Nginx 只负责转发请求,不能替代后端应用的启动日志和运行日志。此时应先让后端稳定启动并生成正确的监听端口或 Socket,再回到 Nginx 入口验证。

采用最小权限方式修复

修改前记录现状和影响范围

权限变更前先记录受影响路径的所有者、模式和 ACL:

TARGET_DIR=/srv/www/example

sudo stat -c '%A %a %U:%G %n' "$TARGET_DIR" > /var/tmp/example-permission-before.txt
sudo getfacl -p "$TARGET_DIR" > /var/tmp/example-acl-before.txt 2>/dev/null || true

如果准备修改整个目录树,应扩大记录范围,或先使用现有备份保存目录树的权限和所有者信息。记录文件应放在受控位置,因为其中可能包含路径、用户和访问控制信息。回滚时使用此前保存的准确值,不要用一个统一的“反向权限命令”覆盖整个目录。

只修复代码读取权限

如果确认后端只需要读取代码,不需要修改代码,可以为明确的目录和文件范围设计专用组,而不是直接放开所有权限:

sudo chown root:appgroup /srv/www/example
sudo chmod 750 /srv/www/example

sudo chown -R root:appgroup /srv/www/example/current
sudo find /srv/www/example/current -type d -exec chmod 750 {} \;
sudo find /srv/www/example/current -type f -exec chmod 640 {} \;

这些命令会改变指定目录树中的所有者和权限,影响范围较大。只有在已经确认目录归属、用户组关系和应用读取方式后才适合执行。执行前应备份或完整记录原有 ACL、所有者和模式;如果结果不符合预期,应根据记录逐项恢复。

确认服务用户是否属于专用组:

id www-data
getent group appgroup

必要时可以将服务用户加入专用组,但组成员变更通常需要重新启动相关服务或重新建立进程,已经运行的进程不会自动获得新的组列表。相比把服务加入多个无关用户组,更推荐建立职责清晰的应用专用组。

只让实际运行目录可写

如果错误来自缓存、日志、上传或临时目录,不要把整个应用根目录改成可写。只对实际需要写入的目录设置应用用户和用户组:

sudo install -d -o appuser -g appgroup -m 750 /srv/www/example/var/cache
sudo install -d -o appuser -g appgroup -m 750 /srv/www/example/var/log

install -d 在目录不存在时会创建目录,在已存在目录上也可能调整属性。执行前要确认变量和路径没有写错,目标不是不应修改的软链接或挂载点,并先记录已有内容和权限。

Nginx 通常不需要写入应用代码目录。只有在 Nginx 确实负责接收上传或生成特定缓存时,才为对应的单独目录授予写权限,并避免让上传内容直接作为可执行代码处理。

按配置修复后端 Socket 权限

如果日志明确显示 Nginx 无法访问 Unix Socket,应同时核对:

  1. Nginx 的 fastcgi_pass 或 proxy_pass 路径;
  2. 后端服务的 listen 路径;
  3. Socket 的所有者、所属组和权限模式;
  4. Socket 所在目录及其所有父目录的进入权限。

以 PHP-FPM Pool 为例,常见设计是让 Nginx 用户通过专用组访问 Socket:

listen = /run/example/app.sock
listen.owner = www-data
listen.group = appgroup
listen.mode = 0660

这只是配置示例,实际用户、用户组、路径和服务名称必须以当前系统为准。修改前备份对应配置:

sudo cp -a /实际/php-fpm-pool.conf /实际/php-fpm-pool.conf.bak

修改后重启负责创建 Socket 的后端服务,并立即检查新文件:

sudo systemctl restart <后端服务名>
sudo stat /run/example/app.sock
sudo namei -l /run/example/app.sock

重启后端会造成短暂服务中断,适合在维护窗口或已确认影响范围时执行。如果后端每次重启都会重新创建 Socket,手工执行 chown 或 chmod 只能临时生效,应把所有者、用户组和模式写入后端 Pool 配置或对应的运行目录管理配置中。

沿同一条链路复测

先检查配置,再让新身份生效

Nginx 配置修改后先执行:

sudo nginx -t

只有配置测试成功,才执行平滑重载:

sudo systemctl reload nginx

如果修改了 Nginx 的 user、Socket 访问组或 worker 相关配置,必须确认新 worker 已经按预期启动。若修改的是后端服务用户、Pool 配置或运行目录,Nginx reload 不会改变已经运行的后端进程身份,还需要按实际情况重启后端服务。

再验证后端本身

TCP 后端:

sudo ss -lntp | grep ':8080'
curl -v --max-time 10 http://127.0.0.1:8080/health

没有专用 /health 路径时,改用实际存在且不会产生副作用的只读请求。

Unix Socket 后端:

sudo stat /run/example/app.sock
sudo namei -l /run/example/app.sock

如果后端支持通过 Socket 进行专用健康检查,应使用该服务支持的检查方式。后端直连成功,说明进程和基本协议可用,但还不能证明 Nginx worker 一定有权限访问 Socket。

最后验证 Nginx 实际入口

从服务器本机或实际访问位置请求 Nginx:

curl -i --max-time 15 http://127.0.0.1/实际路径

如果依赖域名匹配,补充 Host 请求头:

curl -i --max-time 15 \
  -H 'Host: example.com' \
  http://127.0.0.1/实际路径

同时观察错误日志:

sudo tail -f /var/log/nginx/error.log

不同结果对应不同的下一步:

  • 后端直连失败,Nginx 入口也返回 502:优先处理后端进程、端口、Socket 或应用目录权限;
  • 后端直连成功,Nginx 仍返回 502:重点检查 Nginx worker 用户、协议配置、Socket 路径和父目录权限;
  • Nginx 不再返回 502,但变成 403:上游连接问题可能已经解决,现在单独检查 Nginx 读取静态文件或目录的权限;
  • 返回 404:请求可能已经到达应用,但路由、根目录或入口文件配置不正确;
  • 返回 500:请求已经进入后端,继续查看应用日志和后端运行目录;
  • 返回 200 或预期业务状态码,且错误日志不再出现新的上游错误:说明本次权限链路已经通过基本验证。

排查中最容易误判的情况包括:只检查文件、不检查父目录;只看 Nginx 主进程、不看 worker;只看 Socket 文件、不看创建 Socket 的后端配置;只重载 Nginx、不重启创建 Socket 的后端;以及把所有目录统一设为可写。按照“生效配置与日志—进程身份—监听端口或 Socket—完整目录链—后端日志—Nginx 入口复测”的顺序逐层核对,才能把表面的 502 定位到具体的用户、用户组、目录或运行时权限,而不是停留在盲目放开权限的猜测上。

目录结构
全文