Series · Linux · Chapter 2

Linux 文件权限:rwx、chmod、chown 与超越它们的机制

系统讲清 Linux 权限模型:rwx 在文件与目录上的不同语义、数字和符号写法、chmod/chown 用法、umask 默认值、SUID/SGID/Sticky 与 ACL,附排障清单。

文件权限看上去是基本功——chmod 755 一敲就完了——但它在生产中惹出的麻烦排得上前几名:服务起不来、部署脚本默默没动静、Nginx 蹦个 403、共享目录漏成筛子、rm 偏偏不让删一个"应该能删"的文件。光记几个魔数对这些都不管用,真正能救场的是同时把三件事想清楚:

  1. 同样的 r/w/x 三个位,在普通文件和目录上含义完全不同——绊住绝大多数人的就是目录这一侧。
  2. 内核的判定走的是 owner / group / others 的三段 if/else if/else,不是把匹配位加起来——所以"刚好在文件所属组里"有时反而比"完全的外人"更糟。
  3. umask、SUID、SGID、sticky bit、ACL 各自存在都有一个具体的理由,超出那个理由去用,是系统被搞穿的常见入口。

这篇会从最小概念开始往上拼:位的语义、数字与符号两种写法、chmod/chown/chgrp、用 umask 控制默认权限、三个特殊位、用 getfacl/setfacl 玩 ACL,最后一份能直接对照排障的清单。例子都是实际碰得到的——webroot、团队共享目录、/tmp、被 +i 钉死的配置文件——而不是教材里的玩具。

权限模型:owner / group / others

mode 字符串解剖以及 rwx 在文件/目录上的差异

