遇到 Permission denied 时,很多人的第一反应是 chmod 777。它有时确实会让报错暂时消失,却把真正的问题也一起盖住了:究竟是谁在访问,访问路径走到了哪一级,目标是文件还是目录,操作需要哪一组权限,ACL 或挂载策略有没有再加一道门?
Linux 的权限判断不是一张贴在文件上的静态标签。发起操作的是进程,进程带着 UID、GID 和附加组;路径上的每个目录都有自己的 mode;最终对象还可能受 ACL、挂载参数、capabilities 与强制访问控制约束。把这些证据按顺序对齐,权限问题就从“玄学报错”变成了可以逐项验证的判断题。
下面从这一条判断链出发。你会看到普通文件和目录的 r/w/x 为什么完全不同,删除为什么由父目录控制,umask 为什么不是简单的默认权限,setgid 与 sticky 怎样服务共享目录,以及 su、sudo 到底改变了身份、认证和授权中的哪一环。
一次文件操作至少包含五个要素:
/ 或当前目录出发,每一级目录是否允许搜索,也就是是否有目录 x。noexec、nosuid 挂载,capabilities,SELinux/AppArmor,文件属性等都可能改变结果。这五问有一个固定顺序。路径走不通时,目标文件即使是 777 也没有用;删除目录项时,盯着目标文件的写位也会找错对象;ACL 生效时,只看 ls -l 的九位又不够。

图:权限排查不是反复试数字,而是沿系统实际做判断的顺序收集证据。
chmod 777 同时把读、写、执行开放给所有身份。它既不能修复缺少父目录搜索权限、只读挂载或 MAC 拒绝,也常常制造新的写入与执行面。先确定缺的是哪一位,再只改那一位。
内核主要使用数字身份,不依赖人类可读的用户名。一个进程同时可能有多组 UID/GID:
不带用户名运行 id,查看的是当前进程继承的身份向量:
id
id -u
id -ru
id -g
id -Gid -u 默认给出有效 UID,id -ru 给出真实 UID。大多数普通 Shell 中二者相同;在受控身份切换程序里才可能看到差异。给 id 传一个用户名时,它会重新查询账号和组数据库,那是在问“数据库里这个用户应有哪些组”,不等于某个早已登录的进程已经刷新了自己的组向量。
用户有一个主组,也可以属于多个附加组。登录会话建立时,主 GID 与附加组列表被写入进程凭据,子进程继续继承。文件组只要匹配进程的有效 GID或任一附加 GID,就能进入 group 权限类。
例如:
uid=2102(bob) gid=2300(auditors) groups=2300(auditors),2200(team)若文件是 alice:team,bob 虽然主组是 auditors,仍会因为附加组含 team 而匹配文件的 group 类。修改 /etc/group 或运行用户管理命令后,旧会话的进程凭据不会凭空更新;重新建立会话,或由明确的组切换机制启动新进程,才会拿到新组列表。
传统三组 mode 的选择是互斥分支:进程身份匹配文件 owner 时,只看 owner 位;否则,文件组匹配有效组或附加组时,只看 group 位;再否则才看 other。owner 位不足时不会再向 group 或 other 回退。
ls -l 的第一列有十个主字符:第一个是文件类型,后九个依次是 owner、group、other 的 rwx。例如:
-rw-r----- 1 alice team 128 Jul 15 09:18 report.txt这表示普通文件、owner 可读写、group 只读、other 无权限。权限串后面若出现 +,通常提示存在 ACL 等扩展访问方法,应该继续运行 getfacl,不能把九位当成全部规则。
stat 更适合做精确核对:
stat -c 'mode=%A octal=%a owner=%U(%u) group=%G(%g) type=%F' report.txt可能得到:
mode=-rw-r----- octal=640 owner=alice(2101) group=team(2200) type=regular file数字 UID/GID 能排除名称解析歧义,%a 能排除肉眼数错权限位。stat link 默认看符号链接自身,stat -L link 才解引用到目标;诊断链接时应该把两者都看一遍。
访问 /srv/team/project/report.txt 时,进程必须依次搜索 /、/srv、/srv/team 和 /srv/team/project。任何一个中间目录缺少适用于该进程的 x,路径解析都会以 EACCES 失败,目标文件自己的 640 甚至还没有机会被检查。
namei -l /srv/team/project/report.txtnamei -l 会逐级列出类型、mode、owner 和 group,是排查“文件看起来可读却打不开”的高效工具。路径中有符号链接时,它也能展示跳转后重新开始解析的各级目录。配合以下命令可以把链接本体和最终对象分开:
readlink -f path/to/link
stat path/to/link
stat -L path/to/link
图:先证明进程身份,再证明路径和对象;每条命令回答的问题不同。
对普通文件,三个位可以先这样理解:
x 只是执行检查的一关。文件还需要是内核可识别的二进制,或有合法 shebang 的解释器脚本;文件系统的 noexec 也可能阻止直接执行。脚本能否被解释器读取还涉及脚本的读权限和解释器行为,所以“有 x 就一定能跑”并不准确。
目录保存的是名字到对象的映射。它的 r/w/x 语义因此变成:
这解释了一个很实用的组合:目录只有 x 时,无法 ls,但若知道 report.txt 的准确名字且目标文件允许读取,仍可打开它;目录只有 r 时,可能列出 report.txt,却无法继续解析并读取。

