新加坡服务器上的Nginx报403,如何检查目录权限并按最小权限修复

文件已经设为 644,Nginx 仍然返回 403,并不矛盾。文件可读不代表路径可达:只要某一级父目录不允许 Nginx 工作进程穿越,请求就可能被拒绝。不过,403 也可能来自缺少首页、访问规则或上游应用,不能看到状态码就直接修改权限。
排查新加坡服务器上的这类问题,应依次确认:请求命中的站点与错误日志 → Nginx worker 的实际身份 → 文件及每一级父目录权限 → ACL 与安全策略。只有确认是文件系统权限不足,才为实际服务身份补齐必要权限;不要使用 chmod -R 777,也不要把整个站点交给 Nginx 所有。
先区分:403 是否真的由目录权限引起
以下示例适用于 Linux 上直接部署的 Nginx,站点路径假设为 /srv/www/site/public。路径、域名和用户名均需替换为实际值。检查命令大多只读;修改权限的步骤需要 root 权限,并应先备份。容器部署需在对应容器或挂载命名空间中核验,不能直接套用宿主机用户名。
先查看生效配置:
sudo nginx -T
重点检查命中请求的 server_name、listen、location、root 或 alias,以及 index、try_files、error_log。如果服务通过自定义配置文件启动,应使用相同的 -c、-p 参数,避免检查到另一份配置。
nginx -T 可能包含内部地址或认证配置,不要未经脱敏公开完整输出。
针对同一个请求查看错误日志:
# 替换为实际 error_log 路径
sudo tail -n 100 /var/log/nginx/error.log
| 日志或现象 | 更可能的原因 | 下一步 |
|---|---|---|
open() "...index.html" failed (13: Permission denied) | 路径权限、文件权限、ACL 或安全策略拒绝访问 | 检查 worker 身份与完整路径 |
directory index of "..." is forbidden | 请求落到目录,没有可用首页且未开启目录列表 | 检查 index、首页文件及路径映射 |
access forbidden by rule | Nginx 访问控制规则拒绝请求 | 检查命中的 allow、deny 等配置 |
请求经 proxy_pass 转发,403 来自上游 | 上游应用拒绝访问 | 不要先修改本地静态目录权限 |
Permission denied 说明发生了访问拒绝,不等于一定是传统的 chmod 权限位错误。SELinux、AppArmor 也可能产生类似现象。
如果日志指向目录首页问题,可以请求一个已知存在的静态文件。静态文件正常而目录 URL 返回 403 时,应优先核对首页配置,不要通过开启 autoindex 掩盖问题,以免暴露目录内容。
为什么文件可读,Nginx 仍然访问不了
对普通静态资源请求,Nginx 通常需要:
- 对路径中的每一级目录具有
x,即搜索、穿越权限。 - 对最终文件具有
r,即读取权限。 - 不需要对静态文件具有
x,也不需要对站点目录具有w。
目录的 r 允许列出目录项,x 允许通过已知名称访问目录内对象。普通静态文件读取通常不要求列出整个目录,因此只为访问已知路径补权限时,目录 x 比目录 rwx 更符合最小权限原则。
例如,访问:
/srv/www/site/public/index.html
即使 index.html 是 644,如果 /srv/www/site 是 700,所有者又不是 worker 用户,Nginx 仍然可能无法穿越这一级目录。
确认执行读取的是谁
不要用 master 进程的 root 身份判断站点权限。正常服务请求通常由 worker 处理:
ps -eo pid,ppid,user,group,args | grep '[n]ginx'
找到 nginx: worker process 对应的用户。发行版和安装方式不同,账号可能不同,应以实际输出为准。
假设检查结果是 www-data:
id www-data
# 将 1234 替换为实际 worker PID
sudo grep -E '^(Uid|Gid|Groups):' /proc/1234/status
id 展示账号当前的用户组信息,/proc 展示运行中进程的实际身份和附加组。刚调整过组成员关系时,两者可能不一致。
后续 sudo -u www-data 测试会按账号当前身份启动新进程,不会自动复现旧 worker 的所有运行条件。若依赖新增用户组修复,应按服务管理方式使 worker 更新身份,再核验 /proc;重启可能影响连接,应安排维护窗口,而不是只凭 id 判断已经生效。
沿实际路径检查,而不是只看站点目录
先检查每一级目录及目标文件:
namei -l /srv/www/site/public/index.html
输出会逐层列出路径的权限、所有者和所属组。根据 worker 的身份逐级判断:
- worker 是所有者时,先看所有者权限。
- 不属于所有者时,再结合所属组、附加组及其他用户权限判断。
- 存在扩展 ACL 时,还要检查命名用户、组及 ACL mask,不能只看
ls -l。
权限类别并不是“这一组不允许,就自动尝试下一组”。例如用户已经匹配文件所有者,不会再借用其他用户权限获得访问能力。
检查 ACL 和实际读取能力:
sudo getfacl -p /srv /srv/www /srv/www/site \
/srv/www/site/public /srv/www/site/public/index.html
sudo -u www-data -- test -x /srv/www/site
echo $?
sudo -u www-data -- test -r /srv/www/site/public/index.html
echo $?
sudo -u www-data -- head -c 1 /srv/www/site/public/index.html >/dev/null
echo $?
紧跟命令的退出码 0 表示成功,非零表示失败。head 会实际打开并读取文件,比只观察权限位更直接。
如果系统缺少 namei、getfacl 或 setfacl,先确认发行版,再使用对应包管理器安装所需工具,不要混用不同系统的安装命令。
还应检查软链接:
readlink -f /srv/www/site/public/index.html
如果发布目录指向另一个版本目录,必须检查目标路径及其父目录。链接所在目录权限正确,并不能保证链接目标可访问。若日志中的路径与预期不一致,先修正 root、alias 或内部跳转映射,不要给错误目录增加权限。
按最小权限修复:只授权读取所必需的路径
静态站点通常应由发布账号维护,Nginx 只负责读取。把文件所有者改成 worker 用户,可能让服务进程获得原本不需要的修改能力;将目录统一设成 755,则可能扩大其他本地账号的访问范围。
对于“仅缺少一个服务账号的读取权限”这一情况,可以使用 ACL 精确授权。前提是文件系统支持 POSIX ACL,且已确认实际 worker 用户。
下面假设:
- worker 用户确实是
www-data。 /srv、/srv/www已允许穿越。/srv/www/site、public缺少穿越权限。index.html缺少读取权限。
修改前备份并检查影响范围
在 root shell 中执行以下命令;操作仅涉及列出的路径,不递归修改整个站点:
sudo -i
backup=$(mktemp /root/nginx-perms.XXXXXX)
getfacl -p /srv /srv/www /srv/www/site \
/srv/www/site/public \
/srv/www/site/public/index.html > "$backup"
printf '权限备份:%s\n' "$backup"
确认备份成功且内容完整后,再预览变更:
setfacl --test -m u:www---x \
/srv/www/site /srv/www/site/public
setfacl --test -m u:www-r-- \
/srv/www/site/public/index.html
ACL 的 mask 会限制命名用户及组条目的有效权限。 setfacl 默认可能重新计算 mask,如果原有条目受 mask 限制,扩大 mask 可能同时恢复其他主体的权限。因此应将预览与 getfacl 输出对照;存在复杂 ACL 时,先确认不会扩大无关账号权限,再执行。
仅补齐已确认缺失的权限
setfacl -m u:www---x \
/srv/www/site /srv/www/site/public
setfacl -m u:www-r-- \
/srv/www/site/public/index.html
这组修改不授予写权限,也不改变文件所有者。静态页面引用的 CSS、JavaScript、图片若位于其他路径,还需要逐项确认相应目录可穿越、文件可读。
若整个发布树都要授权,应先明确其中是否混有密钥、备份或应用配置,再划定范围;不要直接对混合内容目录递归授权。新增文件是否继承权限,也取决于发布方式:临时目录构建后切换软链接、移动或替换文件,都可能让一次性修复失效。
权限变更通常无需 reload Nginx,后续打开文件时即可使用新权限。
需要撤销时恢复备份
在同一 root shell 中执行:
setfacl --restore="$backup"
若已退出该 shell,应把变量替换为之前记录的备份绝对路径。恢复仅针对备份列出的对象,不会撤销其他配置修改;文件若在此期间被发布流程替换,应先核对当前对象,避免覆盖后续的合法权限调整。
权限看似正确时,检查 ACL 与安全策略边界
如果 getfacl 中出现:
user:www-r--
mask::---
命名用户虽然写着可读,实际仍受 mask 限制。应结合输出中的 effective 权限判断,而不是只看单个条目。
如果以 worker 用户执行读取成功,但真实 HTTP 请求仍因访问拒绝失败,需要继续检查进程安全上下文。以 SELinux 系统为例:
getenforce
ls -Zd /srv/www/site /srv/www/site/public
ls -Z /srv/www/site/public/index.html
sudo ausearch -m AVC -ts recent
若启用了 AppArmor,可查看:
sudo aa-status
sudo journalctl -k --since "10 minutes ago" | grep -i apparmor
这些命令仅适用于已安装对应组件的系统。应将拒绝记录中的时间、进程和路径与请求对应起来。不要把关闭 SELinux、AppArmor 或放宽整个站点权限当作常规修复。 确认策略拒绝后,应备份现有规则,按实际目录用途调整持久标签或对应 profile,并记录恢复方法。
如果进程看到的路径与宿主机不同,还需核验服务沙箱、容器挂载或 chroot;此时宿主机上的读取测试并不能证明 worker 一定可读。
用同一个请求验证,避免“修好了却测错站点”
修复后先重复 worker 身份下的读取测试,再使用原域名、协议、端口和 URI 发起请求。以下适用于本机 Nginx 监听 HTTP 80 端口的情况:
curl -sS -D - -o /dev/null \
--resolve example.com:80:127.0.0.1 \
http://example.com/index.html
HTTPS 应使用实际域名和 443 端口,使 Host 与 TLS SNI 同时匹配;若 Nginx 未监听回环地址,则替换为实际监听地址。不要只请求 IP,否则可能命中默认站点。
验收时至少确认:
1. 已知静态文件返回预期响应,访问日志记录的是本次请求。
2. 同一时间的错误日志不再出现对应路径的 Permission denied。
3. 目录 URL 能按预期找到首页,页面依赖的静态资源也可读取。
4. getfacl 显示 worker 只获得所需权限,没有意外增加写权限。
5. 再次发布后权限仍符合预期,不会因替换文件而复发。
如果静态文件已经可读,但目录 URL 仍返回 403,应回到首页和路径映射检查;如果日志不再报告文件访问失败,而是上游返回 403,就应停止扩大目录权限。新加坡服务器上的 Nginx 权限修复是否到位,最终要由实际 worker 身份、实际请求路径和对应日志共同证明,而不是由某个统一的 chmod 数值决定。