Linux 是多用户系统,所以每个 inode(普通文件、目录、符号链接、设备……)都挂着三个身份接口:

  • 属主(u:拥有这个 inode 的 UID,一般就是创建者。
  • 属组(g:一个 GID,组内成员共享组级权限。
  • 其他人(o:既不是属主、也不在属组里的所有人。

内核做权限检查不是"把匹配的位加起来",而是一个严格的首匹配级联

if  调用者 uid == 文件 uid       -> 用 OWNER 位,决策即终
elif 调用者所属组集合 ∩ {文件 gid}  -> 用 GROUP 位,决策即终
else                              -> 用 OTHERS 位

这个顺序带来一个常常吓到人的后果:owner 位禁止做某事,那么"恰好在组里"也救不了你chmod 047 myfile 这一下,属主自己读不了自己的文件,反倒 others 能读、能写、能执行。

root(uid 0)凭借 CAP_DAC_OVERRIDE 直接绕过整个检查。唯一一个反方向的例外是 sticky bit,下面会讲。

10 个字符的 mode 字符串

ls -l 输出形如 -rwxr-xr-x,从左到右逐位看:

位置含义
1文件类型:- 普通、d 目录、l 软链接、c/b 字符/块设备、s socket、p FIFO
2–4属主的 rwx
5–7属组的 rwx
8–10其他人的 rwx

每一位是下面三种之一:

  • r(4)—— 读
  • w(2)—— 写
  • x(1)—— 执行(在目录上是"穿过")

每一组三位相加得到一位 8 进制,三组拼起来就是熟悉的 755644600。如果在 x 的位置看到 s/S/t/T,说明还附带了一个特殊位——后面会展开。

rwx 在文件与目录上:最容易翻车的地方

绝大多数权限 bug 都藏在这里。同样的字母,不同的语义。

在普通文件上

含义没它就做不到
r读字节catlesscp 当源
w覆盖、截断、O_TRUNC 打开> 重定向、原地编辑
x把文件当程序执行(要有合法的 ELF 头,或脚本要有 shebang)./prog

注意 w 管修改文件内容。删除文件不需要文件本身的 w——那由父目录的 w 决定。

在目录上

含义没它就做不到
r列出目录里有哪些条目ls dir/
w创建、删除、重命名条目(同时还要有 xtouch dir/xrm dir/x、目录内 mv
x在目录里按名查找、并把它作为路径的一部分穿过去cd dircat dir/已知名字、任何包含 dir 的路径访问

三个小实验把规则坐实:

情况 A —— 有 rx(mode 644)

1
2
3
4
chmod 644 mydir
ls mydir              # OK:能列出名字
cd mydir              # 拒绝:没有穿过位
cat mydir/file.txt    # 拒绝:路径解析需要每一段都有 x

情况 B —— 有 xr(mode 311)

1
2
3
4
chmod 311 mydir
ls mydir              # 拒绝:不能枚举
cd mydir              # OK
cat mydir/file.txt    # OK,前提是你**正好知道**文件名

“私有 bin"目录的小把戏就是这么来的:能穿过、不能列。

情况 C —— 有 wx(mode 622)

1
2
chmod 622 mydir
touch mydir/newfile   # 拒绝:连目录都进不去

目录上单独的 w 毫无用处,必须配 x

目录的经验法则:x 是承重位;w 只有和 x 一起才有意义;r 是锦上添花。

chmod:数字 vs 符号写法

两种写法到达同一个目标

两种写法最终写入的都是相同的九位。差别在于:你是在声明一个绝对的目标,还是在做一个相对的修改

数字写法 —— 绝对

按身份把 r=4w=2x=1 加起来,三段拼成三位:

1
2
3
4
5
chmod 755 script.sh   # rwxr-xr-x —— 典型可执行
chmod 644 file.txt    # rw-r--r-- —— 典型数据文件
chmod 600 secret.key  # rw------- —— 私钥、ssh key、.env
chmod 700 ~/.ssh      # rwx------ —— 私有目录
chmod 777 shared      # rwxrwxrwx —— 几乎一定是错的

适合已知目标状态的场景:脚本化部署、新生成的文件,反正之前是什么不重要。

符号写法 —— 相对

身份u/g/o/a)+ 操作符(+/-/=)+ 位(r/w/x/X/s/t):

1
2
3
4
5
chmod u+x  script.sh        # 只给属主加 x
chmod go-w file.txt         # 把组和 others 的 w 都拿掉
chmod o=r  file.txt         # others 设成正好 r--
chmod a+r  notes.md         # 所有人都能读
chmod u=rwx,g=rx,o=    dir  # 多个子句用逗号分隔

对目录树来说,大写的 X 是杀手锏

1
chmod -R u=rwX,go=rX  project/

X 只会在目录、以及"原本就至少有一个 x“的文件上加 x。没有它,chmod -R 755 project/ 会把每个 .md.png.csv 也变成"可执行”——本身没什么破坏力,但很丑,对扫描错配 webroot 的人来说也是免费情报。

符号写法适合只动一个维度而不去碰其它位的场景。

chown / chgrp:改所有权

1
2
3
4
5
6
sudo chown alice file               # 只改属主
sudo chown alice:devs file          # 同时改属主和属组
sudo chown :devs file               # 只改属组(也可以用 chgrp)
sudo chgrp devs file                # 同上
sudo chown -R alice:devs project/   # 递归
sudo chown --reference=template new # 复制另一个文件的所有权

几条底线:

  • 只有 root 能任意改所有权。普通用户不能把文件"送给"别人——否则就能用"把文件改成别人的"绕开磁盘配额。
  • 属主可以 chgrp自己所属的任意组;不能把文件丢进自己不在的组里。
  • chown 出于安全考虑,会清除普通文件上的 setuid/setgid 位——重要细节:如果你 chown root 了一个 SUID 二进制,事后必须重新打上 SUID。

几个会写一百遍的常见模式:

1
2
3
sudo chown -R www-data:www-data /var/www/html       # webroot
sudo chown -R :developers       /srv/project        # 团队目录
sudo chown -R postgres:postgres /var/lib/postgresql # 数据库文件

umask:默认权限的过滤掩码

umask新 inode 创建时要从系统默认里减掉的位。系统默认是:

  • 普通文件 0666(不给 x,避免数据文件意外可执行)
  • 目录 0777(目录需要 x 才能穿过)

实际权限 = 默认 AND NOT umask

umask新文件新目录适用
022644755桌面默认;世界可读
027640750服务器 / 生产 —— 属主之外只允许同组访问
002664775用了 USERGROUPS(每个用户一个独立主组)的开发协作机
077600700最严 —— ~/.ssh、密钥目录

查看与修改:

1
2
3
umask              # 0022 —— 开头的 0 是特殊位槽,忽略它
umask 027          # 仅当前 shell
echo 'umask 027' >> ~/.bashrc   # 用户级持久化

系统级默认在 /etc/login.defsUMASK)和 /etc/profile/etc/pam.d/* 里。systemd 服务请在 unit 文件里写 UMask=不要指望它读 ~/.bashrc——服务进程从来不读。

特殊权限位:SUID、SGID、sticky

SUID / SGID / sticky 一览

chmod 实际上接受的是四位八进制。最高位打包了三个开关:4(SUID)、2(SGID)、1(sticky)。可以叠加:chmod 6755 就是 SUID + SGID + 755。

SUID(4xxx)—— 以属主身份运行

加在可执行文件上之后,进程以文件属主的有效 UID 运行,无论是谁启动的。最经典的例子:

1
2
3
$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root … /usr/bin/passwd
#    ^  属主 x 位上的 s 就是 SUID

passwd 要写 /etc/shadow0640 root:shadow),但普通用户必须能改自己的密码。SUID + 一个被仔细审计过的小程序,是这个矛盾的经典解法。

1
2
3
chmod u+s prog        # 符号
chmod 4755 prog       # 数字(4 = SUID)
chmod u-s prog        # 去掉

小写 s 表示 SUID 底下的 x 也置上了;大写 S 表示 SUID 在但 x 没有——基本一律是配错。

SUID 是真的危险。SUID-root 二进制里一个 bug 就是本地提权。要定期审计:

1
sudo find / -xdev -perm -4000 -type f 2>/dev/null

凡是不在标准列表里(passwdsudomountsu、老发行版的 ping 等)的,都得说出存在的理由。

SGID(2xxx)—— 两种用法

加在可执行文件上:进程的有效 GID 变成文件的属组。常见于需要访问某个私有组资源的工具,比如 walltty 组拥有的 /dev/tty*

加在目录上(远更常见的用法):在该目录里创建的文件和子目录自动继承该目录的属组,而不是创建者的主组。这才是搭团队共享目录的正路:

1
2
3
sudo mkdir /srv/project
sudo chown :developers /srv/project
sudo chmod 2770 /srv/project        # SGID + rwxrwx---

之后 developers 组里任何人在里面建的文件都是 group=developers,组里别的人能读能写。没有 SGID,每次创建后你都要记得 chgrp ——而人就是会忘。

Sticky bit(1xxx)—— 受限删除

加在目录上时它改写了一条规则:即使目录对所有人可写,里面的条目也只能由文件的属主(或 root)unlink 或重命名/tmp 是教科书案例:

1
2
3
$ ls -ld /tmp
drwxrwxrwt 17 root root … /tmp
#         ^ others x 位上的 t 就是 sticky

如果没有 sticky,1777/tmp 就成了任人删彼此 socket、锁文件的乱场。

1
2
chmod +t  dir         # 符号
chmod 1777 /tmp       # 数字

加在文件上的 sticky 是历史遗留(曾经表示"text 段常驻 swap”),现代 Linux 直接忽略。

常见场景 —— 可直接拷走,附原理

共享目录上的属主 / 属组 / others 决策表

1. 跑脚本提示 “Permission denied”

1
2
3
4
$ ./deploy.sh
zsh: permission denied: ./deploy.sh
$ ls -l deploy.sh
-rw-r--r-- 1 alice alice 432 Jan 18 09:14 deploy.sh

没有 x。修:

1
2
3
chmod u+x deploy.sh        # 只有自己要跑
# 或者,如果它放在共享 bin/
chmod 755 deploy.sh

如果改完仍然报 exec format error,是脚本第一行缺 shebang(如 #!/usr/bin/env bash),内核不知道用哪个解释器。

2. Web 服务器返回 403

nginx/apache 在 Debian/Ubuntu 上以 www-data 跑,在 RHEL 系上以 nginx 跑。它要的是:

  • 目标文件的 r
  • 从根到这个文件的每一段目录的 x

后面这条最容易被忽视——/home/alice/site/ 一般是 700www-data 连进都进不去。要么把 docroot 挪到 /var/www/ 下,要么给路径放行:

1
2
3
4
5
sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;
# 一行等价:
sudo chmod -R u=rwX,go=rX /var/www/html

3. 团队共享项目目录

目标:developers 组里所有人都能读写所有内容,组外完全看不到。

1
2
3
4
5
sudo mkdir /srv/project
sudo chown :developers /srv/project
sudo chmod 2770 /srv/project        # SGID + rwxrwx---
# 顺手把 umask 调一下,让新文件默认就让组内可写
echo 'umask 002' | sudo tee /etc/profile.d/team-umask.sh

2(SGID)保证继承;770 把外人挡掉;umask 002 让新文件落到 664 而不是默认的 644,组内才能改。

4. 多用户临时目录

发行版已经替你做好了——/tmp 就是 1777。如果你要类似的共享暂存区:

1
2
sudo mkdir /srv/scratch
sudo chmod 1777 /srv/scratch

5. 锁紧私钥

1
2
chmod 600 ~/.ssh/id_ed25519
chmod 700 ~/.ssh

ssh 拒绝使用任何组或世界可读的私钥——这是特性,不是 bug。

ACL:当三个桶不够用时

传统 UGO 与 ACL 扩展的对照

经典 mode 位只给三个桶。现实需求经常超出这个范围:比如"让审计员 eve 读这份报告,但她不在 developers 组里",或者"封掉一个特定外包同学对这个目录的访问"。POSIX ACL 就是干这个的。

看 ACL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ getfacl report.csv
# file: report.csv
# owner: alice
# group: developers
user::rw-
user:eve:r--           # 额外条目 —— eve 可读
group::r--
group:qa:rw-           # 额外条目 —— qa 组可读写
mask::rw-              # 额外条目的"上限",下面会讲
other::---

ls -l 末尾的 +(如 -rw-r-----+)就是 ACL 存在的可见信号。

设 ACL

1
2
3
4
5
setfacl -m u:eve:r       report.csv     # 给 eve 读
setfacl -m g:qa:rw       report.csv     # 给 qa 组读写
setfacl -m u:mallory:--- report.csv     # 显式封掉 mallory
setfacl -x u:eve         report.csv     # 移掉 eve 那条
setfacl -b               report.csv     # 全部清掉,回到纯 UGO

对目录,-R 是递归,默认 ACL 还能传给将来在里面创建的子项 —— 类似 SGID,但是按用户粒度的:

1
2
setfacl -R -m   u:alice:rwx,g:devs:rwx project/   # 应用到现有的所有内容
setfacl -R -d -m u:alice:rwx,g:devs:rwx project/  # 同时应用到将来新建的

ACL mask 这个坑

mask:: 那一行是除 user::(属主)和 other:: 之外,所有"额外条目"的有效权限上限。一个常见惊吓:在带 ACL 的文件上跑 chmod g=... 实际改的是 mask,而不是真正的 group 条目。要直接改 group 条目:

1
2
setfacl -m g::r-- report.csv    # 改的是 group 条目,不动 mask
setfacl -m m::rw  report.csv    # 改的是 mask

文件系统得挂载时支持 ACL。现代 ext4/xfs/btrfs 默认就支持,可以用 tune2fs -lmount | grep acl 确认。

chattr / lsattr:文件系统层的属性

chattr 设置的是 ext4/xfs 的文件属性,位于权限系统之下——对 root 也生效。

1
2
3
4
5
6
7
sudo chattr +i /etc/resolv.conf      # immutable:谁都改不了、删不了、重命名不了
sudo lsattr /etc/resolv.conf         # ----i---------------- /etc/resolv.conf
sudo chattr -i /etc/resolv.conf      # 要编辑前先去掉

sudo chattr +a /var/log/audit.log    # append-only:不能截断、不能覆盖
echo entry >> /var/log/audit.log     # OK
echo entry >  /var/log/audit.log     # OPERATION NOT PERMITTED

+i 适合钉死关键配置(/etc/fstab/etc/passwd/etc/sudoers),让一次手抖的 sed -i 不至于把救援启动也毁掉。+a 适合钉日志文件,让事后篡改更难。两者都是回答"怎么防止 root 不小心删掉这个"的正解——不先 chattr -i,root 也删不掉。

排障清单

按下面的顺序走,能覆盖大约 9 成的权限问题。

Step 1 —— 我究竟是谁?

1
2
3
whoami                              # 交互 shell
id                                  # 顺便列出我属于哪些组
ps -o user,uid,gid,cmd -p $(pidof nginx)   # 服务的话,看真正运行身份

如果"消费者"是个服务,相关身份是 systemd unit 里的 User= / Group=,不是你登录的那个。

Step 2 —— 检查路径上的每一段目录

只有叶子文件的 r+x 是不够的;内核会对路径上的每一段重新检查 x。最快的工具是 namei

1
2
3
4
5
6
7
$ namei -l /var/www/html/index.html
f: /var/www/html/index.html
drwxr-xr-x root     root     /
drwxr-xr-x root     root     var
drwxr-xr-x root     root     www
drwxr-x--- alice    alice    html        # <-- others 没有 x;www-data 在这里就是 others
-rw-r--r-- alice    alice    index.html

第一行"相关身份缺 x“的目录就是元凶。

Step 3 —— 是不是 ACL?属性?挂载选项?

1
2
3
getfacl  file        # 是不是有个 + ACL 在背后改写规则?
lsattr   file        # 是不是被 +i 或 +a 钉住了?
mount | grep ' on / '   # 是不是 ro 挂的?noexec?nosuid?

/tmp 如果挂了 noexec,无论你 chmod 怎么改,./script.sh 都会被默默拒绝。

Step 4 —— SELinux / AppArmor

RHEL/Fedora/CentOS 上,getenforceEnforcing?那就 ls -lZ file 看 SELinux 标签,audit2why 解释最近的拒绝。Ubuntu 上,aa-status 列出 AppArmor 配置。这些机制可以在经典权限"看上去允许"的情况下照样拒绝。

常见症状对照

已知没问题的脚本仍然 Permission denied → 缺 x、缺 shebang,或者在 noexec 挂载点上。

Web 服务器 403www-data 在哪都是 others;用 namei -l 找路径上缺 x 的那一段。

rm: cannot remove ...: Operation not permitted(注意:不是 Permission denied)→ lsattr 会显示 +i;先 chattr -i

居然能写一个不属于自己的文件 → 看父目录的 w。文件属主对删除/重命名不重要。

心智模型与延伸阅读

把下面三条嚼透,几乎所有真实情况都能扛过去:

  1. 文件 vs 目录:在文件上 x 是"它是不是程序”。在目录上 x唯一通向内容的闸门。
  2. 首匹配级联:owner 或 group 或 others——永远不是相加。审计的时候问自己"调用者落进哪个桶?"
  3. 特殊位各司其职:SUID = “让无权调用者去做某件被精确审计过的特权操作”;目录上的 SGID = “团队目录”;sticky = “公共可写但不能互删”。在这些场景之外,不要乱开。

继续往深里走的方向:

  • man 1 chmodman 2 chmodman 5 aclman 1 chattr —— 权威来源,意外地好读。
  • 《Linux 用户管理》(本系列下一篇)—— /etc/passwd/etc/shadow、组、sudoers、PAM。
  • 《Linux 管道与重定向》 —— 在文件描述符与 stdin/stdout/stderr 之上继续搭。
  • 强制访问控制(MAC)框架:SELinux(RHEL 系)和 AppArmor(Debian/SUSE 系)在我们这里讲的自主访问控制之上再加一层。同样的问题,不同的答案。

如果现在让你看 drwxr-s---+ 4 alice developers 这一行,你能立刻说出属主、属组、设了哪个特殊位、还有一份 ACL 在场,并且说出 “developers 组里的 bob” 和 “组外的 eve” 各自能做什么——那模型就在你脑子里了,剩下就是肌肉记忆。

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub