搞Linux的人,谁没被权限坑过几回?我先说说自己的翻车现场。有一回,我在某测试服务器上部署一个静态页面,嫌每个目录单独指定权限太麻烦,直接一个chmod -R 777丢下去。当时页面是能打开了,第二天同事告诉我,站点被人改了一版。虽然只是测试环境没造成大事,但这件事让我反思了很久:权限这件事,真不是“给个777就万事大吉”那么简单。这篇笔记,是我在服务器和开发环境里反复折腾之后整理出来的,从最基础的 rwx 模型讲起,一路聊到 SUID、ACL、属性设置这些进阶玩法。不管你是刚接触 Linux 的开发者,还是平时只会chmod 755、一遇到权限错误就懵的老手,应该都能从中找到点有用的东西。
1. 权限模型先厘清:10分钟读懂 ls -l 里那一长串字符
1.1 十位字符:不止是 rwx 三个字母
很多人看ls -l的输出,只盯着后面三组 rwx 看,前面的类型字符、硬链接数、属主属组一概略过。这其实是最容易埋雷的地方。随便找个文件执行一下:
ls -l /etc/passwd -rw-r--r-- 1 root root 3156 Nov 21 10:20 /etc/passwd第一列是 10 个字符。第一个字符代表文件类型:-是普通文件,d是目录,l是符号链接,c是字符设备,b是块设备,s是套接字,p是管道文件。后面 9 个字符分成三组,分别是属主(user)、属组(group)、其他人(other)的权限。每一组里按顺序固定是 r、w、x 三个位,有对应权限就显示字母,没有就用-占位。
这里有个细节我过去常忽略:第二列的硬链接数。对文件来说,它表示这个文件有多少个硬链接指向同一个 inode;对目录来说,它一般是“子目录数量加 2”,因为每个目录都自带.和..两个隐藏项。看起来是知识题,但排错时有用。我之前遇到过一个问题,日志文件怎么删都“删不掉”,后来发现是另一个目录里还有一条硬链接,inode 没被释放。这要是对权限模型不熟,根本想不到方向。
1.2 rwx 在文件和目录上的语义完全不同
这是理解整个权限系统的关键。同样是r、w、x三个字母,用在文件上和用在目录上,含义完全不是一回事。
| 权限 | 对文件 | 对目录 |
|---|---|---|
| r | 读取文件内容 | 列出目录里的文件名清单 |
| w | 修改、截断文件内容 | 在目录里创建、删除、重命名条目 |
| x | 作为命令/脚本执行 | 进入目录、访问其中的文件(路径搜索) |
很多“诡异”的权限错误,根源都在这里。比如用户明明对某个文件有 r 权限,却打不开,因为上级目录缺少 x 权限,用户根本进不去。又比如目录给了 r 和 w 却没给 x,用户能ls看到文件名,但cd进不去,也没法对里面的文件做任何操作。
最容易被误解的是目录的 w 权限。目录的 w 权限管的是“增删改目录里的条目”,不是管文件内容的修改。文件内容改不改,看文件自己的 w 权限。所以你会看到一种组合:某个共享目录允许所有人创建文件(目录 w 开放),但每个文件的写权限被严格限制,别人删不了也改不了。这其实就是 /tmp 目录的基本设计思路,后面讲 Sticky Bit 时再展开。
1.3 属主、属组、其他人:身份的优先级
ls -l的第三列和第四列是属主和属组。权限判断时,系统按顺序查身份:先看你是不是文件的属主,是,就用属主那组权限;不是,再看你属不属于属组,是,就用属组那组权限;都不是,才落到“其他人”这组。这个顺序很重要,它意味着“其他人”权限只有在前面两轮都没命中时才会生效。
我建议每个运维新手都养成一个习惯:排查权限问题先跑id看自己当前的真实身份和附加组,再跑ls -ld看目标对象的属主属组。很多问题根本不在权限位上,而在身份上。比如程序明明在组里,但运行它的进程是以另一个用户身份启动的,那“属组权限”自然用不上。权限的尽头是身份,先把“谁在执行”搞清楚,再谈“允许做什么”。
2. 修改权限的三驾马车:chmod、chown、chgrp 的正确打开方式
2.1 符号模式和数字模式:两套打法都要熟
chmod有两种写法:符号模式和数字模式。新手期我只用数字,因为chmod 755背下来就能干活。但后来发现符号模式在“只改一个位”的场景下特别高效,比如只给脚本加执行权限,chmod +x run.sh比chmod 755 run.sh更直观,而且不用重新算数字。
符号模式的基本语法是“对象+操作符+权限”。对象有u(属主)、g(属组)、o(其他人)、a(全部);操作符有+(增加)、-(去除)、=(精确设置);权限除了 rwx,还有X(特殊执行位)、s(SUID/SGID)、t(Sticky)。
数字模式大家已经很熟了:r=4,w=2,x=1,三组加起来。这里我给一个我自己常用的速算表:
| 数字 | 含义 | 常见场景 |
|---|---|---|
| 600 | rw------- | 私钥、敏感配置 |
| 644 | rw-r--r-- | 普通文件、页面文件 |
| 700 | rwx------ | 私人目录 |
| 750 | rwxr-x--- | 协作目录(组内可读可执行) |
| 755 | rwxr-xr-x | 程序、脚本、公开目录 |
| 775 | rwxrwxr-x | 团队共享目录 |
| 1777 | rwxrwxrwt | /tmp 类共享临时目录 |
大写的X是个冷门但好用的特性:它只对目录和已有执行权限的文件生效。也就是说,chmod -R a+X .可以安全地给整个目录树里的所有目录加执行权限,不会误伤普通文件。这个在部署项目时非常好用,一条命令解决“目录好进好读、文件别乱执行”的问题。
2.2 批量操作和参考模式:别再造 777 的轮子
处理一整个目录树时,最忌讳的就是chmod -R 777。先不说安全性,它对性能也不是好东西。合理做法是分开处理目录和文件,用find配合chmod:
find . -type d -exec chmod 755 {} \; find . -type f -exec chmod 644 {} \;这两条命令跑完,目录统一是 755,普通文件统一是 644。为什么要这样区分?因为目录需要 x 权限才能进入,文件却不需要执行位。一刀切 777 等于把所有文件都变成了可执行状态,权限的细化就全毁了。
另外,chmod和chown都支持--reference参数,意思是以某个文件为模板复制权限/归属。比如chmod --reference=template.conf user.conf。如果有一套标准的“模板文件”,批量同步权限时不用一次次手算数字,效率会高很多。
2.3 关于属主和属组:chmod 管门锁,chown 管钥匙归谁
chmod只管“门锁是几道”,不管“钥匙归谁”。钥匙归谁由chown和chgrp决定。这一块我栽过好几次。
第一次部署 Web 服务时,我把网站目录设成了755、文件设成了644,按理说权限没问题,但页面就是 403。排查到最后才发现,进程是用某个专用服务账号运行的,而整个目录的属主是我自己的账号,其他账号一点权限都没有。解决办法很简单:把目录属主改成服务账号,或者把属组改成服务账号所在组并配上相应权限。
chown有个小细节:同时修改属主和属组可以直接用冒号,不用跑两条命令:
chown -R alice:staff /srv/www但我更想强调的是另一件事:chown操作会顺手清除文件上的 SUID/SGID 特殊权限位,因为系统认为“换属主之后,原来的特殊权限已失去意义”。这个坑很隐蔽,我之前给一个二进制程序换属主后,发现它突然变成一个普通程序,排查了好久才想起来是 SUID 位被清了。涉及特殊权限的文件,改属主前一定要留意,改完记得用ls -l复查一下。
3. 特殊权限位:SUID、SGID、Sticky Bit 的实战场景
3.1 SUID:为什么普通用户也能改密码
名字听上去晦涩,其实 SUID 就一个作用:当用户执行某个带 SUID 位的程序时,进程以“文件属主”的身份运行,而不是以“执行用户”的身份运行。最典型的例子是passwd:
ls -l /usr/bin/passwd -rwsr-xr-x 1 root root 63960 Feb 7 2024 /usr/bin/passwd看到属主权限里的s了吗?普通用户执行passwd时,实际是以 root 身份去改 /etc/shadow 的。这就是为什么普通用户能修改只有 root 才能写的密码文件。如果去掉 SUID 位,普通用户一跑passwd就会直接报权限错误。
SUID 是好东西,但也特别危险。一旦某个可写文件被设置了 SUID 且属主是 root,任何执行它的人都能以 root 权限做程序定义的操作,这等于给攻击者递了一把刀。我建议定期扫一遍系统里带 SUID 位的文件:
find / -type f -perm -4000 2>/dev/null看到结果后逐个核对:这个文件需不需要 SUID?属主是不是 root?是否存放在任何人可写的目录里?如果发现异常,先chmod u-s <文件路径>去掉 SUID 位再说。
3.2 SGID 和 Sticky Bit:目录场景里的黄金搭档
SGID 分两种场景。文件上的 SGID 和 SUID 类似,只是以属组身份运行;但目录上的 SGID 更有用:它能让“新创建的文件自动继承目录的属组”。团队共享目录非常依赖这个特性。
举个例子。某公司有个团队协作目录/srv/proj,大家希望能互相读写。光靠传统权限会别扭:每个人新建的文件,属组都是自己的私有组,别人根本进不去。但如果把目录配上 SGID 并设好组权限,问题就迎刃而解:
mkdir /srv/proj chown :devteam /srv/proj chmod 2770 /srv/proj2770里的2就是 SGID 位。之后无论是谁在这个目录下新建文件,新文件的属组都会自动变成devteam,配合目录的rwxrwx---权限,组内所有成员都能顺畅协作。这个组合我在多个项目环境里用下来,可以说是团队目录的最佳实践。
Sticky Bit 则是另一个守护神。它最常见于/tmp:drwxrwxrwt末尾的t就是它。有了 Sticky Bit,目录即使对所有人开放写权限,用户也只能删除/重命名“自己拥有”的文件,不能动别人放在里面的文件。没有它,共享临时目录会变成一场大混战,谁都能删别人的缓存。给任意目录设置的方法:
chmod +t /some/shared/dir3.3 特殊权限位的显示与清除
显示规则里有个小细节:如果s或t是小写,说明对应的 x 权限也存在;如果显示成大写S或T,说明设置了特殊位但没有 x 权限,这个特殊位是“无效”的。比如-rwSr--r--里的 SUID 位实际上不会生效,因为文件本身没有执行权限。
清除时对应关系如下:
chmod u-s 文件路径 # 去掉 SUID chmod g-s 目录路径 # 去掉 SGID chmod o-t 目录路径 # 去掉 Sticky Bit再啰嗦一句安全心得:特殊权限位是“能用就不用”。普通场景用传统 rwx 就能解决的事,不要为了炫技加 SUID。真要加,也务必确认执行者的来源、文件所在目录的安全性,以及程序本身有没有被篡改的余地。特殊权限位一旦失控,普通用户拿到 root 级能力只是时间问题。
4. ACL 访问控制列表:打破“一个人或一组人”的僵局
4.1 从一次“谁都要写”的争吵说起
传统权限模型里,一个文件的权限三组位只能表达“一个属主、一个属组、其他人”。但现实里经常有这种需求:某个文件,A 用户要读写,B 组要读写,C 用户只要读。靠chgrp只能绑定一个属组,靠“其他人”权限又无法精确到人。
ACL(Access Control List)就是为这种场景设计的。它把“属主、属组、其他人”这三组扩展成一张更长的名单,可以精确到多个用户和多个组。
操作方法很简单:
setfacl -m u:alice:rwx /srv/data setfacl -m g:devteam:r-x /srv/data getfacl /srv/data设置之后,ls -l /srv/data的权限位末尾会多一个+,说明这个文件带有额外的 ACL 条目。getfacl则能列出完整的 ACL 内容。ACL 的判定逻辑我优化成一句话:先查属主,再查命中的具名用户条目,再查属组和具名组条目,最后落到其他。顺序不同,结果就不同。
4.2 mask 与默认 ACL:最容易翻车的地方
ACL 里有个 mask 字段,排错时特别容易坑人。mask 的作用是“所有具名用户、具名组和属组的最大有效权限”。什么意思?比如我给 alice 设置了rwx,但 mask 只有r--,那 alice 最终实际可用的权限也只是r--,不是rwx。
这个设计是为了防止“手滑给某个人开太多权限”时至少有一层兜底拦截。但它也导致了一个经典翻车现场:你给用户加了rwx,用户却说还是打不开,一查getfacl,发现 mask 悄悄变成了r--。原因多半是有人后来用chmod调整过文件权限,掩掉了 mask 里的写、执行位。
解决办法是显式把 mask 重新提回来:
setfacl -m m::rwx /srv/data还有一个好用的进阶功能:默认 ACL。对目录设置默认 ACL 后,目录里新建的文件会自动继承一套基础权限,省去每次手工加条目的工夫:
setfacl -m d:u:alice:rwx /srv/data这个特性在团队共享目录里特别好使,前提是你得理解:默认 ACL 继承的是“默认值”,一旦子文件被显式 chmod,继承关系就可能被覆盖,又得回到排查 mask 的老路。
4.3 删除和备份:别让 ACL 成为“不可见”的权限
清除某个用户或组的 ACL 条目,用-x;清除某文件所有 ACL 权限,用-b:
setfacl -x u:alice /srv/data setfacl -b /srv/data比较关键的是备份。ACL 不会跟着普通cp走,tar和rsync也需要特定参数才会保留。cp -a会保留 ACL;tar要用--acls或者结合--xattrs;rsync要加-A。所以如果哪天你备份了一份数据,恢复出来发现“权限全对了但列表里多了 ACL”,先别意外,十有八九是备份命令没带对应参数,ACL 悄悄被丢了或者被展开成旧的 group 权限了。
5. umask 和 chattr:最容易忽略的两个权限控制层
5.1 umask:决定你新建文件“天生”是什么权限
umask 是很多人学了 Linux 半年都不一定碰的命令,但它的影响无处不在。它决定了一个事实:为什么你新建目录默认是 755,新建文件默认是 644,而不是 777 和 666。
原理其实简单:新建目录的默认权限是 777,新建文件是 666(因为文件默认不给执行位),然后“减去” umask 里对应的位。umask 默认通常是 022,所以目录变成 755,文件变成 644。
很多人把“减去”理解成普通算术,然后发现umask 022、文件666-022=644碰巧对得上,就觉得懂了。但换成umask 027时,666-027=639?这就错了。正确理解是逐位清权:0表示保留,2表示清掉写位,7表示清掉读、写、执行。所以666在 umask027下,属主保留 6,属组清掉写位变成 0,其他人清成 0,结果是640,而不是639。
| umask | 新建文件权限 | 新建目录权限 | 适用场景 |
|---|---|---|---|
| 022 | 644 | 755 | 常规服务器 |
| 002 | 664 | 775 | 团队写作目录 |
| 027 | 640 | 750 | 需要收紧属组权限 |
| 077 | 600 | 700 | 单用户独占 |
如果服务账号的 umask 是 077,它生成的文件属组信息再多也没用,其他组一概进不去。这类问题排查起来极隐蔽,因为文件权限看起来“正常”,实际上权限极窄。
5.2 chattr:给文件上最后一道锁
chmod 管的是“谁能碰”,chattr 管的是“它到底能不能被碰”——哪怕你是 root。chattr +i是 immutable(不可变)属性,文件一旦加上,内容不能改、不能被删、不能被重命名甚至不能创建硬链接。chattr +a则是 append-only,只能追加写,不能截断或覆盖。
实际使用中,+i适合保护重要配置文件,比如/etc/nginx/nginx.conf、关键密钥文件。设置之后,哪怕脚本里有>重定向误写,也不会把配置文件冲掉。+a适合日志文件,保证日志只能往后追加,半夜日志被外部工具清空这类事故能直接避免。
sudo chattr +i /etc/nginx/nginx.conf sudo lsattr /etc/nginx/nginx.conf # 需要改配置时先解锁 sudo chattr -i /etc/nginx/nginx.conf这里要特别提醒:带 immutable 属性的文件,即使 root 去删除也可能看到Operation not permitted。常见的问题是运维在排查问题时以为文件被锁在文件系统层,怎么都删不掉,直到lsattr才发现是自己之前加的锁。所以遇到“权限正确却无法操作”的怪事,记得跑一下lsattr。
5.3 挂载选项对权限的影响:chmod 也不总是万能
前面聊的都是文件系统内部的权限位,但权限真正生效还依赖挂载层。mount的选项能在一层之内把权限“阉割”掉:
ro:整个文件系统只读,chmod 想改也改不了noexec:即使有 x 权限,也不能执行文件nosuid:忽略 SUID/SGID 位,即使设置了也不生效
这解释了为什么你把一个脚本拷到 U 盘上、权限明明给了 755,运行却说Permission denied。大概率是挂载 U 盘时带了noexec,或者是文件系统本身不支持 Unix 权限。Windows 下的 FAT/NTFS 尤其典型,Linux 里 chmod 改不动它,全看挂载参数怎么定。
遇到这类问题,先用mount或findmnt看当前挂载选项,再决定是在挂载层面放开,还是换个文件系统。别一上来就怀疑自己的权限命令写错了,往往是上层环境就把它拦住了。
6. 实战复盘:从一个 Permission denied 到完整权限整改
6.1 排查三板斧:id、namei、getfacl
权限出问题的时候,最忌讳的是瞎试chmod 777。我的排查流程固定是三步:
第一步,id确认当前身份和附加组,看进程到底是谁在跑。很多时候服务类进程用的是专用账户,终端里你是 root,服务不是。
第二步,namei -l查看路径每一级目录的权限,确认“到文件之间”的每一步都畅通。这一步能查出那种“文件权限明明对,但上层目录没 x”的诡异情况:
namei -l /var/www/site/index.html第三步,getfacl看有没有 ACL 条目,确认没有 mask 在背后截断。特别提醒:看到ls -l权限末尾是+时,一定要跑这一步,因为你看到的“普通权限”已经不是完整的生效规则了。这套流程下来,绝大多数权限问题都能在一分钟内定位到根因。
6.2 常见权限问题速查表
下面这张表是我平时排查时最常用的一批结论,遇到权限错误可以先对号入座。
| 现象 | 常见原因 | 排查命令 | 解决手段 |
|---|---|---|---|
| 文件显示有权限,但程序读不到 | 上级目录缺 x 权限 | namei -l | 补齐路径目录执行权限 |
| 页面 403 / 目录打不开 | 属主或属组不符 | ls -ld、id | chown 调整属主属组 |
| 改文件提示 Operation not permitted | chattr +i 或文件系统只读 | lsattr、mount | chattr -i / 重新挂载 rw |
| 用户加了 ACL 权限仍无效 | mask 被截断 | getfacl | setfacl -m m::rwx |
| U 盘脚本无法执行 | 挂载带 noexec | findmnt / mount | 去除 noexec 重新挂载 |
| 删除文件提示 Not owner | 目录无 Sticky 权限控制 | ls -ld 看 t 位 | 按 Sticky 规则调整属主 |
这里要强调一个容易混淆的点:Permission denied和Operation not permitted是两个不同层面的错误。前者是正常权限校验不通过;后者往往是文件属性或文件系统挂载导致,连 root 都可能被挡。遇到后者还死磕 chmod,方向就错了。
6.3 一次真实整改:从 777 回归最小权限
最后复盘一个发生过很多次的项目场景。某开发环境目录被历史项目搞成了777,整个目录树七零八落,有 root 的、有某个开发者的,还有几个归属不明的属组。我接手后的整改动作是:
第一步,识别真实使用者。通过ps aux看服务进程身份,通过getfacl -R看现存权限全貌,把目录里到底有谁在读写整理清楚。第二步,重建属主属组。把项目目录统一归到专用服务账号和项目组,比如chown -R appuser:appgroup /srv/project。第三步,用find分目录和文件批量重设权限,目录一律 755、普通文件一律 644,可写目录单独用 775 加组权限。第四步,扫描并清理特殊权限与 ACL,不需要的直接find /srv/project -perm -4000确认后去掉。
整个过程里踩过的坑有两个。第一个是“改了权限但程序还报错”,后来发现是应用进程的缓存记住了旧路径权限,重启服务后立刻正常。所以权限整改后,服务该重启就重启,别在半夜被一个陈旧状态误导。第二个是“目录权限对了但默认 ACL 没设”,团队协作时新建文件依旧按旧 umask 生成,落地到共享目录里别人读不了。这时候正确做法是配合第 4 章的默认 ACL,把协作目录的基础规则一次性固定下来,而不是每天追在新文件后面补权限。
6.4 最后分享两个小习惯
第一,每次设置完权限后用ls -ld和getfacl各看一眼,确认结果符合预期,顺手养成这个习惯能避免大量“改了没生效”的返工。第二,递归命令前先想想范围,chmod -R、chown -R这类操作一旦带上.或绝对路径,就要格外小心。宁可先备份一份权限清单再动手,也不要拿生产环境试错。权限管理的本质是给系统画边界,边界画对了,日常运维能省下无数“为什么又能访问了”的半夜电话。