图:同一个字母落在不同对象类型上,保护的操作完全不同。
删除和重命名改变的是父目录中的目录项。一个 0400 的只读文件,只要攻击者对父目录有 w+x,仍可能被删除;反过来,即使文件自身可写,父目录不允许修改目录项,也不能靠文件 w 删除它。sticky 位会在共享目录上再收紧这条规则,后面单独说明。
每组 rwx 是三个比特:r=4、w=2、x=1。一位八进制数正好覆盖一组:
chmod 0640 report.txt 把三组完整设为 owner 读写、group 只读、other 无权限。前导 0 在 chmod 命令中不是必需,但写成四位能清楚表明这是八进制 mode;若包含特殊位,则用 4755、2750、1777 等四位主体表示。
八进制适合“我知道最终状态应该是什么”。它会整体覆盖相关位,因此对已有目录树批量使用前要确认不会误删某些对象原有的执行或特殊位。
符号模式由“对象、操作、权限”组成:
u/g/o/a 分别是 owner、group、other、all。+/-/= 分别是添加、移除、精确设为。r/w/x/X/s/t,也可以引用另一组的当前权限。chmod u+x deploy.sh # 只给 owner 增加执行
chmod go-w report.txt # 移除 group 和 other 的写
chmod o= private.txt # other 精确设为空
chmod g=u shared.txt # 把 owner 当前 rwx 复制给 group
chmod u=g shared.txt # 把 group 当前 rwx 复制给 owner
chmod -R a+X public-tree # 目录加 x,普通文件仅在原有执行位时加 xX 是递归场景里很有价值的条件执行位:对象是目录,或普通文件原先已有任一执行位时,它才产生效果。相比 chmod -R a+x,它不会把图片、日志和数据文件全部标成可执行。
省略 u/g/o/a 时,符号模式会受到当前 umask 对类别的限制。为了让脚本意图可读,推荐明确写出对象,例如 a+r、u+x,不要依赖读者猜省略规则。

图:数字模式描述完整结果,符号模式描述相对变化,X 保留文件与目录的语义差别。
创建新对象时,程序会向内核请求一个 mode,umask 再从请求中清除相应位:
最终 mode = 请求 mode & ~umask常见工具创建普通文件时请求 0666,创建目录时请求 0777。普通文件起点没有执行位,是因为“刚写入的一串字节”不应自动变成可执行程序。以 umask 0027 为例:
普通文件:0666 & ~0027 = 0640
目录: 0777 & ~0027 = 0750不要把它机械背成十进制减法。umask 是按位清除;程序若主动只请求 0600,umask 不会给它补成更宽权限。父目录存在 default ACL 时,还会使用 ACL 继承与创建请求共同计算,新对象结果不能只看当前 Shell 的 umask。
umask
umask 027
touch demo-file
mkdir demo-dir
stat -c '%n %a %A' demo-file demo-dirumask 是进程状态,子进程会继承。把它改成 077 不会追溯修改已经存在的 0640 文件,只会影响之后的创建;某些处理秘密数据的程序还会主动采用比 Shell 更严格的请求或掩码。

图:umask 只做减法意义上的“收窄”,不会添加程序没有请求的权限,也不追溯旧对象。
chmod 改权限位,chown 改 owner 或同时改 group,chgrp 只改 group:
chown alice report.txt
chown alice:team report.txt
chgrp team report.txt改变 owner 通常需要 CAP_CHOWN;普通文件 owner 可以把 group 改成自己所属的有效组或附加组,不能随意转给任意组。改变可执行文件的 owner/group 还可能清除 setuid/setgid,避免把旧的身份提升语义意外带给新 owner。
符号链接要特别谨慎。chown link 通常跟随链接修改目标,chown -h link 才尝试改链接本身。对可能被其他进程改动的不可信目录树使用递归和跟随链接选项,可能出现检查与使用之间的竞态;先限定根目录、检查链接,再决定是否递归。
部署文件时,install 可以一次性复制并设定属性,减少“复制完成后短暂暴露错误权限”的窗口:
install -o root -g app -m 0640 app.conf /etc/myapp/app.conf
install -d -o root -g app -m 0750 /var/lib/myappowner 是谁、group 与 mode 是什么,应该在部署定义中一起写清。分散的 cp、chown、chmod 不仅难审查,中途失败时还可能留下只完成一半的状态。
普通可执行文件运行时,进程通常保持调用者的身份。二进制可执行文件设置 setuid 后,成功执行可能把有效 UID 设为文件 owner;setgid 类似地改变有效 GID。ls -l 会用 s 表示“特殊位和对应 x 同时存在”,用大写 S 表示“特殊位存在但对应 x 不存在”。
chmod u+s program
chmod g+s program
chmod 4755 program这不是通用的“给脚本提权”办法。Linux 忽略解释器脚本上的 setuid/setgid,实验中 4755 shell script 仍以调用者的真实和有效 UID运行。即便是二进制,底层文件系统使用 nosuid、进程设置 no_new_privs、被跟踪等条件也会让身份转换失效。
setuid 程序一旦有输入校验、路径搜索、环境处理或竞态漏洞,缺陷会在获得的身份下放大。现代服务更倾向把权限拆成较小的 capability、由服务管理器设定身份,或通过狭窄的 sudo 规则暴露必要动作。
setgid 放在目录上时,重点不再是执行身份,而是协作继承:新建文件和目录优先继承父目录的 group,新子目录通常继续带 setgid。这样团队目录不会因为创建者主组不同而逐渐混入多种 group。
chgrp team /srv/project
chmod 2770 /srv/project2770 表示 setgid 加上 owner/group 的 rwx。它只解决“新对象属于哪个组”,不会自动保证组可写:文件最终 mode 仍受创建请求、umask 与 default ACL 影响。若团队成员要互相编辑,常见做法是搭配 umask 0002 或更明确的 default ACL,并在沙盒里验证新文件和新目录的实际结果。
/tmp 常见 mode 是 1777:所有人都能创建目录项,但 sticky 限制删除和重命名。通常只有目标文件 owner、目录 owner 或具备相应权限的进程能删除该目录项。
chmod 1777 shared-dropbox
ls -ld shared-dropbox
drwxrwxrwt ... shared-dropbox末尾小写 t 表示 sticky 与 other 的 x 同时存在;大写 T 表示 sticky 存在但 other 没有 x。sticky 不会阻止别人读取一个公开可读文件,也不会给文件内容增加写保护,它只收紧共享可写目录中的删除和重命名规则。

