权限这个东西平时没什么存在感,一出问题就是线上事故。我印象很深的一次,某台内部工具服务器上页面全部返回 403,排查了半小时,最后发现是有人为了“省事”对整个站点目录敲了chmod -R 777,后来又有人反向执行了一条chmod -R 644,把目录的进入位一起抹掉了,Nginx 连index.html都读不到。这类事故的共同点是:命令本身只有几个字符,但背后牵扯的是 Linux 的整套权限模型——谁能读、谁能写、谁能进目录、谁能执行脚本,全由那十个字符决定。这篇内容就是围绕Linux chmod 命令展开的,我会讲清楚它到底在改什么、符号模式和数字模式各自适合什么场景、递归改权限为什么会把人送进坑里,以及一套可以直接抄走的目录权限配置方案。不管你是刚接触 Linux 命令行的新手,还是已经能在服务器上跑服务但权限配置一直靠“试”的开发,都能从里面找到能立刻用上的东西。
1. 先搞明白 chmod 到底在改什么东西
很多人学 chmod 的第一天就在背“755 是读写执行”,但真正卡住人的从来不是这几个数字,而是不知道改完之后谁会受影响、影响在哪个层面。所以在敲命令之前,得先把ls -l输出的那串字符完整读懂。
1.1 读懂 ls -l 那一串十位字符
在终端里随便ls -l一下,你会看到类似这样的输出:
-rwxr-xr-- 1 alice devs 20480 Mar 12 09:41 deploy.sh drwxr-xr-x 5 alice devs 4096 Mar 12 09:40 scripts第一位字符描述的是文件类型,不是权限:-表示普通文件,d表示目录,l表示符号链接,c和b表示字符设备与块设备,s表示套接字,p表示命名管道。这一段经常被忽略,但它在排查“为什么这是个目录我却改不了”时很关键——目录和普通文件的权限语义完全不一样,后面会专门说。
紧接着的九位字符才是权限,它被严格切成三组,每组三位,顺序永远是rwx:
| 位置 | 组别 | 作用对象 |
|---|---|---|
| 第 2-4 位 | u(user/owner) | 文件的属主 |
| 第 5-7 位 | g(group) | 文件所属用户组里的成员 |
| 第 8-10 位 | o(others) | 既不是属主也不在属组里的其他所有用户 |
以-rwxr-xr--为例:属主可读可写可执行,属组成员可读可执行,其他人只能读。这里的“可执行”对目录来说含义完全不同,这就是新手最容易糊掉的地方。
1.2 r、w、x 作用在目录上到底意味着什么
对普通文件,三位权限的语义很直白:r能读内容,w能改内容,x能让内核把它当作程序加载执行。但对目录,语义整体偏移了一层,可以这样理解——目录本身是一张“文件名到 inode”的对照表:
r表示能不能读这张表,也就是能不能ls列出目录里有哪些文件;w表示能不能修改这张表,也就是能不能在目录里新建、删除、重命名文件;x表示能不能穿过这个目录,也就是能不能访问目录里的某个具体文件。
这里有个非常反直觉的点:在目录里删除一个文件,看的是目录的写权限,而不是文件本身的写权限。一个文件即使是444只读,只要它所在目录对你有w,你照样能rm掉它。这也是为什么/tmp会给所有人写权限,但必须配合粘滞位(sticky bit),否则任何用户都能删掉别人创建的临时文件。
另一个高频故障是缺x。一个目录是644而不是755时,ls能看到文件名,但访问里面的文件会直接Permission denied,因为内核拒绝了你“穿过”目录的动作。很多人看到 403 就去查文件权限,其实问题出在父目录上——从根到目标文件的每一级目录都必须有x,缺一个都不行。
1.3 数字模式背后是二进制位,不是魔法数字
755、644、600这些数字之所以能表示权限,是因为三位权限正好对应三个二进制位,把它当成八进制数读就通了:
| 八进制 | 二进制 | 权限表示 | 含义 |
|---|---|---|---|
| 0 | 000 | --- | 无权限 |
| 1 | 001 | --x | 仅执行 |
| 2 | 010 | -w- | 仅写 |
| 3 | 011 | -wx | 写 + 执行 |
| 4 | 100 | r-- | 仅读 |
| 5 | 101 | r-x | 读 + 执行 |
| 6 | 110 | rw- | 读 + 写 |
| 7 | 111 | rwx | 读写执行 |
所以755拆开就是7 5 5→rwx r-x r-x,644拆开是6 4 4→rw- r-- r--。理解了这个换算,你就不需要再背任何一张表,看到数字就能在脑子里还原出字符,反过来也行。需要提醒的是,数字只能是 0 到 7,写chmod 855 file会直接报invalid mode,因为它本质上不是一个十进制数,而是一个三位的八进制位掩码。
1.4 第四位数字:suid、sgid 和粘滞位
除了三位基本权限,还有一个更前面的“特殊位”,写成四位数字时才会出现,分别是4(SUID)、2(SGID)、1(Sticky):
4755让程序以属主身份运行,最典型的是系统里的passwd命令,普通用户改密码需要写/etc/shadow,靠的就是这个;2755作用在目录上时,目录内新建的文件会自动继承该目录的属组,多用户协作目录几乎都靠它;1777就是/tmp的经典配置,允许所有人创建文件,但只有文件属主(或 root)能删除自己的文件。
注意:给普通脚本随便加 SUID 是非常危险的做法,现代内核在很多挂载配置下会直接忽略脚本上的 SUID 位,写了也不生效,而且一旦生效就意味着任何人执行它都拥有属主的权限,包括 root 权限。
2. 两种语法怎么选:符号模式与数字模式
chmod 提供了两套表达方式,它们不是新旧关系,而是适用场景不同。实际工作中我的习惯是:大范围重设权限用数字模式,微调某一位用符号模式。
2.1 符号模式的语法结构拆解
符号模式的完整形式是chmod [ugoa][+-=][rwxXst] 文件,可以拆成三个部分看:
- 第一部分是“对象”,
u属主、g属组、o其他人、a全部(等价于ugo),省略时默认等价于a,但会受umask影响,这一点在 2.4 节细说; - 第二部分是“操作符”,
+添加、-移除、=直接赋值(会先清空该组原有的所有权限位); - 第三部分是“权限”,除
rwx外还有X、s、t三个特殊字符。
举几个真实会用到例子:
chmod u+x deploy.sh # 只给属主加执行权限,最安全的加执行方式 chmod a-w notes.txt # 移除所有人的写权限,防止误改 chmod g+w,o-rwx shared.txt # 给组加写,同时掐掉其他人的所有权限 chmod o= config.ini # 清空其他人的全部权限,= 后面留空即可 chmod u=rw,g=r,o= secret.key # 一次性精确设定,等价于 640这里要特别注意=的行为:chmod o= file会把 others 的r、w、x全部清零,而不是只清某一个。很多人误以为它跟-一样是“移除指定项”,实际是把整组重写。所以用=的时候要一次性把该组的权限写全,chmod g=rw和chmod g+w在结果上可能一样,但在语义上一个是从零重写、一个是增量添加,遇到特殊位时差别会显现出来。
2.2 大写 X 是递归场景下的救命字符
X是符号模式里最被低估的一个字符。它的规则是:只有当目标是目录,或者文件本身已经有任意一个执行位时,才给它加执行权限。
为什么需要它?想象你要给一个共享目录做递归授权。如果用chmod -R a+x,所有普通文本文件、图片、配置文件都会被打上执行位,虽然不至于立刻出问题,但这是典型的权限膨胀,而且会让人分不清哪些文件真的是可执行程序。而用chmod -R a+rX,目录会自动拿到x(因为目录需要x才能被进入),已经有执行位的脚本会保留执行位,普通文件则依然不带执行位。
sudo chmod -R a+rX /srv/shared这一条命令基本可以覆盖“给一组人开放某个目录的读取访问”的全部需求,比手工分别处理目录和文件要稳妥得多。
2.3 数字模式的效率与风险
数字模式的优点是一次写完,没有中间状态。要做“把权限精确设成 640”这种操作,只有chmod 640 file最干脆,用符号模式得写chmod u=rw,g=r,o= file才能达到同样效果。
但它有两个风险。第一个风险是它会覆盖掉特殊位。chmod 755 dir会把之前设置的 SGID 位一并清掉,因为三位数字隐含了第四位为0。如果你维护的是一个协作目录,本来设了2775,某次脚本里来一句chmod -R 755,SGID 就消失了,之后新建文件的属组全变成执行者的主组,第二天同事就说“我上传的文件别人改不了”。
第二个风险是数字模式对目录和文件不加区分。chmod -R 644 /path是很多事故的源头,它把目录的执行位也抹成了r--,结果整棵子树从外面看得到、进不去。这一点在第 3 节会用具体案例展开。
2.4 符号模式省略对象时的 umask 陷阱
chmod +x file这种省略对象的写法看起来最省事,但它有一个历史遗留行为:GNU 版本的 chmod 在省略对象时,会把umask中已经置位的权限位一并加进去。换句话说,如果你的umask是022,chmod +x file实际等价于chmod a+x;但如果umask是077,语义上可能更接近chmod u+x。
这个行为在不同实现上存在差异,写脚本时非常不可靠。我在生产脚本里一律强制写全对象:要全部就给a+x,要属主就给u+x,绝不用裸的+x。这种习惯不会让命令变长多少,但能保证换一台机器、换一个 shell 环境结果完全一致。
3. 完整实操:给一个站点目录配置权限
光讲概念没意义,直接走一遍我实际部署静态站点时用的流程。这是一个虚构的模拟项目,目录结构大致是:源码和静态资源由 root 掌控,上传目录需要运行服务的账号可写,配置文件只允许服务和 root 读。
3.1 权限规划表
先把要达成的目标列出来,动手前想清楚比敲完再改省事得多:
| 路径 | 期望权限 | 属主:属组 | 理由 |
|---|---|---|---|
/var/www/app | 755 | root:root | 根目录只读进入,防止被篡改 |
/var/www/app/static | 755 | root:root | 静态资源只需读取 |
/var/www/app/scripts | 750 | root:root | 运维脚本不对外开放 |
/var/www/app/uploads | 2775 | www-data:www-data | 服务可写,新文件继承属组 |
/var/www/app/config | 750 | root:www-data | 服务可读,其他人完全隔离 |
/var/www/app/config/*.yml | 640 | root:www-data | 配置含敏感信息,禁其他人 |
这张表的核心逻辑就是最小权限原则:每一条访问路径上,只有确实需要的人拿到确实需要的那一位,其余一律收掉。特别注意config目录给了750而不是755,因为目录的r会让别人看到里面有哪些配置文件,虽然读不到内容,但文件名本身也是信息。
3.2 分步执行与顺序讲究
# 1. 先定属主属组,再动权限 sudo chown -R root:root /var/www/app # 2. 上传目录换属主,注意这一步要在 chmod 之前 sudo chown -R www-data:www-data /var/www/app/uploads # 3. 用 find 分别处理目录和文件,而不是 -R 一把梭 sudo find /var/www/app -type d -exec chmod 755 {} + sudo find /var/www/app -type f -exec chmod 644 {} + # 4. 单独修正例外目录 sudo chmod 2775 /var/www/app/uploads sudo chmod 750 /var/www/app/scripts sudo chmod 750 /var/www/app/config sudo chmod 640 /var/www/app/config/*.yml # 5. 给运维脚本单独加执行位 sudo find /var/www/app/scripts -type f -name "*.sh" -exec chmod 750 {} +这里有三个顺序细节值得说。第一,必须先 chown 再 chmod,因为某些内核配置在改变文件属主时会自动清除 SUID/SGID 位,这是安全设计——防止你把一个 SUID 程序换个属主就变成了提权工具。如果你先 chmod 设了2775再 chown,SGID 可能白设了。
第二,用find -type d和-type f分开处理,而不是chmod -R 755。前者给目录755、给文件644,各自都是合理值;后者会给所有文件也加上执行位,虽然不算致命,但等于放弃了“哪些是程序”这个信息维度。命令末尾的{} +比{} \;效率高,因为它会把多个文件路径合并成一次调用,而不是每个文件启一次 chmod 进程——在几万个文件上差别非常明显。
第三,例外目录要在批量操作之后单独修正,不能在批量操作里靠-R的遍历顺序去碰运气。
3.3 验证与现场核对
改完不验证等于没改。最直接的方式是stat一次看全:
stat -c '%a %U:%G %n' /var/www/app /var/www/app/uploads /var/www/app/config /var/www/app/config/*.yml输出类似:
755 root:root /var/www/app 2775 www-data:www-data /var/www/app/uploads 750 root:www-data /var/www/app/config 640 root:www-data /var/www/app/config/db.yml%a会以八进制显示完整权限,包括第四位的特殊权限,这就是我偏爱stat而不是ls -l的原因——ls -l里 SGID 显示为rwxrwsr-x中那个s,小写s表示执行位也在,大写S表示有 SGID 但执行位缺失,肉眼很容易看漏。批量核对可以写一条循环:
find /var/www/app -maxdepth 2 -exec stat -c '%a %U:%G %n' {} + | sort对于需要回滚的场景,动手前先存一份快照:
find /var/www/app -exec stat -c '%a %U:%G %n' {} + > /tmp/perm-backup.txt出问题就按这份记录反向写回去。这条习惯帮我救过好几次场,尤其是接手别人维护的目录时——你永远不知道某个看起来奇怪的750是不是某个服务依赖的。
3.4 实测验证:用非 root 账号走一遍
权限配置最容易犯的错误是“用 root 验证一切正常”。root 几乎不受权限位限制(只要某一位有x,root 就能执行),所以 root 能读不代表服务账号能读。正确的验证方式是su到真实运行账号再试:
sudo -u www-data cat /var/www/app/config/db.yml # 应有输出 sudo -u www-data ls /var/www/app/config # 应能看到文件列表 sudo -u www-data touch /var/www/app/uploads/t.txt # 应成功 sudo -u www-data touch /var/www/app/static/t.txt # 应失败四条命令跑下来,预期结果都能对上,这套权限配置才算落地。
4. 常见报错与排查速查表
权限类问题的麻烦之处在于报错信息往往只有一个Permission denied,什么都不告诉你。下面这些是我这些年反复遇到的场景,整理成速查表会省很多时间。
4.1 chmod 自身执行失败的三类原因
| 报错信息 | 根因 | 处理方式 |
|---|---|---|
Operation not permitted | 当前用户既不是文件属主也不是 root | 用ls -l确认属主,切换到属主或提权执行 |
Read-only file system | 挂载点以只读方式挂载 | mount | grep 挂载点确认,重新挂载为读写 |
No such file or directory | 路径写错,或中间某一级目录缺x无法穿越 | 逐级ls检查路径 |
第三类最隐蔽。chmod 755 /a/b/c/file报“文件不存在”,很可能file明明就在那儿,只是/a/b这个目录对当前用户没有x,内核在解析路径时就失败了,报出来的错误却像是文件不存在。排查办法是从根开始逐级ls -ld,看哪一级的权限位里没有x。
4.2 权限改完了但访问仍然失败
这一类问题比 chmod 报错更让人抓狂,因为命令明明成功了。常见原因有四种:
- 父目录缺
x:最常见。从/到目标文件的每一级目录都要有x,任何一级缺失都会让访问失败,即使目标文件本身是777。 - ACL 规则覆盖:文件上如果挂了 POSIX ACL,
ls -l显示的权限位只是“掩码”的一部分,真实生效的是 ACL 条目。用getfacl 文件看完整规则,别只信ls。 - 挂载选项限制:目录以
noexec挂载时,文件即使有x也无法执行,报Permission denied;以nosuid挂载时 SUID 位会被忽略。 - 强制访问控制策略:某些发行版默认启用了额外的安全模块,会在传统权限之外再做一层判断。这类问题通常表现为“权限位完全正确但就是不让你访问”,需要用对应的审计日志确认。
排查顺序我一般固定为:先namei -l /完整/路径一次性列出整条路径每一级的权限,再getfacl,最后看挂载选项。namei这个命令很多人不知道,但它在排查路径类问题时比一级一级ls快得多。
4.3 递归改权限的典型翻车现场
chmod -R是我见过事故率最高的参数之一,几乎每个翻车现场都能对应到下面某一种:
第一种,chmod -R 644 目录。目录失去了x,整棵子树无法进入。现象是网页 403、cd进不去、ls能看到名字但读不到内容。修复方式是用find -type d把目录单独补回755。
第二种,chmod -R 777 目录。这是另一个极端,看似“解决了所有权限问题”,实际上把写权限开放给了所有人,任何能登录的账号都能篡改站点文件。而且它会掩盖真正的权限设计缺陷——本来应该通过属组解决的问题,被 777 粗暴盖过去了。
第三种,chmod -R 755覆盖了 SGID。协作目录本来是2775,一次递归之后变成755,之后新建文件的属组不再继承,同事之间的互相修改立刻失效。这类问题往往过几天才暴露,排查成本很高。
第四种,对符号链接递归。chmod -R会跟随目录内的符号链接去改目标,如果链接指向系统目录,可能造成灾难性后果。稳妥的做法是先find 目录 -type l看看有没有链接,必要时改用find -type d、-type f精确控制,或者用-P相关选项避免跟随。
提示:任何时候准备敲
chmod -R,先在心里问一句“这棵子树里有没有符号链接、有没有需要保留特殊位的目录、有没有本来就不该带执行位的文件”。三个问题里有一个答不上来,就别用-R,改用find分类处理。
4.4 特殊位相关的异常表现
SUID、SGID、Sticky 出问题时的现象都不太直观,值得单独列一下。执行一个本该以 root 身份运行的脚本却提示“权限不足”,先确认 SUID 位还在不在——用stat -c '%a'看第一位是不是 4。协作目录里新建的文件属组总是变成执行者的主组,说明 SGID 丢了或者目录根本没设。/tmp里能删别人的文件,说明粘滞位没生效,多半是被某次chmod 777覆盖成了三位数字。
还有一个细节:SGID 作用在目录上时,子目录会继承 SGID,但普通文件只会继承属组,不会继承 SGID 位。这也是为什么新建的普通文件不会莫名其妙带上特殊权限,这个设计本身是合理的,但如果不了解,就会误以为“SGID 没生效”。
5. 把权限管住:从单条命令到工程习惯
chmod 本身只是一条命令,真正决定生产环境稳不稳的,是围绕它建立起来的一整套习惯。这一节讲几个我认为收益最高的实践。
5.1 用 umask 从源头上减少补救
大多数权限问题其实是“创建时权限就不对,事后靠 chmod 补”。而创建时的默认权限由umask决定:新文件的默认权限是666减去 umask,新目录是777减去 umask。
| umask | 新建文件 | 新建目录 | 适用场景 |
|---|---|---|---|
| 022 | 644 | 755 | 最常见的默认值,同组可读 |
| 002 | 664 | 775 | 团队协作目录,同组可写 |
| 077 | 600 | 700 | 私有数据,其他人和同组都不可见 |
| 027 | 640 | 750 | 折中方案,同组只读,其他人隔离 |
设置方式有三种粒度:临时用umask 027只影响当前 shell;写进~/.bashrc影响某个用户;写进/etc/profile影响所有登录用户。服务进程的 umask 通常由服务管理器配置,默认值可能是022也可能是027,这一点在排查“服务生成的文件为什么别人读不到”时经常被忽略。
我在协作型项目里的固定做法是:把共享目录设成2775,同时在相关账号的 shell 配置里设umask 002。两头都做,新建文件的权限天然就是对的,基本不需要事后批量 chmod。
5.2 权限变更要可追溯、可回滚
生产环境里我给自己定了一条规矩:任何涉及-R的权限变更,前后各存一次快照。
find /srv/app -exec stat -c '%a %U:%G %n' {} + | sort > /tmp/before.txt # ...执行变更... find /srv/app -exec stat -c '%a %U:%G %n' {} + | sort > /tmp/after.txt diff /tmp/before.txt /tmp/after.txtdiff出来的内容就是这次变更的真实影响面,比凭记忆判断靠谱得多。如果变更范围超出预期,立刻按before.txt回滚。这套流程看着啰嗦,但比起线上故障后花两小时定位,几十秒的准备工作非常划算。
如果目录上用了 ACL,还要额外备份:getfacl -R /srv/app > /tmp/acl.bak,回滚用setfacl --restore=/tmp/acl.bak。ACL 和传统权限位是两套机制,只备份权限位是不够的。
5.3 什么时候该放弃 chmod,改用 ACL
chmod 的模型只有“属主 / 属组 / 其他人”三个槽位,一旦遇到“需要让 A、B 两个组的成员都能读,但 C 组不能读”这种需求,靠不断建新组、改属主是解决不了的,越改越乱。
这种时候应该上 POSIX ACL:
# 让 devs 组对目录有读写执行,ops 组只读 setfacl -m g:devs:rwx /srv/app setfacl -m g:ops:r-x /srv/app # 设置默认 ACL,让目录内新建的文件自动继承 setfacl -d -m g:devs:rwx /srv/app setfacl -d -m g:ops:r-x /srv/app需要注意 ACL 生效后,ls -l里权限位的最后一位会显示成+,表示“还有额外规则”,此时你看到的rwx已经不是全貌了,必须用getfacl查看。我见过有人看到ls -l显示rwxr-xr-x就以为别人读不到,结果 ACL 里明明白白给某个组开了权限。只要目录上有+标记,判断权限就必须以getfacl为准。
5.4 几条我踩过坑之后固化下来的习惯
第一,不写裸的chmod 777。如果一时想不到正确方案,就先设755或者750,然后去查到底是谁、在哪一步缺了哪一位。777 只是把问题藏起来,而且会给后续排查埋下更大的坑——一个 777 的文件,你无法通过权限位判断它“本来应该是什么”。
第二,脚本和配置文件的权限分成两类管。凡是需要执行位的,一律750或755并且属主是 root;凡是纯数据的,一律640或644。这条线划清楚,后面就不会出现“给配置文件加了执行位”这种别扭事。
第三,改动前先确认自己是谁。whoami和id这两条命令在权限排查里用得非常多,尤其是用sudo切换到别的账号操作之后,很容易忘了当前身份。我遇到过有人在容器里以 root 身份改完权限,回到宿主机发现文件属主全变成了容器里的某个 UID,权限位看着对,但对不上任何一个真实用户。
第四,跨机器同步代码时注意权限漂移。用打包工具或版本控制同步文件时,执行位很容易丢失或被意外加上。如果是 git 管理的脚本,记得检查文件模式位是否被正确记录;如果是压缩包分发,解压后统一跑一遍find -name "*.sh" -exec chmod 750 {} +补齐执行位,比逐个chmod靠谱。
第五,给别人交接目录时附一份权限清单。我自己维护的每个服务目录旁边都会放一个注释文件,写清楚每个子目录的期望权限和原因。看起来是多余的动作,但半年后你自己回头看,或者换人来接手,这份清单能省下大量“这条 750 到底是故意的还是手滑”的猜测。
最后分享一个我自己常用的组合:接手一个陌生目录时,先namei -l看整条路径,再find 目录 -maxdepth 2 -exec stat -c '%a %U:%G %n' {} + | sort拉一份完整清单,两分钟就能对这套权限的“设计意图”有个大致判断。比一条条ls -l翻要快得多,也不容易漏掉那些藏在深层、权限设得莫名其妙的目录。