「Permission denied」到底是谁不给权限:读透 Linux 文件权限的完整排查手册
服务器运维里最高频、也最容易让人抓狂的一句话就是 Permission denied。Nginx 报 403、PHP 写文件失败、cron 脚本跑不动、SSH 密钥登不进去 —— 表面都是权限问题,但根因可能藏在权限位的三个数字里、藏在 umask 里、藏在 POSIX ACL 里,甚至藏在一个看不见的 chattr +i 属性里。
这篇文章给你一套完整的排查思路:从最基础的 ls -l 权限位读法,到 umask 的隐式影响,再到 ACL 和 chattr 这两个「隐藏关卡」,最后给一份从现象到根因的排查清单。全部用真实命令,可以直接在服务器上跟着敲。
一、先读懂 ls -l 那一长串
执行 ls -l,你会看到类似这样一行:
-rwxr-x--- 2 www-data www-data 4096 Oct 4 21:00 uploads
drwxr-xr-x 5 root root 4096 Sep 30 10:00 /var/www拆开看第一列的 10 个字符:
- rwx r-x ---
| | | |
| | | └── other(其他人)的权限
| | └─────── group(所属组)的权限
| └──────────── owner(属主)的权限
└──────────────── 文件类型(- 普通文件,d 目录,l 符号链接)权限位含义:
- r (read, 4):读。对文件是读取内容;对目录是「能列出目录里的文件名」(ls)。
- w (write, 2):写。对文件是修改内容;对目录是「能在里面创建/删除文件」。
- x (execute, 1):执行。对文件是运行它;对目录是「能进入(cd)并在里面访问已知名字的文件」。
八进制表示就是三者相加:rwx=7,r-x=5,---=0。上面那个 uploads 目录是 750,属主全权,组内可读可进,其他人无权。
二、目录的权限比文件更容易踩坑
新手最常见的误解:只看文件的权限,忘了目录的权限。记住这个规则:
要访问 /a/b/c.txt,你需要对路径上每一个目录都有 x(执行)权限。
如果 /var/www/uploads 的权限是 drw-------(属主可读写但没有 x),那么即使文件本身是 777,你也进不去这个目录,读不到文件。这是「文件权限明明对了却还是 Permission denied」的头号原因。
另一个反直觉的点:删除一个文件,取决于目录的 w 权限,而不是文件的 w 权限。 只要你对目录有写权限,就能删除里面任何文件,哪怕那个文件是 444 只读的。这也解释了为什么共享目录里别人能删你的文件。
快速诊断路径上的目录权限:
namei -l /var/www/uploads/2026/file.txtnamei -l 会把路径上每一级的权限都列出来,一眼看出是哪一级缺了 x:
f: /var/www/uploads/2026/file.txt
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-x--- www-data www-data uploads
drwx------ www-data www-data 2026
-rw-r--r-- www-data www-data file.txt这里 2026 目录是 700 属主 www-data,如果当前用户不是 www-data,就在这一级被挡住。诊断完成。
三、umask:新文件权限为什么永远不是你想的那样
你 touch newfile,然后 ls -l,发现权限是 644 而不是 666。这不是 bug,是 umask 在起作用。
umask 是「掩码」,作用是从默认权限里减掉某些位。默认:普通文件 666,目录 777。umask 值里的位会被屏蔽掉:
# 查看当前 umask
umask
# 输出 0022
# 文件:666 去掉 022 → 644
# 目录:777 去掉 022 → 755常见 umask 值:
022:默认,属主可读写,其他人只读不写。适合大多数场景。002:同组可写。多人协作目录常用。077:只有属主有权限,其他所有人无权。安全敏感场景(如 ~/.ssh)用。
如果你的应用创建的文件权限不对(比如 web 程序生成的文件其他进程读不了),先查 umask。对 PHP-FPM,可以在 pool 配置里强制:
; /etc/php/*/fpm/pool.d/www.conf
; 不直接提供 umask 指令,但可以通过 systemd 设置
[Service]
UMask=0022或者对 Nginx/PHP 进程所属的 systemd service 加 UMask=0022 后 daemon-reload 重启。
四、ACL:当三个数字不够用的时候
传统的 owner/group/other 模型有个死结:一个文件只能有一个属主和一个属组。如果我想让 alice 能读写、bob 只读、web 组能读写,三个数字根本表达不了。
POSIX ACL(访问控制列表)解决这个问题,能给任意用户/组单独授权。查看文件的 ACL:
getfacl /var/www/shared/config.php输出会显示基本权限 + 扩展的 ACL 条目:
# file: var/www/shared/config.php
# owner: root
# group: root
user::rw-
user:alice:rw-
user:bob:r--
group::r--
mask::rw-
other::---注意这里的 mask 行 —— 它是 ACL 的「有效权限上限」,会和你常见的 ls -l 里显示的权限位混淆。当你给文件设了 ACL,ls -l 里 group 位置显示的不再是真实的组权限,而是 mask(后面会有一个 + 号提示有 ACL):
-rw-rw----+ 1 root root 1234 config.php
# ↑ 这个加号表示文件有 ACL添加 ACL 条目:
# 给 alice 读写权限
setfacl -m u:alice:rw /var/www/shared/config.php
# 给 web 组只读
setfacl -m g:web:r /var/www/shared/config.php
# 给目录设默认 ACL(新建文件自动继承)
setfacl -d -m u:alice:rw /var/www/shared/
# 删除某条 ACL
setfacl -x u:alice /var/www/shared/config.php最容易踩的坑:mask 会限制 ACL 条目的实际生效权限。 你设了 u:alice:rw,但 mask 是 r--,那 alice 实际只有读权限。解决方法是把 mask 也放开:
setfacl -m mask::rw /var/www/shared/config.php排查 ACL 问题时,永远先 getfacl 看 mask。
五、chattr:藏在权限位下面的「隐藏关卡」
有时候权限明明是 777,root 也改不了、删不掉,报 Operation not permitted(注意不是 Permission denied)。这时候要怀疑文件被设了不可变属性。
查看扩展属性:
lsattr /etc/important.conf输出里如果第一个字母是 i,说明文件被设了 immutable(不可变):
----i---------e---- /etc/important.conf加了 +i 的文件:不能修改、不能删除、不能改名、不能创建硬链接,连 root 都不行(除非先去掉这个属性)。这是防篡改的终极手段,但如果你忘了自己设过,排查时会很奇怪。
chattr -i /etc/important.conf # 去掉不可变属性
chattr +i /etc/important.conf # 重新加上另一个常见属性是 +a(append only,只能追加不能覆盖),日志文件常用。同理,用 chattr -a 解除。
lsattr 报 Permission denied 看不到属性时,说明你连读属性都没权限,先解决基础的读权限。
六、从现象到根因的排查清单
遇到 Permission denied,按这个顺序走:
- 确认是谁在操作:
ps aux | grep 进程名,看是哪个用户/组。Nginx 是www-data,PHP-FPM 也是。别拿 root 的视角去推测 www-data 能不能访问。 - 用 namei 看整条路径:
namei -l /完整/路径,找出哪一级目录缺 x。 - 看目标文件权限:
ls -l,注意那个+号,有就是有 ACL。 - 有 ACL 就 getfacl:重点看 mask 是不是限制住了。
- 权限看着正常却还是不行:
lsattr查 immutable/append-only。 - 新创建的文件权限不对:查 umask,以及父目录有没有默认 ACL。
- SELinux/AppArmor:Debian/Ubuntu 上如果有 AppArmor,
dmesg | grep -i denied能看到被策略拦下的记录 —— 这种报错和文件权限长得一样,但根因完全不同。
七、实战:修一个 Nginx 403
假设访问 https://site.com/uploads/a.jpg 返回 403,而文件确实存在。按清单走:
# 1. Nginx 跑在 www-data
ps aux | grep nginx
# 2. 看路径每一级
namei -l /var/www/site/uploads/a.jpg
# 发现 uploads 是 drwx------ root root —— 问题在这
# 3. 修复:让 www-data 能进入该目录
chown -R www-data:www-data /var/www/site/uploads
chmod 755 /var/www/site/uploads
# 4. 验证
curl -sI https://site.com/uploads/a.jpg | head -1
# HTTP/2 200 ✅整个过程不到一分钟。关键在于用 namei -l 定位到具体哪一级目录,而不是盲目地把权限改成 777。
顺便说一句:永远不要用 chmod -R 777 来「解决」权限问题。它只是掩盖症状,还会引入严重的安全风险(任何用户都能改你的网站文件)。真正理解了 owner/group/other、目录的 x 语义、umask、ACL、chattr 这五层,你就能精确地授予「刚好够用」的权限 —— 既让服务跑起来,也不给攻击者留门。