图:三个特殊位不能只背 4、2、1,还要把它们放回可执行文件或目录的具体语义。
owner/group/other 不够表达“只给 bob 额外读写,但不让整个 team 写”时,可以使用 POSIX ACL:
setfacl -m u:bob:rw report.txt
getfacl report.txt典型输出可能是:
user::rw-
user:bob:rw- #effective:r--
group::r--
mask::r--
other::---user:bob:rw- 是条目声明,mask::r-- 是命名 user、文件所属组和命名 group 能获得的最大权限。bob 的有效权限是二者交集,所以这里只读。文件 owner 的 user:: 和 other:: 不受 ACL mask 限制。
ACL 的选择顺序仍然是分支:先匹配文件 owner;再匹配命名 user;再汇总所有匹配的文件组或命名 group,并与 mask 相交;最后才使用 other。ls -l 中 group 三位在扩展 ACL 存在时常对应 mask,因此仅凭 ls 不能看出是哪条命名规则被收窄。

图:条目写着 rw 不代表实际 rw,mask 是命名 user/group 的共同上限。
目录可以有 default ACL:
setfacl -m d:u:bob:rwX,d:m:rwx project
getfacl project它不直接授权访问目录自身,而是在之后创建子项时复制成新对象的 access ACL。创建调用请求的 mode 仍是上限:普通文件即使从 default 条目继承了 rwx,若程序只请求 0666,执行位也会被清除;目录请求 0777 时则可能保留 x。
default ACL 不追溯已经存在的文件。设置完成后要分别创建一个文件和一个子目录,用 getfacl、stat 观察继承结果,而不是从父目录配置推断全部子项已经变化。
传统 mode 属于自主访问控制的一层。下面任一条件都可能让“九位看着没问题”却仍失败:
CAP_DAC_OVERRIDE、CAP_DAC_READ_SEARCH、CAP_CHOWN 等单元。UID 0 在容器、用户命名空间或收窄后的 capability 集合中未必拥有所需单元。noexec 限制直接执行,nosuid 让 setuid/setgid 与文件 capabilities 不产生提权效果。可按环境选择证据命令:
findmnt -no TARGET,OPTIONS --target path/to/object
getfacl path/to/object
getcap path/to/program
lsattr path/to/object
ls -Z path/to/object # 仅在支持安全上下文的环境中有意义root 也不能被描述成“无视一切”。传统超级用户通常能绕过很多自主访问检查,但执行一个所有执行位都关闭的普通文件仍有额外限制;只读文件系统、缺少 capability、MAC、命名空间和硬件/远端策略都可能继续约束它。准确说法是“检查当前进程实际拥有的身份与能力”,而不是看到 uid=0 就停止分析。
su user 运行目标 UID/GID 的 Shell,但为了兼容通常保留较多调用方环境和当前目录;su - user 或 su --login user 会更接近一次登录:清理大部分环境,初始化 HOME、SHELL、USER、LOGNAME、PATH,并切到目标主目录。
su alice
su - alice
su - alice -c 'id; pwd; printf "%s\n" "$HOME"'认证由 PAM 和系统配置参与。普通场景中 su 常要求目标账号的凭据,但不能把这一点写成跨所有系统的绝对规则;root 调用、PAM 策略、账号状态都会改变结果。更重要的是,su 一旦给出交互 Shell,后续每条命令不再逐条经过 sudo 策略匹配,审计粒度与授权范围都更粗。
sudo 前端会把调用者、目标身份、主机、命令与参数交给策略插件。常见 sudoers 部署会认证调用者并在短时间缓存凭据,但 NOPASSWD、targetpw、PAM 和其他策略都能改变认证方式,因此要查看具体规则:
sudo -l
sudo -ll
sudo -n /usr/bin/id
sudo -ksudo -l 列出授权,-n 禁止交互提示,适合自动化在需要密码时立即失败,sudo -k 使当前缓存凭据失效。编辑规则使用 visudo 或 visudo -f /etc/sudoers.d/name,先做语法检查再生效。
一个实验性的最小规则可以只允许 bob 以 root 运行 /usr/bin/id:
bob ALL=(root) NOPASSWD: /usr/bin/id这不允许 bob 读取 /etc/shadow,也不允许运行 /usr/bin/env。生产规则还要评估参数匹配、被授权程序是否有 Shell escape、插件加载或任意文件写入能力。允许编辑器、解释器、包管理器或用户可改写目录里的程序,常常等同于允许一条绕出命令白名单的路径。
sudo 可记录成功与失败尝试,策略也能配置输入/输出日志;但 I/O 记录不是“安装 sudo 就自动拥有”的保证,日志位置和保留方式也由系统日志与插件配置决定。审计设计必须写清记录什么、保存多久、谁能读、如何防篡改。

