1. 从一个改不动的 Android 目录说起:chmod 的边界到底在哪
我最早对 chmod 产生"这东西没那么简单"的认知,不是在生产服务器上,而是在一台安卓手机上。当时的需求很朴素:某个第三方应用的Android/data目录里塞了一堆导出文件,我想用终端把它整个目录的权限放开,让另一个工具能读写。命令敲下去,返回的是这么一行:
unable to chmod '/storage/emulated/0/Android/data/xxx': Operation not permitted我第一反应是权限不够,于是前面加了su,还是不行。换了个文件管理器,报错几乎一模一样。等我回到电脑上,对着一个普通 ext4 分区里的目录敲chmod 777,一秒生效。同一个命令,两种命运,这就说明chmod 能不能起作用,本身是有前提条件的,而不是"命令敲对了就一定成"。
这篇内容我打算把 chmod 从头到尾捋一遍。不是照着手册念参数表,而是按一个实际做项目的人的思路走:它到底改了什么、数字和字母两种写法各自的地雷在哪、什么时候改完没效果、改错了怎么救回来、以及它搞不定的场景该用什么替代。适合已经会敲chmod 755但经常被"权限明明给了还是不行"卡住的读者,也适合把 chmod 当成万能钥匙、习惯性chmod -R 777的朋友——我自己也曾是后者。
先说结论性的判断,后面再逐条展开:**chmod 修改的是文件 inode 上那 12 个权限位(加上 setuid/setgid/sticky 三位共 12 个可见位),它不改变文件属主,不改变 SELinux 上下文,不改变挂载选项,也不改变文件系统是否支持 POSIX 权限。**这四个"不改变",就是绝大多数"改了没用"的根源。
1.1 三个身份乘以三个动作,先把权限位的基本盘摆清楚
Linux 的权限模型其实很简单,就是"三类人 × 三种动作"的九宫格。三类人分别是:文件属主(u,user/owner)、属组(g,group)、其他人(o,other)。三种动作是读(r,read)、写(w,write)、执行(x,execute)。九宫格里每个格子填一位,就得到了ls -l里那串让人眼熟的rwxr-xr-x。
这里有个非常关键、但很多用了几年 Linux 的人都说不清的规则:**权限判定是"短路匹配",只认第一个命中的身份。**假设一个文件的属主是 alice、属组是 devteam,你现在用 bob 这个账号去访问,而 bob 恰好也在 devteam 组里,那么系统匹配到的是"属组"这一档,用的是 g 的三位,而不是 o 的三位。反过来说,如果 alice 自己访问,哪怕 g 和 o 全是---,她也照样能读——因为属主位是rwx。
这个规则带来的一个常见困惑是:用户明明在组里,为什么还是没权限?答案通常是用户没重新登录。组的成员关系是在登录会话建立时确定的,/etc/group改完之后不重新登录(或者不执行newgrp),当前 shell 里id命令看到的组列表还是旧的,权限判定自然按旧身份走。我在这上面浪费过整整一个下午,最后发现只需要退出重登。
另外还有一个必须分清的点:**目录上的 r、w、x 和文件上的含义不一样。**文件上的 r 是读内容、w 是改内容、x 是当程序运行;目录上的 r 是"能列出里面有哪些名字"(ls),w 是"能在里面创建、删除、重命名条目",x 是"能进入这个目录、能访问里面已知名字的条目的 inode 信息"。所以你会看到一个诡异现象:某个目录权限是rw-r--r--,ls能列出来,但你cd不进去,cat里面的文件也全报 Permission denied——因为缺 x。这个坑在批量改权限时极其常见。
1.2 数字权限的算法与最容易记反的那几个值
chmod 的数字写法是很多人入门时背下来的口诀:r=4,w=2,x=1,三档相加。所以:
| 数字 | 权限串 | 典型用途 |
|---|---|---|
| 644 | rw-r--r-- | 普通文本、配置文件、网页静态资源 |
| 600 | rw------- | 私钥、含密码的配置、token 文件 |
| 755 | rwxr-xr-x | 可执行程序、脚本、目录默认值 |
| 700 | rwx------ | 用户私有目录、.ssh目录 |
| 664 | rw-rw-r-- | 团队共享的可写文件 |
| 775 | rwxrwxr-x | 团队共享的可写目录 |
| 777 | rwxrwxrwx | 谁都能改,慎用 |
| 1777 | rwxrwxrwt | 加粘滞位,如/tmp |
| 2755 | rwxr-sr-x | 目录加 setgid,新建文件继承属组 |
| 4755 | rwsr-xr-x | 可执行文件加 setuid |
前导那一位(1/2/4)是特殊位,很多人算权限的时候会漏掉,或者把它和后面的三位混淆。1777里的 1 表示粘滞位(sticky bit),它的作用是:在这样一个目录里,用户只能删除或重命名自己拥有的文件,哪怕这个目录是全员可写的。/tmp就是标准配置,没有粘滞位的话,任何用户都能删掉别人放在/tmp里的东西。
有个细节值得专门说:**写脚本或者做自动化时,别用八进制字符串拼接的方式去算权限。**我见过有人写出这种代码:
perm=$(printf "%o" $((6*64 + 4*8 + 4))) chmod $perm file能跑,但一旦将来要加特殊位就会算错,而且可读性极差。更稳的写法就是直接写常量chmod 644,需要动态计算时也建议明确拆开"特殊位 | 属主 | 属组 | 其他"四段,而不是想当然地拼字符串。另外,chmod对八进制参数是宽容的,chmod 0644和chmod 644等价,但前导 0 在脚本里更安全,因为不会有人误读成十进制。
1.3 chmod 改的是 inode 上的标记,不是别的东西
回到开头那个"改不动"的场景。要说清为什么会有改不动这回事,得先明确 chmod 到底动了什么。
chmod调用的系统调用是chmod(2)(对符号链接还有lchmod/fchmodat),它修改的是目标文件 inode 中的i_mode字段的低 12 位。就这么一个动作。它不碰:
- 文件的属主和属组——那是
chown/chgrp的事,而且普通用户改属主要CAP_CHOWN能力,所以基本都得sudo; - 文件的扩展属性里的 ACL——ACL 存在
system.posix_acl_access这个扩展属性里,chmod会修改 ACL 的 mask 部分,但不会删除已有的 ACL 条目,这一点非常容易被误解,后面第 6 章会专门讲; - SELinux 上下文——存在
security.selinux扩展属性里,chmod完全不管它。所以在一个开着 SELinux 的 enforcing 系统上,你把权限放开了,访问照样可能被拒绝,因为拒绝的理由根本不是传统权限; - 挂载选项——
noexec、nosuid、ro这些是挂载时决定的,属于文件系统层面的策略,比文件权限位的优先级更高。
还有一个反直觉的行为:**chmod 会跟随符号链接。**你chmod 755 link的时候,改的是链接指向的那个目标文件,链接本身在 Linux 上权限永远是lrwxrwxrwx,而且不可修改。如果你确实需要"改链接本身"这个操作(实际上没多大意义),GNU coreutils 提供了chmod -h,但在大部分场景下这不是你想要的。很多人以为改了链接就能改目标,或者反过来以为改不了,都是没意识到这个跟随规则。
理清这一层之后,"改不动"就不再神秘了。它一定是下面这四种情况之一:你没有权限去改这个 inode(EPERM/EACCES)、这个文件系统根本不支持 POSIX 权限位(返回 ENOTSUP,或者干脆静默无效)、这个文件系统被只读挂载了(EROFS),或者你在安卓这类沙箱化系统里,路径根本就不是真实的文件系统路径。第 4 章会把这条排查链路完整走一遍。
2. 我把项目里的 777 全清了:最小授权的落地方法
先说个我自己的黑历史。早年接一个内部管理系统,前端资源目录老是白屏,排查几次都是权限问题,后来我图省事直接对整个 web 根目录来了个chmod -R 777,白屏瞬间消失,我当时还觉得挺得意。直到某次例行检查,发现uploads/目录下面多出来一个.wp 结尾的小文件,打开一看是一段 PHP 代码。攻击链简单到可笑:上传接口允许上传任意扩展名 → 目录 777 全员可写可执行 → 上传的文件被 Web 服务当脚本执行了。
那之后我花了小半天时间,把所有项目的权限按角色重新梳了一遍。这个过程本身不复杂,但思路值得完整讲一遍,因为它决定了你是"每次出事都靠删文件救火"还是"从设计上不给机会"。
2.1 777 究竟放开了哪三扇门
chmod 777展开是rwxrwxrwx,对目录和文件的效果要分开看:
对目录来说,它意味着任何人(包括 Web 服务之外的任意本地用户,以及所有能触达该路径的进程)都可以列出内容、在其中创建和删除条目、进入其中访问文件。注意"删除"这一条非常致命——只要目录可写,目录里任何文件都能被删掉或替换,哪怕那个文件自己是 644、属主是 root。这是很多人想不通的一点:**文件的删除权由目录的 w 位决定,不由文件自身的权限决定。**所以你在一个 777 目录里把一个配置文件设成 600,一点用都没有,别人直接删掉重写一个就行。
对文件来说,它意味着任何人可读可写可执行。对脚本和二进制来说,"可执行"这一条就是前面那个事故的直接成因——Web 服务配置了在这个目录里执行脚本,而目录里恰好能被外人放东西进去,这两件事凑一起就是一条完整的利用链。
顺带一提,"其他人可写"还有一个副作用容易被忽略:某些程序出于安全考虑会拒绝加载全员可写的配置或密钥。最典型的是 SSH——~/.ssh/id_rsa一旦变成 644 或 777,ssh直接报UNPROTECTED PRIVATE KEY FILE拒绝使用;/etc/ssh/sshd_config如果其他人可写,sshd也会拒绝启动。也就是说 777 不只是"不安全",它还会直接把一堆工具搞罢工。
2.2 网页目录、共享目录、临时目录该怎么分
按"谁需要什么"来分,比按"改到能用就行"要可靠得多。我常用的分配逻辑是这样的:
Web 根目录(如/var/www/site)设 755,属主和属组统一到运行 Web 服务的账号(比如www-data)。静态资源和 PHP 脚本都用 644,因为解析器读文件靠的是读权限,不需要执行位。只有真正需要被当作程序执行的脚本才给 755。这里有个小陷阱:如果你用的是php-fpm加nginx的组合,PHP 文件其实不需要执行位,但很多模板和部署脚本会习惯性给 755,不算错,只是没必要。
上传目录(如uploads/)设 755 且属主为 Web 账号,或者更严格一点 750。绝对不要给它执行位。如果 nginx 配置里对这个路径做了 PHP 解析(很多老配置的location ~ \.php$是全站生效的),一定要显式拒绝,否则允许别人往这里写文件就等于允许别人往这里写代码。
需要 Web 账号写入的目录(缓存、日志、会话)设 775,属组统一,然后用setgid 位保证新建的文件继承同一个属组——这一招比事后chown -R省事一百倍,具体配方在第 6 章。这些目录同样不需要执行位,755/775 里的那个 x 是"目录可进入"的意思,不是"里面文件可执行"。
/tmp用系统默认的 1777 就行,粘滞位已经挡住了互相删除的问题。但要注意,1777 的/tmp里依然有可能被塞进恶意的临时文件或符号链接,所以涉及安全敏感的临时文件,尤其是mktemp之外自己拼路径的,得换成带随机后缀的私有目录。
顺便提一句,在国产 Linux 发行版(比如银河麒麟桌面版)上 chmod 的语法和通用 Linux 完全一致,但因为预置的安全策略和桌面环境对用户目录的处理不同,某些系统目录改了以后效果可能和你在 Debian 上试出来的不一样。遇到这种情况别急着怀疑命令,先用mount | grep 目标路径和lsattr看一眼有没有策略层面的东西在起作用。
2.3 什么场景下 777 真的是唯一解
我不想把 777 一棍子打死,有一些场景确实绕不过去,但处理方式要讲究。
第一种是临时排障。线上出问题、需要快速验证"是不是权限问题"的时候,先chmod 777跑一遍确认现象消失,这本身是合理的诊断手段。但验证完必须收回,而且最好改之前记一下原始权限。我的习惯是改之前先stat -c '%a %U:%G %n' 目标存一份到临时文件,改完对照着还原。养成这个习惯之后,我至少两次靠它避免了半夜翻部署文档找默认值。
第二种是无属主概念的共享文件系统。一些挂载进来的外部存储、某些容器共享卷、开发机上团队共用的 scratch 目录,本身就没有严格的属主管理需求,777 或者 1777 是务实的做法。判断标准很简单:这个目录里会不会被执行程序?会不会被当作配置加载?如果两个都是否,风险就可控。
第三种是老旧设备和特殊应用。我遇到过某些嵌入式设备上的采集程序,硬编码检查某个目录是否可写,权限不对就直接退出,而它又以 nobody 身份运行。这种时候没办法谈最小授权,只能给它 777,但要做三件事补偿:把这个目录单独挂载出来(限制爆炸半径)、加noexec挂载选项(挡住执行)、定期审计里面的文件变化。
有个原则我自己一直遵守:777 的目录不应该出现在任何自动部署的脚本里。可以手工临时开,但不能成为流程的一部分——因为没人会回来收。
3. 递归与符号模式:chmod 里最容易把人送进坑的两个参数
如果要说 chmod 里哪两个东西最容易出事,我的答案是-R和符号模式(就是u+x、go-w那种写法)。前者的问题在于范围失控,后者的问题在于语义容易记反。
3.1 -R 在不同类型节点上的行为差异
chmod -R 755 dir看起来干净利落,但它做的事是"把 dir 下所有节点,包括子目录、子文件、符号链接、特殊文件,统统改成 755"。这里面的问题有:
**第一,文件和目录需要的权限本来就不一样。**目录要 755(必须有 x),普通数据文件要 644(不该有 x)。你-R 755一下,所有数据文件都获得了执行位。虽然多数情况下不至于立刻出事,但从安全审计的角度,一堆-rwxr-xr-x的日志文件就是明显的异常。更麻烦的是某些程序会拒绝加载"可执行"的配置文件,理由是可能被篡改。
第二,符号链接的处理方式。前面说过chmod默认跟随链接。-R的时候它会递归进每一个子目录,也会跟随每一个链接去改目标。如果你的目录里有一个指向/etc或者上一层级的软链,一次-R就可能把范围扩到你想不到的地方。这种事故我见过一次:某人在一个包含软链的项目目录里执行递归改权限,结果改到了宿主机上共享的基础库目录,最后靠重装包才恢复。所以递归改权限之前,先跑一遍find 目标 -type l看看有没有软链,这个前置检查只要十秒钟。
第三,目录缺 x 会让递归卡在半路。如果某个中间目录本身权限有问题(比如 700 且属主不是你),-R会在这个节点上报错并跳过,但已经改过的部分不会回滚。所以你会得到一个改了一半的目录树。批量操作遇到报错时,先别急着加--silent或者2>/dev/null把错误吞掉,那些被跳过的节点恰恰是最需要关注的。
3.2 大写 X 和 --reference 的组合拳
针对"文件和目录要分别处理"这个矛盾,chmod 提供了一个特别实用但知名度不高的写法:大写 X。
规则是:X只在目标本身是目录,或者目标文件已经有任意执行位的时候,才会加上执行权限;否则不加。这一条几乎就是为批量操作设计的。所以:
# 目录全给 755 语义,文件只给读写 chmod -R u=rwX,go=rX /var/www/site这一条命令等价于"目录变成 755、文件变成 644(如果原本没有执行位)",一条顶两条,而且不会有数据文件被误加执行位。我现在的部署脚本里处理静态资源,基本都用这个写法,比先find -type d -exec chmod再find -type f -exec chmod两遍要利索,也不容易因为顺序写反而出错。
另一个利器是--reference:
chmod --reference=/var/www/site/public/index.php /var/www/site/public/newfile.php它会直接复制参考文件的所有权限位(包括特殊位)。这在"同目录下新建文件要和旁边文件保持一致"的场景里非常省事,尤其是当你不确定旁边那个文件到底该是多少的时候——用现成的正确样本当模板,比自己去推数字更不容易出错。同理还有--reference用于 ACL 的 cp 选项,属于另一条路子。
3.3 符号模式里那个会清位的等号
符号模式的基本语法是[ugoa][+-=][rwxXst],其中+是加、-是减、=是赋值。问题就出在这个=上。
chmod go=r file的意思是"把属组和其他人的权限设置为r",也就是把原本的 w 和 x 全部清掉。这符合语义,但很多人写的时候心里想的是"给 go 加个 r",结果写成了=。我就干过这事:一个 750 的可执行脚本,我顺手写了个chmod go=r,脚本直接变成 740,别的账号跑不了,排查了半天。
还有一个更隐蔽的点:**不写[ugoa]时,chmod 默认按a处理,但受 umask 影响。**这是 GNU chmod 的特性:chmod +x file等价于chmod a+x file再减去 umask 的位。也就是说,如果你的 umask 是 022,chmod +x file实际得到的是u+x,g+x,o-x——其他人不会被加上执行位。而chmod a+x file会无视 umask,真的给所有人加。这两者在 umask 非 000 的系统上结果不一样,是脚本里一个很难复现的坑。我现在的建议是:写脚本一律显式写全u、g、o、a,不要依赖省略写法。
符号模式在"只调一两位、不想重算整个八进制数"的场景下确实更好用,比如只知道要给属组加写权限、其他位保持不动,chmod g+w比先查当前值再算775要安全。工具没有优劣,只有用对没用的区别。
4. unable to chmod 的完整排查链路:从错误码到挂载点
回到开头那个安卓目录的报错。这条错误信息其实给的信息很少,Operation not permitted对应的是EPERM,它和EACCES(Permission denied)不是一回事,区分这两个是排查的第一步。下面按我实际的排查顺序讲一遍,从简单的到复杂的。
4.1 先分清 EPERM 和 EACCES
很多人把这两个当同义词,但在内核层面它们的含义有明确区别:
| 错误 | 典型触发条件 | 排查方向 |
|---|---|---|
| EACCES | 调用者经过传统权限判定后被拒绝 | 你是属主/属组/其他人哪一档?缺 r、w 还是 x? |
| EPERM | 权限判定之外的限制阻止了操作 | 文件系统只读、不可变属性、特殊文件系统、缺少所需能力 |
| EROFS | 文件系统以只读方式挂载 | `mount |
| ENOTSUP / EOPNOTSUPP | 该文件系统不支持这个操作 | FAT/exFAT 家族、部分网络文件系统 |
| ENOENT | 路径不存在(安卓上很常见,被沙箱隐藏了) | 路径拼写、是否有权限列出父目录 |
关键在于:**EACCES 说明"你的身份不够",加 sudo 或者改属主可能有用;EPERM 说明"这事在这个文件系统上根本不被允许",加 sudo 完全没用。**回到安卓那个例子,报的是 EPERM,这就直接排除了"权限不够"这条路——你就算用 root,在非 root 的 shell 里也改不动,因为那条路径压根不是真实 ext4 上的 inode。
判断方法很简单,先看挂载:
mount | grep -E ' /storage| /sdcard| /mnt' # 或者看更完整的挂载信息 findmnt -T /path/to/targetfindmnt -T会告诉你目标路径所属的挂载点和文件系统类型,这一条命令能省掉大量猜测。
4.2 只读挂载、FAT 家族与 noexec
拿到文件系统类型之后,对照检查三件事。
**第一,是不是只读挂载。**系统进入保护模式、磁盘出错后内核会把文件系统重挂为只读;容器里挂进来的 volume 也可能是 ro。这种情况下的报错通常是 EROFS,mount输出里那个ro就是答案。要改的话,需要先确认只读的原因——如果是内核因为检测到 I/O 错误而被迫转只读,强行重挂可能会造成更严重的数据损坏,dmesg里会有相关记录,必须查。
第二,是不是不支持 POSIX 权限的文件系统。FAT32 和 exFAT 从设计上就没有权限位这个概念,所有文件的权限由挂载参数统一决定(uid、gid、fmask、dmask)。在这种文件系统上执行 chmod,有的实现会返回 ENOTSUP,有的会静默成功但毫无效果——后者更坑,因为脚本不报错,你以为改好了,实际什么都没发生。遇到这种文件系统,正确的做法是改挂载参数而不是改权限:
# 卸载后以指定属主和掩码重新挂载 sudo umount /mnt/usb sudo mount -o uid=1000,gid=1000,fmask=0133,dmask=0022 /dev/sdb1 /mnt/usbNTFS 通过ntfs-3g挂载的情况介于两者之间,它有一套权限映射机制,某些配置下 chmod 看起来是生效的(它会把结果存在映射文件里),但和目标机行为不完全一致,跨机拷文件时容易出问题。
**第三,挂载选项里的 noexec 和 nosuid。**这两个不影响 chmod 本身能不能执行,但会让"权限改了、脚本还是跑不起来"的现象出现。noexec挂载的分区上,任何文件都不能被执行,哪怕权限是 755。所以当你遇到"权限没问题但执行报 Permission denied"时,除了看挂载选项,也要想想是不是这条。nosuid则是让 setuid 位失效——一个挂了nosuid的分区上,chmod 4755改出来的 setuid 程序不会获得提权效果,这一点在排查"为什么我的提权程序没用"时经常被漏掉。
4.3 SELinux 上下文与不可变属性这两道暗闸
如果文件系统是 ext4、挂载正常、权限位也对,但访问还是被拒绝,那就要考虑两道更隐蔽的闸。
SELinux(或同类强制访问控制机制)。它给每个文件打一个上下文标签(ls -Z可以看),进程也有自己的标签,能不能访问取决于策略而不是传统权限位。典型症状是:ls -l显示权限完全正确,但访问报 Permission denied,/var/log/audit/audit.log里有 AVC 拒绝记录。这时候 chmod 解决不了任何问题,需要的是:
# 查看上下文 ls -Z /path/to/file # 恢复默认上下文(最常用、最安全) sudo restorecon -Rv /path/to/dir # 临时关闭 enforcing 验证问题(仅用于确认,不要长期用) sudo setenforce 0我的经验是,遇到"权限对但访问不了",先跑restorecon -Rv再说——它按策略配置恢复默认标签,绝大多数因为拷贝文件、解压归档导致的上下文错乱都能一次修好。手动chcon只适合临时验证,因为手工设的标签在下次 relabel 时会被冲掉。
另一个是不可变属性。chattr +i之后,文件连 root 都不能改、不能删、不能改权限,chmod 会返回 EPERM 而且不说原因。用lsattr一看就明白了:
lsattr /path/to/file # ----i---------e---- /path/to/file <- 那个 i 就是问题所在 sudo chattr -i /path/to/filechattr +i常见的合法用途是保护关键配置文件不被误改,但它也会成为一个"事后完全想不起来"的坑。排查权限类问题时,lsattr应该和ls -l、ls -Z并列为三步走。
4.4 Android 11 之后的 Android/data:为什么 shell 也改不了
单独把这条拎出来说,因为它是搜索量最大的一类困惑,也最能说明"chmod 的能力边界"。
安卓上的/sdcard(也就是/storage/emulated/0)不是真实文件系统路径,而是一个通过 FUSE 暴露出来的虚拟视图。真实数据在/data/media/0下面,权限属于media_rw这类特殊身份。你通过 FUSE 看到的那些文件,权限位是由 FUSE 守护进程统一伪造出来的,通常是 660 或 770 这种固定值。你执行 chmod,请求发给 FUSE,FUSE 一看这不是它能处理的操作,直接返回 EPERM。这就是为什么报错文案一模一样,无论你加不加 sudo。
而Android/data和Android/obb这两个目录更特殊。从 Android 11 开始,系统对它们做了访问限制:普通应用通过文件选择器看不进去,adb shell下的 shell 用户也访问不了(因为 shell 不在被授权的应用列表里)。这时候你看到的甚至可能不是"权限不足",而是目录看起来是空的,或者路径直接不存在(ENOENT)。
在这个前提下,chmod 完全帮不上忙。要操作这些文件,实际可行的路子有这么几条:
- 通过应用自己导出。正经的应用会在设置里提供"导出到公共目录"或者"分享"功能,这是最省事也最合规的做法。文件会落到
Download或Documents这类公共目录,权限正常,工具也能读。 - 用 SAF(存储访问框架)。在应用里通过系统的目录选择器拿到某个目录的持久化访问授权,之后应用就能读写这个目录,不需要任何权限位。这是 Android 官方设计的正路,能绕过分区存储的限制。
- 通过 adb 拉到电脑上再处理。
adb pull对受限制的目录同样可能失败,但对应用私有目录(/data/data/<包名>/files)在 debug 构建上是可以的,因为 shell 和应用的 uid 之间有 adb 的调试授权。 - 应用私有目录的权限是设计好的。
/data/data/<包名>默认 700、属主是应用自己的 uid,这是有意为之的隔离机制,不是 bug,不该去改。想在应用间共享数据,用ContentProvider或FileProvider,别想着改权限。
一句话总结安卓这部分:当 chmod 返回 EPERM 且路径在/storage或/sdcard下,就别再纠缠 chmod 了,换成 SAF、MediaStore 或者导出到公共目录。
5. 权限改坏之后的救援:从 /etc 到 ~/.ssh 的抢救顺序
比起"改不动",更让人手心冒汗的是"改过头"。chmod -R敲错一个路径,或者复制粘贴时多了个斜杠,后果可能是整台机器登不进去。这一章把几种典型误操作和救援路径讲清楚,希望用不上,但真遇到了不至于慌。
5.1 三种典型误操作及其破坏面
**第一种,chmod -R 777 /或者chmod -R 777 /etc。**破坏面最广。权限位被全部打开之后,会有连锁反应:sudo会因为检测到自己的配置文件或二进制被改而拒绝运行(报 setuid 相关的错),ssh会拒绝使用所有私钥,sshd会拒绝启动,/etc/shadow变成全员可读意味着所有密码哈希暴露。这台机器在权限意义上已经不可信了。
**第二种,chmod -R到了家目录或者项目根目录。**相对温和,但会导致自己的私钥不可用(~/.ssh里文件必须是 600、目录 700),以及各种配置被加上执行位。个人常用目录的修复成本低,照着几个固定值改回来就行。
**第三种,把 setuid/setgid 位误清了。**比如对整个/usr/bin做了一次chmod -R 755,sudo、passwd、su这些依赖 setuid 的程序就全废了,表现为"输密码没反应"或者"operation not permitted"。这种事故的隐蔽性在于,权限值看起来"很正常",没人会怀疑 755。
一个通用教训:**执行-R之前,把命令完整念一遍,确认路径末尾没有多余空格、没有把chmod打成chown、没有在错误的目录里按了回车。**我知道这听起来像废话,但几乎所有事故都发生在这三秒钟。
5.2 救援阶梯:从单条命令到包管理器校验
权限改坏之后的恢复,思路是"从影响面小的方法开始试,不行再上重的"。
第一步,如果还能登上机器,先修最关键的几处让它恢复可用。sudo坏了可以先试试用已经开着的 root shell(如果之前留着),或者从单用户模式/救援模式进去。修sudo的最小操作通常是:
# 恢复 setuid 位 chmod 4755 /usr/bin/sudo # 恢复关键文件的权限 chmod 644 /etc/passwd /etc/group chmod 640 /etc/shadow /etc/gshadow chmod 600 /etc/ssh/ssh_host_*_key chmod 644 /etc/ssh/ssh_host_*_key.pub chmod 700 /root第二步,如果范围很大、手工改不过来,用包管理器做校验和恢复。基于 RPM 的系统:
# 校验所有包的文件权限,输出会有大量 P 标记(permission) rpm -Va # 查出某个文件属于哪个包 rpm -qf /usr/bin/sudo # 重装该包,会恢复默认权限和属主 dnf reinstall sudo基于 Debian 的系统有debsums,可以先用它检查文件内容是否被改动,权限方面则需要重装:
apt-get install --reinstall sudo openssh-server重装包的好处是它会把属主、属组、权限位、SELinux 上下文一次性复位到出厂状态,比手工猜默认值可靠得多。缺点是有些包重装会重置配置,所以动手前把/etc单独备份出来。
第三步,如果连系统都起不来了,用同版本的安装介质进救援模式,把根分区挂上,然后 chroot 进去执行上面的操作。或者更省事的办法是找一台同版本、配置相近的机器,用find / -printf '%m %u:%g %p\n'导出一份权限清单,对比着修复。这个方法笨但有效,尤其适合自建系统或者很多包管理器管不到的自定义目录。
5.3 关键路径权限基线对照表
平时存一份基线,出事的时候能省大量时间。下面这些值是我自己默认的恢复参考:
| 路径 | 权限 | 属主:属组 | 说明 |
|---|---|---|---|
| /etc/passwd | 644 | root:root | 全员可读 |
| /etc/shadow | 640 或 600 | root:shadow | 只有认证相关能读 |
| /etc/ssh/ssh_host_*_key | 600 | root:root | 主机私钥 |
| /etc/sudoers | 440 | root:root | 建议保持只读 |
| ~/.ssh | 700 | 用户自己 | 目录不能给别人任何权限 |
| ~/.ssh/id_rsa | 600 | 用户自己 | 私钥,不能多给 |
| ~/.ssh/authorized_keys | 600 或 644 | 用户自己 | 部分实现要求 600 |
| /root | 700 | root:root | |
| /tmp | 1777 | root:root | 粘滞位不可少 |
| /var/www/*/ | 755 | 部署账号 | 视 Web 服务账号调整 |
需要提醒的是,这张表是"通用参考"而不是"绝对真理",不同发行版、不同服务可能有自己的要求(比如有的系统/etc/shadow是 600,有的用shadow组)。真正可靠的做法是在系统装好的时候就把这份清单导出来留档,而不是事后凭记忆推断。
6. chmod 搞不定的部分:ACL、umask 与交付脚本里的权限固化
用了几年之后我逐渐意识到,chmod 解决的是"属主、属组、其他"这三档的需求,一旦现实中出现"三类人之外还有一类人"的情况,它就直接无能为力了。这一章讲三个补位方案。
6.1 setfacl 解决"第四类人"的需求
真实场景经常是这样:一个项目目录,属主是deploy,属组是devteam,权限 770。现在来了个外部审计账号auditor,它既不是deploy也不在devteam里,只要求能读。用 chmod 你得把 other 位打开(变成 774 或 775),但这等于把读权限给了系统上所有人,不合格。
这时候用 ACL:
# 给指定用户单独读权限 setfacl -m u:auditor:rx -R /srv/project # 给指定组读权限 setfacl -m g:qa:r -R /srv/project # 查看结果 getfacl /srv/projectgetfacl输出里会出现user:auditor:r-x这样的行,而且ls -l的权限串末尾会多一个+号,表示这个文件有扩展 ACL。只要看到那个+,就不要只用ls -l判断权限了,必须用getfacl看全貌。
这里有个和 chmod 直接相关的陷阱,前面提过但值得展开:**chmod 修改有 ACL 的文件时,会重写 ACL 的 mask 行。**mask 是 ACL 中"有效权限的上限",所有命名用户和命名组的权限都会被 mask 与一次。所以会出现这种情况:你给auditor设了rx,但文件本身的 group 位是r--,mask 就成了r--,结果auditor的rx被裁成了r,x没生效。排查这类问题的关键就是看getfacl输出里的mask::那一行。修法也简单:
# 重新计算并设置 mask,让它涵盖所有命名条目 setfacl -m m::rx /srv/project/file另外,如果目录需要新建文件自动继承 ACL,还得设默认 ACL:
setfacl -d -m u:auditor:r-x /srv/project默认 ACL 只对目录有意义,它决定了以后在该目录中新建的条目会带上什么 ACL。不加-d的话,ACL 只对当前已有的文件生效,新文件又回到普通权限,这就是为什么很多人设完 ACL 之后觉得"过几天又失效了"。
6.2 umask 与 setgid 粘滞位的共享目录配方
如果你有一台多人共用的机器,要建一个团队共享目录,下面这套配置我用了很多次都没出过问题:
sudo mkdir -p /srv/shared sudo chown root:devteam /srv/shared sudo chmod 2775 /srv/shared # 让新文件默认是 664 而不是 644 sudo setfacl -d -m g:devteam:rwx /srv/shared sudo setfacl -d -m o::- /srv/shared关键是那个2(setgid 位)。它让目录里新建的所有文件和子目录自动继承父目录的属组,也就是devteam。没有它的话,每个人创建的文件属组都是自己的默认主组,共享目录很快就变成一堆乱七八糟的属组,然后就有人开始用chmod 777了——恶性循环。
那 2775 里的 775 保证组内成员能读写进入,other 只有读和进入。如果连读都不希望给别人,用 2770。
粘滞位(1)在共享目录里的作用和/tmp不同。在共享目录里,通常我们不希望粘滞位生效,因为团队成员之间需要互相修改文件(比如改别人的代码、删除构建产物)。粘滞位会限制只能删除自己的文件,这在协作场景下反而是障碍。所以共享目录一般是 2775 而不是 3775——粘滞位不是"越安全越好",要看业务需求。
umask 这块也要提一句。默认 umask 022 意味着新建文件是 644、新建目录是 755。如果共享目录里需要"新建文件默认可写"(664),要么把 umask 改成 002(全局生效,会影响这个用户创建的所有东西,谨慎),要么依赖上面的默认 ACL(推荐)。优先用默认 ACL 而不是改 umask,因为 ACL 的作用范围是目录级的,不会污染用户在其他地方的行为。
6.3 把权限写进部署与打包流程
最后说一个我觉得最容易被忽略、但性价比最高的实践:不要指望事后修权限,要在部署和打包时就把权限固化下来。
打包这一侧,tar 会保留权限位(除了特殊位受 umask 影响),所以构建产物的时候顺手把权限设对,比部署完再批量改要可靠。用tar时注意--no-same-permissions这类选项的行为——解包时的最终权限还受目标机 umask 影响,严格的场景下应该用--same-permissions配合-p。
部署这一侧,把这些动作写成幂等的脚本,每次部署都执行一遍:
# 幂等的部署后权限修正 set -e WEBROOT=/var/www/site chown -R deploy:www-data "$WEBROOT" # 目录 755,文件 644(没有执行位的文件不会被加上) chmod -R u=rwX,go=rX "$WEBROOT" # 需要写入的目录单独提权 chmod 775 "$WEBROOT/storage" "$WEBROOT/cache" chmod -R g+w "$WEBROOT/storage" "$WEBROOT/cache" # 需要执行的脚本单独给 find "$WEBROOT/bin" -type f -name '*.sh' -exec chmod 755 {} +这段脚本的价值在于幂等:不管之前权限被谁改成了什么样,跑一遍就回到正确状态。u=rwX,go=rX这个写法保证目录拿到 x、文件不拿到 x,前面 3.2 节讲的原理在这里落地。
还有一个细节:**容器和 CI 环境下的权限往往和你的开发机不一致。**镜像层里文件的属主由构建时的 uid 决定,跑起来之后映射到宿主机的 uid 又可能是另一个数。如果你的应用需要在容器里写日志或缓存目录,别依赖 chmod,而是在 Dockerfile 里就用USER指令切换到正确的账号,并把目录属主在构建阶段设好。运行阶段临时chmod 777是很多容器权限问题的源头。
我在实际使用中最深的一个体会是:chmod 的语法半小时就能学会,但要真正用对,靠的是理解它不能做什么。改不动的时候先看挂载和文件系统类型,改完没效果的时候先看 SELinux 上下文和 ACL 的 mask,需要细分权限的时候想到 setfacl 而不是把 other 位打开,需要批量处理的时候用大写 X 而不是一把-R 777。这四条能覆盖我这些年遇到的绝大多数权限问题,剩下的那些,基本都能靠错误码和几条诊断命令定位出来。