图:su 更像进入另一个身份会话,sudo 更适合把必要动作限定在一条策略边界内。
下面的实操只在一次性 Debian 容器中创建临时用户、组和测试对象。这样做的原因不是“为了演示几条命令”,而是要让 owner、文件组、附加组、ACL mask 与 sticky 的判断条件彼此独立,同时避免改动日常账号和目录。容器退出后由 --rm 删除;脚本末尾仍显式清理实验目录并复查。
复制并运行以下命令。它安装最小 ACL 工具,创建 alice、bob、carol 和 team/auditors 两个组;bob 的主组是 auditors,附加组含 team,因此能验证“附加组也参与文件组匹配”。
docker run --rm -i --name welearn-linux-ch09-practice \
debian:bookworm-slim bash <<'LAB'
set -u
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq acl util-linux >/dev/null
LAB=/tmp/welearn-ch09
rm -rf "$LAB"
mkdir -p "$LAB"
groupadd -g 2200 team
groupadd -g 2300 auditors
useradd -m -u 2101 -g team -G auditors -s /bin/bash alice
useradd -m -u 2102 -g auditors -G team -s /bin/bash bob
若环境无法联网拉取 acl 包,脚本会在安装阶段停止,不应把后续缺少 setfacl 误判为文件系统不支持 ACL。若已有同名容器,先确认没有正在进行的练习,再删除那个已停止的练习容器;不要去掉 --rm 后把临时账号环境长期留着。
不要从“应该可以”开始,按证据推进:
access() 风格的预检查再使用,因为两步间对象可能被替换。id,必要时看目标进程的 /proc/PID/status 中 Uid、Gid、Groups、CapEff。namei -l path 找到第一个缺少目录 x 的组件,同时注意符号链接目标。stat 获取类型、数值 mode、UID/GID;getfacl 检查命名条目、mask 与 default ACL。w,删除/重命名看父目录 w+x,sticky 再检查 owner。findmnt、getcap、MAC 日志、文件属性和应用沙盒。stat/getfacl 证据。常见误判可以压缩成一张对照表:
权限的目标不是“越小越安全”这句口号,而是让每个进程刚好能完成被授权的动作,同时让配置可复现、可解释:
install -m -o -g 或配置管理同时声明 owner、group、mode。find、stat、getfacl 审查世界可写、意外 setuid/setgid 与超出预期的 ACL,不要在不了解目录边界时直接递归修正。继续查证时,优先使用这些规范和官方说明:
最后给自己留一条可执行的验收标准:能用 id 说清主体,用 namei 说清路径,用 stat/getfacl 说清对象,用一次实际操作和退出码证明结果,再说明为什么只改了那几位。做到这一步,权限不再是一串需要背诵的数字,而是一套可以复查的授权决策。
先看身份与普通文件。预期 bob 读取成功并返回 0,因为附加组 team 命中 group 的 r--;carol 返回非零并显示 Permission denied,因为她不是 owner,也不属于 team,只能使用 other 的 ---。namei -l 同时证明路径上的目录都有 bob 可用的 x。
再看目录矩阵。dir-x 的 ls 失败,但读取已知 known.txt 成功;dir-r 能输出名字,随后 cat 因缺少搜索位失败;dir-wx 中创建和删除已知名字成功。这里的失败是实验目标,函数打印退出码后继续运行。
对比 0777 与 1777 父目录。bob 能删掉 open 中 alice 的 0400 文件,证明删除不看目标写位;在 sticky 中删除 alice 文件返回 Operation not permitted,alice 自己删除成功。
最后核对创建规则。umask 0027 应得到文件 640、目录 750;setgid 目录里 bob 创建的文件 group 为 team,新子目录还显示 2775。ACL 第一次给 bob 的条目是 rw、mask 是 r,写入失败;mask 改成 rw 后写入成功。default ACL 只出现在之后创建的对象。脚本删除 /tmp/welearn-ch09,输出 LAB_STATUS=PASS,容器再由 --rm 移除。