0.4 权限与安全模型
rwx 之外还有哪些位?目录的 x 与 r 差在哪?umask 怎样算出默认权限?为什么 setuid 危险,ACL 与 capability 如何替代它?sudo 的边界应该画在哪里?
用十二位 mode 解释任意 ls -l 输出;按「属主 → ACL → 组 → 其他」的顺序判定访问;用 ACL 做多人共享,用 capability 替代不必要的 root;审计 setuid 与全局可写文件;写出最小权限的 sudo 规则。
上一章反复出现 -rw-r--r--,但只把它当成一列。这一章把它拆开:九个权限位加三个特殊位,配合文件的 UID/GID、进程的 euid/egid、补充组、ACL 与 capability,共同决定「这次 open 能不能成功」。
Linux 的权限模型有两层。第一层是传统的 DAC(discretionary access control):文件属主自己决定谁能读写。第二层是 LSM(Linux Security Modules,如 AppArmor、SELinux):系统策略在 DAC 之后再判一次。本章主要讲 DAC,因为日常排障 90% 的 Permission denied 出在这一层;LSM 只在第 3 篇展开。
权限问题的难点不在记八进制,而在判定顺序与边界。同一个文件,属主、组成员、ACL 用户、root、带 capability 的程序,看到的结论可能不同。把顺序记错,就会在错误的方向上改权限。
0.4.1 一次 Permission denied 的判定顺序
进程访问文件时,内核用进程的有效 UID/GID(euid/egid)与补充组(supplementary groups),对照文件的 mode、ACL 与属主关系,依次判断:root 特殊处理 → 属主 → 具名 ACL 用户 → 组类(含 mask)→ 其他。第一个命中的类别决定结果,不再往下看。
判定顺序可以压缩成一张图:
open("/srv/data/report.csv", O_RDONLY)
│
├─ euid == 0 ? ──是──→ 允许(普通文件仍需至少一个 x 位才可执行)
│
├─ euid == 文件属主 ? ──是──→ 看 owner 三位(或 ACL user::)
│
├─ ACL 中有 euid 的具名条目 ? ──是──→ 看该条目 & mask
│
├─ egid/补充组匹配属组,或 ACL 具名组匹配 ? ──是──→ 看 group 三位(或条目 & mask)
│
└─ 其他 ──→ 看 other 三位
│
└─ 通过 DAC 后,LSM(AppArmor/SELinux)再判一次
这张图的读法是:类别匹配优先于权限位。如果进程是文件属主,就只看 owner 三位,即使 group 或 other 给了更多权限也不生效。一个常见现象:文件 ----r----- alice dev,alice 是属主且在 dev 组里,她仍然读不了——因为属主类别先命中,owner 是 ---。
root 也不是无条件。root 可以绕过读写权限,但对普通文件执行时,仍要求至少一个执行位;只读文件系统、immutable 属性(chattr +i)、LSM 策略都能挡住 root。sudo 拿到的是 root 的 euid,不是「绕过所有检查」的魔法。
排查权限,先看路径上每一级目录的 x:
namei -l /srv/data/report.csv
# f: /srv/data/report.csv
# drwxr-xr-x root root /
# drwxr-xr-x root root srv
# drwxrwx--- root data data
# -rw-r----- alice data report.csv
namei -l 逐段列出目录权限。上例中,如果调用者不在 data 组,即使文件 other 有读权限,也会在 /srv/data 这一级被拦住——目录没有 x,就无法到达里面的 inode。这是「文件权限明明有,为什么还是 denied」的第一大原因。
0.4.2 mode 的十二位
ls -l 第一列有十个字符:第一个是类型,后九个是权限。stat -c %a 给八进制,可能多出一位特殊位。
| 位(八进制) | 名称 | 对文件 | 对目录 |
|---|---|---|---|
4000 | setuid | 执行时 euid 变为文件属主 | 在部分系统上有特殊语义,Linux 上一般忽略 |
2000 | setgid | 执行时 egid 变为文件属组 | 新建文件继承目录的组;新建子目录继承 setgid |
1000 | sticky | 历史上表示「常驻内存」,现代 Linux 忽略 | 只有文件属主、目录属主或 root 能删除/重命名其中的文件 |
0400/0200/0100 | owner r/w/x | 属主读/写/执行 | 属主列出/增删/进入 |
0040/0020/0010 | group r/w/x | 属组读/写/执行 | 属组列出/增删/进入 |
0004/0002/0001 | other r/w/x | 其他人读/写/执行 | 其他人列出/增删/进入 |
九位权限的八进制换算是把每组 rwx 看成三位二进制:r=4、w=2、x=1。rwxr-x--- = 750;rw-r--r-- = 644;rwxrwxrwt = 1777。
特殊位在 ls -l 里的显示规则:
| 有执行位 | 无执行位 | 含义 |
|---|---|---|
s(owner) | S | setuid |
s(group) | S | setgid |
t(other) | T | sticky |
stat -c '%A %a %n' /usr/bin/passwd /tmp /usr/bin/wall
# -rwsr-xr-x 4755 /usr/bin/passwd
# drwxrwxrwt 1777 /tmp
# -rwxr-sr-x 2755 /usr/bin/wall
/usr/bin/passwd 的 s 在 owner 位:普通用户执行它时,euid 变成 root,才能写 /etc/shadow。/tmp 的 t 在 other 位:任何人都能在里面建文件,但只有属主能删自己的文件。wall 的 s 在 group 位:执行时 egid 变成 tty 组,才能写终端设备。
0.4.3 目录的 r、w、x 是三种不同能力
目录也是文件,但权限含义不同:
| 权限 | 允许 | 不允许 |
|---|---|---|
r | 列出目录项名字(ls dir) | 看不到名字,但知道路径仍可能访问 |
x | 进入目录、访问其中的 inode(cd dir、cat dir/file) | 无法到达目录内的任何文件 |
w | 创建、删除、重命名目录项 | 不能修改目录项 |
三个组合的典型现象:
- 只有
r没有x:ls dir能列出名字,但ls -l dir看不到属性,cat dir/file报Permission denied。这常被用来「隐藏内容但让人知道有哪些文件」,实际很少用。 - 只有
x没有r:不能ls,但如果知道文件名,可以cat dir/known-file。这就是「目录的 x 是通行证」。 - 有
w没有x:不能创建或删除,因为无法到达目录 inode;w必须与x一起才有意义。
删除文件看的是目录的写权限,不是文件的写权限。一个只读文件放在可写目录里,仍然可以被删除(rm 会提示,但 rm -f 直接删)。反过来,文件可写但目录不可写,删不掉。这是「为什么我不能删自己没权限的文件」的答案。
sticky 位把「删除权」从目录属主扩展到文件属主:/tmp 是 1777,任何人都能创建,但只有文件属主、目录属主(root)或 root 能删除别人的文件。共享目录需要「大家都能建,但不能互删」时,用 1777 或 2770 + ACL。
0.4.4 umask 与默认权限
新建文件的权限不是「你想要的」,而是「默认值减去 umask」。规则:
- 普通文件默认
666(不给执行位); - 目录默认
777(需要执行位才能进入); - 最终权限 =
默认值 & ~umask。
umask 是进程属性,Shell 内建命令修改的是当前 Shell 及其子进程的掩码。常见值:
| umask | 文件 | 目录 | 适用 |
|---|---|---|---|
022 | 644 | 755 | 系统默认,其他用户可读 |
027 | 640 | 750 | 团队共享但不对其他人开放 |
077 | 600 | 700 | 私有,仅属主 |
002 | 664 | 775 | 协作目录,同组可写 |
$ id uid=1000(wang) gid=1000(wang) groups=1000(wang),27(sudo) $ umask 0002 $ umask -S u=rwx,g=rwx,o=rx $ mkdir -p /tmp/perm-lab && cd /tmp/perm-lab $ touch default.txt && mkdir default.dir $ ls -ld default.txt default.dir -rw-rw-r-- 1 wang wang 0 Oct 17 11:02 default.txt drwxrwxr-x 2 wang wang 4096 Oct 17 11:02 default.dir $ chmod 640 default.txt $ chmod u+x,g-w,o= default.txt $ ls -l default.txt -rwxr----- 1 wang wang 0 Oct 17 11:02 default.txt $ namei -l /etc/shadow f: /etc/shadow drwxr-xr-x root root / drwxr-xr-x root root etc -rw-r----- root shadow shadow
umask 0002 下,文件得到 664、目录得到 775。chmod 640 把文件设为 rw-r-----;chmod u+x,g-w,o= 再改成 rwxr-----。符号模式的三段分别对应 u/g/o,+/-/= 是加、减、精确设置。
umask 只影响新建,不改已有文件。要改已有目录树,用 chmod -R 或 find -exec chmod,但要想清楚:递归改权限会覆盖掉有意设置的 ACL 与特殊位。
系统级默认在 /etc/login.defs(UMASK)、PAM(pam_umask)与 systemd 的 UMask= 里。Ubuntu 普通用户常见 0002,root 是 0022。服务由 systemd 启动时,继承 unit 的 UMask=(默认 0022),不继承登录用户的 umask。
0.4.5 chmod、chown 与特殊位
chmod 有两种语法:
chmod 750 file # 八进制,精确设置
chmod u=rwx,g=rx,o= file # 符号,按类别设置
chmod g+w,o-rx file # 符号,增量修改
chmod -R u+rwX,go-w dir # X:只给目录和已有执行位的文件加 x
chmod --reference=ref file # 复制另一个文件的 mode
大写 X 很实用:递归给目录加执行位,但不会把普通文本文件变成可执行。chmod -R a+rX 是共享只读目录的常见写法。
chown 改属主,chgrp 改属组,chown user:group 一次改两个:
sudo chown alice:dev /srv/app/config.yml
sudo chown -R www-data:www-data /var/www/site
sudo chgrp -R dev /srv/shared
风险与回滚:
chown -R会跟随命令行给出的符号链接,但默认不跟随目录内部的符号链接(GNU chown 默认-P)。要明确行为,用-h(改链接本身)或-L(跟随所有链接)。- 递归改属主会覆盖掉原本属于其他服务的文件,导致服务起不来。先
find /var/www/site ! -user www-data -print看会影响哪些文件。 - 回滚要记录原始属主:
find ... -printf '%u:%g %p\n' > owners.bak,需要时按文件恢复。
setuid/setgid 的写法:
chmod u+s /usr/local/bin/tool # setuid
chmod g+s /srv/shared # 目录 setgid:新文件继承组
chmod +t /srv/shared # sticky
chmod 2770 /srv/shared # setgid + rwxrwx---
chmod 4755 /usr/local/bin/tool # setuid + rwxr-xr-x
目录 setgid 是团队协作最有用的特殊位:在 /srv/shared 下新建的文件自动属于 dev 组,不需要每个用户手动 chgrp。配合 umask 002 或默认 ACL,团队目录就能「同组可写、其他人不可见」。
setuid 为什么危险:它把「执行这个程序」变成「以文件属主身份执行」。程序里任何一个漏洞(缓冲区溢出、命令注入、路径穿越)都会直接变成属主权限。历史上大量本地提权漏洞来自 setuid 程序。现代做法是尽量不用 setuid root,改用:
- 细粒度 capability(下一节);
- systemd 的
User=、DynamicUser=、AmbientCapabilities=; - 需要特权的操作拆成一个小服务,通过 socket 或 D-Bus 暴露有限接口。
0.4.6 ACL:给「某个人」单独开权限
传统 mode 只有三类人:属主、属组、其他。要允许 alice 读写、bob 只读、其他人不可见,又不想建一堆组,就需要 ACL。
ACL 是挂在 inode 上的一组扩展条目,除 owner/group/other 外,还可以有具名用户(user:alice:rw-)与具名组(group:dev:r-x)。mask 是组类权限的上限,限制所有具名用户、具名组与 group:: 的有效权限;owner 与 other 不受 mask 影响。ls -l 中组三位显示的就是 mask。
# 允许 alice 读写,bob 只读
setfacl -m u:alice:rw-,u:bob:r-- report.csv
# 查看;-n 用数字显示 uid/gid(-p 是保留绝对路径的前导斜杠)
getfacl -n report.csv
# file: report.csv
# owner: wang
# group: wang
# user::rw-
# user:alice:rw-
# user:bob:r--
# group::r--
# mask::rw-
# other::---
# ls 的组权限位现在是 mask,末尾有 +
ls -l report.csv
# -rw-rw----+ 1 wang wang 0 Oct 17 11:10 report.csv
mask::rw- 是当前 ACL 的上限。如果 bob 的条目是 r--,有效权限是 r-- & rw- = r--;如果 alice 是 rwx,有效权限被 mask 压成 rw-。要放开,显式改 mask:
setfacl -m m::rwx report.csv # 放开 mask
setfacl -m m::r-- report.csv # 收紧 mask,具名用户最多只读
getfacl 的 effective: 行会显示某条目经过 mask 后的实际权限。排查「ACL 明明给了 r,为什么还是 denied」时,先看 mask。
目录的默认 ACL 让新建文件自动继承:
# 目录本身:dev 组可读写执行
setfacl -m g:dev:rwx /srv/shared
# 默认 ACL:以后在目录里新建的文件/子目录都继承
setfacl -d -m g:dev:rwx /srv/shared
setfacl -d -m u:alice:rwx /srv/shared
getfacl -n /srv/shared
# default:user::rwx
# default:user:alice:rwx
# default:group::r-x
# default:group:dev:rwx
# default:mask::rwx
# default:other::---
默认 ACL 只影响新建对象,不改变已有文件。批量修复已有文件用 setfacl -R -m ...,但要注意 -R 会递归到所有子目录。
ACL 的运维细节:
- 备份与恢复:
getfacl -R /srv/shared > acls.bak,恢复用setfacl --restore=acls.bak。tar --acls、rsync -A也能保留。 - 删除 ACL:
setfacl -b file删掉所有扩展条目与 mask;setfacl -x u:alice file只删一条。 - 复制:
cp -p会保留 ACL;cp -a也会;普通cp不保留。 - 文件系统支持:ext4、XFS、Btrfs 默认支持;tmpfs 支持;NFS 需要版本与挂载选项配合。
tune2fs -l的Default mount options里若有acl,说明默认启用。
0.4.7 capability:把 root 拆成小块
root 的权限被拆成几十个 capability。一个程序不需要完整的 root,只需要其中一两项时,用 file capability 把权限绑定到可执行文件上。
最常见的例子是「让普通程序监听 80 端口」:
# 需要 root;先确认二进制是自己编译/可信的
sudo setcap cap_net_bind_service=+ep /usr/local/bin/web
getcap /usr/local/bin/web
# /usr/local/bin/web cap_net_bind_service=ep
# 回滚:删除文件上的 capability
sudo setcap -r /usr/local/bin/web
+ep 表示 permitted 与 effective 都置位。只写 +p 时,程序需要通过 cap_set_proc 自己把 capability 提到 effective;大多数程序不会做,所以 +ep 更常见。
查看系统里已有的 file capabilities:
getcap -r /usr/bin /usr/sbin 2>/dev/null | head
# /usr/bin/ping cap_net_raw=ep
# /usr/bin/mtr-packet cap_net_raw=ep
Ubuntu 的 ping 不再 setuid root,而是带 cap_net_raw,这就是「最小权限」的实例:只需要发原始 ICMP 包,不需要读 /etc/shadow、不需要挂载文件系统。
capability 的边界:
- 只对 ELF 可执行文件可靠。脚本由解释器执行,file capability 不会按你的预期附加到脚本上;需要时用一个小 C 包装器,或 systemd 的
AmbientCapabilities=。 NoNewPrivileges=yes会让 execve 忽略 setuid 与 file capability。这是 systemd 服务加固的常用项,和安全目标一致,但会让依赖 setuid 的旧程序失效。- capability 不是沙箱。
cap_sys_admin仍然接近 root;cap_dac_override可以绕过所有文件读权限。只授予真正需要的那一项。 - 容器默认丢弃大部分 capability。Docker 默认保留的集合里没有
CAP_SYS_ADMIN,所以容器里mount通常失败;需要时显式--cap-add,并明白这意味着什么。
查看当前进程的 capability:
getpcaps $$
# 4127: cap_chown,cap_dac_override,...=ep (root 登录时)
# 普通用户的 Shell 通常接近空集
capsh --print | sed -n '1,12p'
0.4.8 账号文件与 sudo 的边界
三个文件定义「你是谁」:
| 文件 | 一行例子 | 字段 |
|---|---|---|
/etc/passwd | wang:x:1000:1000:Wang:/home/wang:/bin/bash | 用户名、密码占位、UID、GID、注释、主目录、登录 Shell |
/etc/shadow | wang:$y$j9T$...:20000:0:99999:7::: | 用户名、哈希、最后修改天数、最小/最大天数、警告天数、不活跃天数、过期日、保留 |
/etc/group | sudo:x:27:wang,alice | 组名、密码占位、GID、成员列表 |
读这些文件用 getent,不要直接 grep:getent 会走 NSS,能查到 LDAP、SSSD、systemd-homed 提供的用户;直接读文件会漏掉网络账号。
getent passwd wang
getent group sudo
id wang
UID 0 是 root;UID 1–999 通常留给系统服务;普通用户从 1000 开始(Ubuntu 的 /etc/login.defs 里 UID_MIN)。把普通用户改成 UID 0 等价于再造一个 root,不要做。
sudo 的配置文件是 /etc/sudoers 与 /etc/sudoers.d/*。只能用 visudo 编辑,它会在保存前做语法检查;直接 vim 写错一个字符,可能让所有 sudo 失效。
最小规则示例:
# /etc/sudoers.d/deploy
# 允许 deploy 用户重启指定服务,不需要密码
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service
# 允许运维组执行所有命令,但需要密码
%ops ALL=(ALL:ALL) ALL
NOPASSWD 的风险不是「不用输密码」这么简单:任何能修改这条命令参数的人,都可能把受限命令变成任意命令。例如允许 systemctl restart *,就能重启任意 unit;允许 vim,就能在 vim 里 :!sh 拿 root shell;允许 tar,就能用 --checkpoint-action=exec 执行命令。sudo 的命令限制不是沙箱,只是防误操作。需要强隔离时,用专用服务、容器或 systemd 沙箱。
sudo 的常用形式:
sudo -l # 看当前用户被允许什么
sudo -u postgres psql # 以指定用户执行
sudo -i # 登录式 root Shell(读 root 的 profile)
sudo -s # 非登录 root Shell
sudoedit /etc/nginx/nginx.conf # 用编辑器改文件,避免 sudo vim
sudo -i 与 su - 的区别:前者按 sudoers 授权、默认重置环境;后者需要目标用户密码(root 密码),并读目标用户的登录文件。生产环境更推荐 sudo,因为它的授权可审计、可细粒度。
账号文件不要用手改。日常增删用户/组用下面这组命令,它们会同时维护 /etc/passwd、/etc/shadow、/etc/group 与家目录:
| 目的 | 命令 | 注意 |
|---|---|---|
| 交互式建用户(Ubuntu) | sudo adduser alice | 会提示设密码、建家目录、复制 /etc/skel |
| 脚本化建用户 | sudo useradd -m -s /bin/bash -G dev alice | -m 建家目录;-G 是附加组,-g 是主组 |
| 改附加组 | sudo usermod -aG dev alice | -a 不能省;只写 -G 会替换掉原有附加组 |
| 删用户 | sudo userdel -r alice | -r 连家目录和邮件池一起删;不带 -r 会留下文件 |
| 建组/加成员 | sudo groupadd dev、sudo gpasswd -a alice dev | gpasswd -d 移除成员 |
| 锁密码/解锁/强制改密 | sudo passwd -l alice、sudo passwd -u alice、sudo passwd -e alice | -l 只锁密码,SSH 公钥仍可能登录 |
| 密码有效期 | sudo chage -l alice、sudo chage -M 90 alice | -M 最大天数,-E 账号过期日 |
两个高频事故:usermod -G dev alice 会把 alice 从 sudo 等其他附加组里踢出去,必须写 -aG;passwd -l 锁的是密码哈希,已经写入 ~/.ssh/authorized_keys 的公钥不受影响,要真正禁止登录,把 shell 改成 /usr/sbin/nologin(usermod -s)或用 chage -E 0 让账号过期。需要直接编辑账号文件的救援场景,用 vipw/vigr 而不是 vim,它们会加锁并做一致性检查。
- 「
chmod 777能解决所有权限问题」——它让所有人可读可写可执行,既扩大攻击面,也可能让程序拒绝启动(如 SSH 私钥要求600)。先判断是哪一类权限缺失。 - 「root 什么都能做」——root 受只读文件系统、immutable 属性、LSM 策略、capability 边界限制;容器里的 root 还受 seccomp 与 capability 集合限制。
- 「ACL 是 mode 之外的独立机制」——mode 的 group 三位在 ACL 存在时显示 mask,二者是同一套判定的一部分。改
chmod g+rw可能同时改 mask。 - 「给脚本 setuid 就能提权」——Linux 忽略脚本上的 setuid 位,解释器不会以脚本属主身份运行。需要提权时用二进制包装器或 systemd。
- 「
NOPASSWD只是省一次输入」——它意味着任何能触发该命令的路径都以 root 执行;命令参数可控时,等于给了 root。限制越具体越好,能不用就不用。
0.4.9 权限审计脚本
下面这个脚本做三件低风险检查:setuid/setgid 文件、全局可写且无 sticky 的目录、无属主文件。它只读,不修改系统。
#!/usr/bin/env bash
# audit-perms.sh — 只读权限审计;需要 root 才能扫全盘
set -euo pipefail
echo "== setuid/setgid 可执行文件 =="
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f \
-printf '%M %u:%g %p\n' 2>/dev/null | sort
echo "== 全局可写且无 sticky 的目录 =="
find / -xdev -type d -perm -0002 ! -perm -1000 \
-printf '%M %u:%g %p\n' 2>/dev/null | sort
echo "== 无属主或无属组的文件 =="
find / -xdev \( -nouser -o -nogroup \) -printf '%u:%g %p\n' 2>/dev/null | head -50
echo "== 最近 7 天被改过权限的文件(ctime) =="
find /etc /usr/local -xdev -type f -ctime -7 \
-printf '%TY-%Tm-%Td %TH:%TM %M %p\n' 2>/dev/null | sort
-xdev 保证不跨文件系统,避免扫到 NFS 或 /proc。-perm -4000 是「至少包含 setuid 位」,不是精确等于;精确匹配用 -perm 4755。! -perm -1000 表示「没有 sticky 位」。
结果怎么读:
- setuid root 的程序越少越好。每个都要能解释「为什么需要」,否则考虑用 capability 替代。
- 全局可写目录如果没有 sticky,任何用户都能删除别人的文件;
/tmp是1777,有 sticky,正常。 - 无属主文件通常是删用户后留下的,可能被新用户继承 UID,带来安全风险;确认后
chown或删除。
- 确认身份:
id、sudo -l,确认 euid/egid 与补充组。 - 确认路径:
namei -l /path/to/file,逐级看目录的x。 - 确认判定类别:
stat -c '%A %a %U:%G %n' file,看你是属主、组还是其他。 - 确认 ACL:
getfacl -p file,看具名条目与 mask;ls -l末尾是否有+。 - 确认 LSM:
sudo aa-status或getenforce,看是否被 AppArmor/SELinux 拒绝;日志在dmesg或journalctl -k。 - 最后才改权限:用最小改动(加组、加 ACL、加 capability),改完记录回滚方式。
0.4.10 mode 的位级结构与内核判定路径
ls -l 的十二个位只是 st_mode 的低位。内核真正比较的是 S_IFMT 掩码下的类型位与三组权限位,判定函数在 fs/namei.c、fs/inode.c、fs/posix_acl.c 里。把位与函数对上,才能解释「为什么 root 也执行不了」「为什么 ACL 改了 mask 反而更严」。
st_mode 的布局(Linux,include/uapi/linux/stat.h):
| 位(八进制) | 宏 | 含义 |
|---|---|---|
0170000 | S_IFMT | 类型掩码 |
0140000 | S_IFSOCK | Unix socket |
0120000 | S_IFLNK | 符号链接 |
0100000 | S_IFREG | 普通文件 |
0060000 | S_IFBLK | 块设备 |
0040000 | S_IFDIR | 目录 |
0020000 | S_IFCHR | 字符设备 |
0010000 | S_IFIFO | FIFO |
0004000 | S_ISUID | setuid |
0002000 | S_ISGID | setgid |
0001000 | S_ISVTX | sticky |
0000700 | S_IRWXU | owner rwx |
0000070 | S_IRWXG | group rwx |
0000007 | S_IRWXO | other rwx |
/* 判断类型与权限,不依赖 ls 的输出格式 */
if (S_ISREG(st.st_mode) && (st.st_mode & S_IXUSR)) { /* 属主可执行 */ }
if ((st.st_mode & 07777) == 04755) { /* 精确 setuid+755 */ }
printf("%04o\n", st.st_mode & 07777);
chmod 4755 写入的就是 04000 | 0755。stat -c %f 给十六进制原始值,%a 给八进制权限;两者都不含类型位,%f 含类型位(81a4 = 0100644)。
访问判定的内核路径:
open/openat
└─ do_filp_open → path_openat → link_path_walk
└─ may_open / inode_permission(inode, MAY_READ|MAY_WRITE|MAY_EXEC)
└─ do_inode_permission
├─ 只读文件系统/immutable/append-only 检查
├─ generic_permission
│ ├─ acl_permission_check(有 ACL 时)
│ │ ├─ owner 匹配 → owner 位
│ │ ├─ ACL_USER 匹配 → 条目 & mask
│ │ ├─ group/ACL_GROUP 匹配 → 组位 & mask
│ │ └─ other 位
│ └─ CAP_DAC_OVERRIDE / CAP_DAC_READ_SEARCH 兜底
└─ LSM hook(AppArmor/SELinux)
几个容易漏掉的点:
- owner 类别先命中就结束。文件属主即使属于某个有更多权限的组,也只看 owner 三位;ACL 的
user::条目与 owner 位是同一类。 - root 的绕过有条件。
CAP_DAC_OVERRIDE可以绕过读写,但执行普通文件仍要求「至少一个 x 位」;CAP_DAC_READ_SEARCH只覆盖读与目录搜索。只读挂载、immutable、LSM 都能挡住 root。 - 目录搜索是
MAY_EXEC。路径上每一级目录都要有x;缺少x时,即使目标文件是 777,也在中途返回EACCES。 - ACL 存在时组位显示 mask。
ls -l的 group 三位不再直接代表group::,而是 mask;getfacl才能看到原始条目。 - protected_* 系列 sysctl 会在判定前加限制。Ubuntu 默认开启
fs.protected_hardlinks=1、fs.protected_symlinks=1、fs.protected_fifos=2、fs.protected_regular=2,在全局可写 sticky 目录里阻止「跟随他人符号链接」「写他人文件」等经典攻击。
sysctl fs.protected_hardlinks fs.protected_symlinks fs.protected_fifos fs.protected_regular
# fs.protected_hardlinks = 1
# fs.protected_symlinks = 1
# fs.protected_fifos = 2
# fs.protected_regular = 2
这些保护只作用于「全局可写且带 sticky」的目录(典型是 /tmp)。它们解释了为什么某些在旧教程里可行的 /tmp 攻击现在失效,也解释了为什么在关闭它们的系统上,脚本的临时文件必须用 mktemp。
内核判定用的是进程的有效 UID/GID(current_euid()/current_egid())与补充组(in_group_p() 遍历 group_info),不是 real UID。setuid 程序执行后 euid 变成文件属主,判定就按新 euid;sudo 的 euid 是 0。文件的凭证是 st_uid/st_gid 与 ACL 条目;两者匹配的类别决定用哪组位。
0.4.11 目录、sticky 与删除/重命名的判定
目录的 r、w、x 是三件独立的事,删除与重命名又额外受父目录与 sticky 约束。把它们混在一起,就会出现「文件是我的,为什么删不掉」「目录能进,为什么 ls 不了」。
| 操作 | 需要的目录权限 | 需要的文件权限 | 额外检查 |
|---|---|---|---|
ls dir | r | — | 无 |
ls -l dir | r + x | — | 对每个条目 stat 需要 x |
cat dir/file | 路径上每级 x | 文件 r | 无 |
创建 dir/new | w + x | — | 目录不可 immutable |
删除 dir/file | w + x | 不需要文件写权限 | sticky 检查 |
重命名 dir/a | 源与目标父目录都 w + x | — | sticky 检查、跨挂载 EXDEV |
| 修改文件内容 | 路径 x | 文件 w | O_APPEND/锁等另算 |
「删除看目录写权限」是最反直觉的一条:一个 0444 的只读文件放在 0777 目录里,任何有目录写权限的人都能 rm 它;反过来,文件 0666 但目录 0555,谁也删不掉。rm 提示 remove write-protected regular file? 只是交互确认,不是内核拒绝。
sticky 位(S_ISVTX,八进制 1000)把删除/重命名的决定权从「目录写权限」收紧为:
- 文件属主可以删/改名自己的文件;
- 目录属主可以删/改名目录内任何文件;
- root(更准确地说,
CAP_FOWNER)可以删/改名任何文件; - 其他有目录
w的用户只能删自己的文件。
内核函数是 may_delete() 与 check_sticky()(fs/namei.c)。/tmp 的 1777 就是「所有人能建,不能互删」。共享目录要「同组可写、组内可互删、组外不可见」,用 2770 + setgid,而不是 1777;1777 允许任何人创建,只是不能删别人的。
rename 比 unlink 多一层:源父目录与目标父目录都要 w + x;如果目标是已存在文件,还要通过目标父目录的 sticky 检查。跨文件系统返回 EXDEV;把一个目录重命名到自己的子目录返回 EINVAL;目标是非空目录返回 ENOTEMPTY。renameat2(RENAME_NOREPLACE) 在目标存在时返回 EEXIST,不覆盖。
chmod +t 只对目录有意义;对普通文件设置 sticky 位在内核里被忽略(历史上表示「常驻 swap」,现代 Linux 不再使用)。ls -l 的 t/T 只反映目录的 sticky。
- 「目录的
r就能读文件」——r只允许列名字;读文件要路径上的x与文件的r。 - 「目录的
w就能删文件」——还需要x到达目录 inode;只有w没有x时创建/删除都失败。 - 「文件只读就删不掉」——删除看父目录写权限,不看文件 mode。
- 「
/tmp是 777,所以能删别人的文件」——sticky 位阻止;能删的是自己的文件。 - 「
chmod -R会把目录的x一起去掉」——chmod -R a-w会去掉目录写位但仍保留x;用chmod -R a-x才会让目录不可进入。共享目录用a+rX而不是a+rx。
0.4.12 umask 的计算表与三个来源
新建文件的权限由「请求 mode」与「umask」共同决定:最终权限 = mode & ~umask。普通文件请求 0666,目录请求 0777,所以 umask 直接决定默认结果:
| umask | 文件 666 & ~umask | 目录 777 & ~umask | umask -S | 典型场景 |
|---|---|---|---|---|
022 | 644 | 755 | u=rwx,g=rx,o=rx | root、系统服务 |
002 | 664 | 775 | u=rwx,g=rwx,o=rx | Ubuntu 普通用户(UPG) |
027 | 640 | 750 | u=rwx,g=rx,o= | 团队共享、不对其他 |
077 | 600 | 700 | u=rwx,g=,o= | 私有、临时目录 |
007 | 660 | 770 | u=rwx,g=rwx,o= | 完全信任同组 |
000 | 666 | 777 | u=rwx,g=rwx,o=rwx | 仅测试,危险 |
计算时用八进制按位与:0666 & ~0022 = 0644,0777 & ~0027 = 0750。umask 输出四位(0022),第一位是特殊位掩码;umask 022 与 umask 0022 等价。
umask 是进程属性,不是文件属性。它影响 open(O_CREAT)、mkdir、mkfifo、mknod 等创建操作,不影响 chmod、chown 或已存在的文件。子进程继承父进程的 umask;Shell 内建 umask 修改当前 Shell,之后启动的命令都继承。
三个来源按优先级叠加:
| 来源 | 位置 | 作用范围 | 备注 |
|---|---|---|---|
| 登录默认 | /etc/login.defs 的 UMASK | 登录会话 | Debian/Ubuntu 常被 PAM 覆盖 |
| PAM | /etc/pam.d/common-session 的 pam_umask.so | 登录/ssh 会话 | 读 UMASK、USERGROUPS_ENAB |
| Shell 启动文件 | ~/.profile、~/.bashrc | 交互 Shell | 直接 umask 077 |
| systemd | unit 的 UMask= | 服务进程 | 默认 0022,不继承登录用户 |
| 程序自身 | open(..., 0600) | 该文件 | 仍会被 umask 削掉位 |
Ubuntu 普通用户常见 0002,原因是 USERGROUPS_ENAB yes:每个用户有同名主组,PAM 把 umask 设为 002,让同组(通常只有自己)可写。root 是 0022。
umask # 0002
umask -S # u=rwx,g=rwx,o=rx
( umask 077; touch /tmp/private && ls -l /tmp/private ) # -rw-------
ls -l /tmp/private # 子 Shell 的 umask 不影响父 Shell;文件仍是 600
mkdir -m 与 install -m 会在创建后显式 chmod,不受 umask 影响:umask 022 下 mkdir -m 777 dir 得到 777。相反,程序直接调用 open(..., 0666) 或 mkdir(..., 0777) 且不额外 chmod 时,mode 会被 umask 削掉;touch 就是后者。touch 没有 -m 选项改 mode,它按 0666 & ~umask 创建;需要精确权限用 install -m 600 /dev/null file。
默认 ACL 与 umask 的交互要单独说:父目录有默认 ACL 时,新建对象从默认 ACL 继承条目,内核仍用请求 mode(已应用 umask)去裁剪,最终通常是「默认 ACL 与 umask 的交集」。所以「设了默认 ACL 就不用管 umask」是错的;团队目录应同时设 umask 002(或 027)与默认 ACL,避免新文件出现意外的 other 权限。
- 定组:
groupadd dev,把成员加入dev(usermod -aG dev alice)。 - 定目录:
install -d -m 2770 -g dev /srv/shared;2770= setgid +rwxrwx---。 - 定默认 ACL:
setfacl -d -m g:dev:rwx,m::rwx /srv/shared。 - 定 umask:成员在
~/.profile写umask 007,避免新文件对 other 开放。 - 验证:用另一个组成员创建文件,
getfacl看继承,ls -l看组位是否为 mask。
0.4.13 chmod 与 chown 的完整语法
chmod 的符号模式不是「u+rwx 这种固定写法」,而是一套语法:
mode ::= clause [, clause]...
clause ::= [ugoa]* ([+=-] ([rwxXst]* | [ugo]))+
含义:
| 元素 | 作用 |
|---|---|
u/g/o/a | 属主/属组/其他/全部;省略时默认 a,但受 umask 影响的位不会被设置 |
+/-/= | 增加/删除/精确设置 |
r/w/x | 读/写/执行 |
X | 仅当目标是目录或已有某个执行位时加 x |
s | setuid(u)或 setgid(g) |
t | sticky(o) |
u/g/o(右侧) | 复制另一类的权限,如 g=u |
chmod u=rwx,g=rx,o= file # 750
chmod g+w,o-rx file # 增量
chmod -R a+rX /srv/public # 目录加 x,普通文件不加
chmod g=u file # 组权限复制属主
chmod --reference=/etc/passwd f # 复制 mode
X 是共享目录的关键:chmod -R a+rX 给目录和已有执行位的文件加 x,不会把 .txt 变成可执行。chmod -R a+rx 会给所有文件加执行位,是常见错误。
chmod 的递归选项:-R 递归;-H 跟随命令行给出的符号链接;-L 跟随所有;-P 不跟随(默认)。--preserve-root 默认开启,拒绝对 / 递归;--no-preserve-root 关闭。对符号链接本身 chmod 通常无效(Linux 的 chmod 系统调用跟随最后一段),链接的权限位不参与访问判定;需要改链接本身没有可移植方法。
chown 改属主/属组:
chown alice file
chown alice:dev file
chown :dev file # 只改组
chown -R www-data:www-data /var/www
chown -h alice link # 改符号链接本身
chown --from=old:old -R new:new dir # 只改匹配旧属主的
chown --reference=ref file
chown 的边界:
- 普通用户不能把文件送给别人(需要
CAP_CHOWN);可以把组改成自己所属的组。 chown会清除 setuid/setgid 位;Linux 2.2.13 起 root 的chown也按同样规则处理,不是只清非 root 的。-R默认不跟随目录内的符号链接,但会跟随命令行给出的链接?GNU chown 默认-P,用-h改链接本身,-L跟随所有。- 递归改属主前先列出会影响谁:
find /var/www -xdev ! -user www-data -printf '%u %p\n' | head。 - 记录回滚:
find ... -printf '%u:%g %p\n' > owners.bak。
chmod/chown 都会更新 ctime,不会更新 mtime。备份工具如果只比较 mtime,权限变更不会被发现;用 getfacl/stat 做单独的元数据备份。
0.4.14 特殊位与 setuid 的真实边界
三个特殊位在文件与目录上的语义不同:
| 位 | 普通文件 | 目录 |
|---|---|---|
setuid 4000 | execve 后 euid = 文件属主 | Linux 上忽略 |
setgid 2000 | execve 后 egid = 文件属组 | 新建文件继承目录组;新建子目录继承 setgid |
sticky 1000 | 历史上表示常驻内存,现代忽略 | 只有文件属主/目录属主/root 能删改 |
setuid 的执行语义比「变成 root」细:
execve检查文件 mode 的S_ISUID,若挂载点有nosuid则忽略。- 进程的 euid 设为文件属主;如果文件属主是 root,euid=0。
AT_SECURE置位,动态链接器进入安全模式:忽略LD_PRELOAD、LD_LIBRARY_PATH,glibc 的secure_getenv()返回 NULL。- dumpable 被清(
fs.suid_dumpable控制 core),/proc/PID的访问受限,ptrace受限。 - 程序可以
setresuid在 real/effective/saved 之间切换;root 的 setuid 程序通常先降权再按需提权。
setuid 为什么危险:程序的任何输入处理漏洞都直接以文件属主身份执行。历史上大量本地提权来自 setuid root 程序(sudo、passwd、mount、pkexec)。现代替代:
- file capability(下一节),只给一项能力;
- systemd 的
User=、DynamicUser=、AmbientCapabilities=; - 把特权操作拆成一个小服务,通过 Unix socket/D-Bus 暴露有限接口;
no_new_privs+ seccomp 限制攻击面。
边界清单:
| 场景 | setuid 是否生效 | 说明 |
|---|---|---|
| 普通 ELF 可执行文件 | 是 | 文件属主非 root 时 euid 变为该用户 |
脚本(#!/bin/sh) | 否 | 内核忽略脚本的 setuid,解释器按调用者身份运行 |
nosuid 挂载 | 否 | mount -o nosuid、容器常默认 |
no_new_privs=1 | 否 | prctl(PR_SET_NO_NEW_PRIVS) 或 systemd NoNewPrivileges=yes |
| 目录上的 setuid | 否 | Linux 忽略 |
被 chown 后 | 位被清除 | Linux 2.2.13 起 root 也一样清 setuid/setgid |
# 审计 setuid/setgid:只读
find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -printf '%M %u:%g %p\n' 2>/dev/null
# 看某个文件属于哪个包,判断是否原装
dpkg -S /usr/bin/passwd
# 看挂载点是否 nosuid
findmnt -T /usr -o TARGET,OPTIONS
对 setuid 程序的最低要求:不要信任环境变量、不要用 system()/popen() 拼命令、打开文件用 openat + O_NOFOLLOW、降权后用 setresuid 且检查返回值、失败时 _exit 而不是 exit(避免 flush 攻击者控制的 stdio)。这些细节在第 2 篇的进程凭证一章展开。
0.4.15 ACL:mask、默认 ACL 与继承
ACL 的完整模型有三层:访问 ACL(当前对象的条目)、默认 ACL(目录给新建对象的模板)、mask(组类条目的上限)。getfacl 的输出把三层都列出来:
# file: srv/shared/report.csv
# owner: wang
# group: dev
user::rw-
user:alice:rw- # 具名用户
group::r-- # 属组
group:ops:r-- # 具名组
mask::rw- # 组类上限
other::---
mask 只约束「组类」:group::、group:name:、user:name:。owner 的 user:: 与 other:: 不受 mask 影响。ls -l 在 ACL 存在时把组三位显示为 mask,并在末尾加 +;所以看到 -rw-rw----+ 时,rw- 是 mask,不一定是 group::。
有效权限 = 条目权限 & mask。getfacl -e 会把每条的有效权限单独列出来:
getfacl -e report.csv
# user:alice:rw- # effective: r--
# group:ops:r-- # effective: r--
# mask::r--
排查「ACL 给了 rwx 为什么还是 denied」,顺序是:getfacl -e 看 effective → 看 mask → 看路径上目录的 x → 看 LSM。
setfacl 的常用操作:
| 命令 | 作用 |
|---|---|
setfacl -m u:alice:rw- file | 增加/修改具名用户 |
setfacl -m g:dev:rwx,m::rwx file | 同时放开 mask |
setfacl -x u:alice file | 删除一条 |
setfacl -b file | 删除所有扩展 ACL 与 mask |
setfacl -k dir | 删除默认 ACL |
setfacl -d -m g:dev:rwx dir | 设置默认 ACL |
setfacl -R -m ... dir | 递归设置访问 ACL |
setfacl -R -d -m ... dir | 递归设置默认 ACL |
setfacl -M file.acl dir | 从文件读条目 |
setfacl --restore=acls.bak | 恢复 getfacl -R 的备份 |
setfacl -n -m ... | 不自动重算 mask(危险,需自己保证) |
setfacl 默认会重算 mask:新增一个 u:bob:rwx 时,mask 自动变成这些条目的并集,可能把原本只读的组条目一起放大。要精确控制,显式写 m::,或用 -n 后自己设 mask。
默认 ACL 的继承规则:
| 新对象 | 从父目录继承 | 结果 |
|---|---|---|
| 普通文件 | default: 条目转为访问 ACL | 有访问 ACL;无默认 ACL |
| 子目录 | default: 条目转为访问 ACL,同时保留为默认 ACL | 子目录继续向下继承 |
| 子目录里的文件 | 子目录的默认 ACL | 逐层传递 |
默认 ACL 只影响新建对象,不改变已有文件。给一个已有目录树补默认 ACL,要 setfacl -R -d -m ...;给已有文件补访问 ACL,要 setfacl -R -m ...。两者不要混。
umask 仍然参与:新建文件的请求 mode(已应用 umask)会进一步裁剪默认 ACL 的条目。团队目录的推荐组合是 2770 + 默认 ACL g:dev:rwx,m::rwx + 成员 umask 007,三者缺一不可。
备份与恢复:
getfacl -R -p /srv/shared > /var/backups/shared.acl
setfacl --restore=/var/backups/shared.acl
-p 保留绝对路径的前导 /;不加 -p 时 getfacl 会把 /srv/... 写成 srv/...,恢复时相对当前目录,容易放错位置。-R 递归,-n 用数字 UID/GID(跨主机恢复更稳)。
ACL 的文件系统支持:ext4 默认挂载含 acl,XFS/Btrfs 默认支持,tmpfs 支持;NFSv3 用 POSIX ACL 需要服务端/客户端配置,NFSv4 用 NFSv4 ACL(nfs4_getfacl/nfs4_setfacl),与 getfacl 不是同一套。tar --acls、rsync -A、cp -a 保留 ACL;cp -r 不保留。
- 「
getfacl -p是数字 UID」——-p是保留绝对路径;数字 UID 用-n。 - 「mask 是额外权限」——mask 是上限,只会削减组类条目,不会增加。
- 「
ls -l的组三位就是 group::」——有 ACL 时它是 mask,getfacl才看得到group::。 - 「设了默认 ACL 新文件就一定有全部权限」——还要过 umask 与请求 mode,三者取交集。
- 「
setfacl -k能删所有 ACL」——-k只删默认 ACL;-b删除所有扩展条目(含默认),保留 owner/group/other 的基础位。 - 「ACL 能替代目录权限」——路径上每一级目录的
x仍是前提,ACL 只作用于目标 inode。
0.4.16 capability:五个集合与文件能力
capability 把 root 的特权拆成约 40 项。一个进程有五组:
| 集合 | 含义 | 如何变化 |
|---|---|---|
| Permitted | 进程允许持有的能力上限 | capset 只能在其内增减 |
| Effective | 当前生效的能力 | capset 从 permitted 提升 |
| Inheritable | execve 时可继承的能力 | capset,受文件 inheritable 影响 |
| Bounding | 进程永远不能获得的能力上限 | 只能删,不能加(除非 CAP_SETPCAP) |
| Ambient | 非特权程序 execve 后保留的能力 | prctl(PR_CAP_AMBIENT),受 permitted/inheritable 限制 |
文件 capability 存在 security.capability xattr 里,有三个位:permitted、inheritable、effective。setcap 的写法:
sudo setcap cap_net_bind_service=+ep /usr/local/bin/web
getcap /usr/local/bin/web
# /usr/local/bin/web cap_net_bind_service=ep
sudo setcap -r /usr/local/bin/web # 删除
+ep 是最常见的组合:执行后 capability 进入 permitted 与 effective,程序立刻能用。只写 +p 时,程序要自己 cap_set_proc 把 permitted 提到 effective,大多数程序不会做。+i 用于「让子进程继承」,需要程序配合。
计算规则(capabilities(7)):
P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable)
P'(ambient) = P(ambient) & P'(permitted) & P'(inheritable)
不需要背公式,记住三条:
- 文件
effective位决定「执行后是否立刻生效」; bounding是硬上限,容器/服务用它把危险能力永久排除;ambient让非 setuid 程序在execve后保留能力,适合脚本/解释器场景。
查看与实验:
getpcaps $$ # 当前 Shell 的能力
capsh --print | sed -n '1,12p' # 五组集合
capsh --decode=00000000a80425fb # 把十六进制 CapEff 解码成名字
grep '^Cap' /proc/self/status # CapInh/CapPrm/CapEff/CapBnd/CapAmb
capsh --drop=cap_sys_admin -- -c 'mount ...' 可以在一层子 Shell 里丢掉某项能力,验证程序是否真的需要它。systemd 服务用:
[Service]
User=web
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes 会让 execve 忽略 setuid 与文件 capability,所以它与 AmbientCapabilities 二选一:要保留 capability 就不要开 NoNewPrivileges,或者用 ambient 在启动时已经加好。
边界:
- 只对 ELF 可靠。脚本由解释器执行,文件 capability 不会按预期附加;需要时用 C 包装器或 systemd ambient。
nosuid挂载会忽略文件 capability,与 setuid 一样。- capability 不是沙箱。
CAP_SYS_ADMIN接近 root,CAP_DAC_OVERRIDE绕过所有文件读权限,CAP_SYS_PTRACE可以读其他进程内存。只给真正需要的那一项。 - 容器默认丢弃大部分能力。Docker 默认保留的集合里没有
CAP_SYS_ADMIN,所以容器里mount通常失败;--cap-add要慎重。 - root 在初始 user namespace 里仍有全部能力;容器里的 root 受 bounding set 与 user namespace 限制。
传统 ping 是 setuid root,为了创建 SOCK_RAW。现代 Linux 允许非特权用户创建 ICMP datagram socket(ping_group_range),Ubuntu 的 ping 改用 cap_net_raw=ep。它只需要「发原始包」这一项能力,不需要读 /etc/shadow、不需要挂载文件系统。用 getcap /usr/bin/ping 能验证;如果看到 setuid 位,说明是旧包或手工改过。
0.4.17 账号文件:passwd、shadow、group 的字段
「你是谁」由 NSS(Name Service Switch)回答,最常见的数据源是三个文件。字段错一位,登录、sudo、家目录权限都会出问题。
/etc/passwd(所有人可读):
wang:x:1000:1000:Wang Wei:/home/wang:/bin/bash
| 字段 | 例 | 含义 | 注意 |
|---|---|---|---|
| 用户名 | wang | 登录名 | 区分大小写;不要用 . 开头 |
| 密码占位 | x | 表示哈希在 shadow | 旧系统可能直接放哈希(危险) |
| UID | 1000 | 用户 ID | 0 是 root;重复 UID 等于同一用户 |
| GID | 1000 | 主组 ID | 必须在 group 中存在 |
| GECOS | Wang Wei | 注释/全名 | 逗号分隔,可放办公室、电话 |
| 家目录 | /home/wang | 登录后的 cwd | 不存在时登录可能失败 |
| 登录 Shell | /bin/bash | execve 的程序 | /usr/sbin/nologin 禁止登录 |
/etc/shadow(仅 root 可读):
wang:$y$j9T$...:20000:0:99999:7:::
| 字段 | 例 | 含义 |
|---|---|---|
| 用户名 | wang | 与 passwd 对应 |
| 哈希 | $y$... | $y$ yescrypt(Ubuntu 24.04 默认)、$6$ SHA-512、$5$ SHA-256、$2b$ bcrypt、$1$ MD5 |
| 最后修改 | 20000 | 1970-01-01 起的天数 |
| 最小天数 | 0 | 两次改密的最小间隔 |
| 最大天数 | 99999 | 超过必须改密;99999 约等于永不过期 |
| 警告天数 | 7 | 过期前多少天提示 |
| 不活跃天数 | 空 | 过期后多少天禁用 |
| 过期日 | 空 | 1970 起的天数;chage -E 设置 |
| 保留 | 空 | 未来使用 |
密码字段的特殊值:! 或 !! 表示锁定(在哈希前加 !),* 表示不可登录,空字段表示无密码(极危险)。passwd -l 加 !,passwd -u 去掉;但锁密码不等于锁账号:SSH 公钥、Kerberos、sudo 的 NOPASSWD 仍可能让用户进入。要真正禁用,把 shell 改成 nologin,或 chage -E 0 让账号过期,或 usermod -L 配合 sshd 的 DenyUsers。
/etc/group:
sudo:x:27:wang,alice
| 字段 | 例 | 含义 |
|---|---|---|
| 组名 | sudo | 组名 |
| 密码占位 | x | 组密码在 /etc/gshadow |
| GID | 27 | 组 ID |
| 成员 | wang,alice | 附加成员;主组成员不在这里列出 |
用户的主组由 passwd 的 GID 决定;/etc/group 的成员列表只列附加组。id wang 会同时显示主组与附加组;groups wang 是简写。判断「用户是否在组里」不要 grep 组文件,用 id -nG wang 或 getent group dev。
getent 走 NSS,能查到 LDAP、SSSD、systemd-homed、winbind 提供的账号;直接读 /etc/passwd 会漏掉网络账号。getent passwd wang、getent group dev、getent shadow wang(需 root)。nsswitch.conf 决定顺序:
passwd: files systemd
group: files systemd
shadow: files
顺序很重要:files 在前时本地文件优先;sss 在前时 LDAP 优先。调试登录问题,先 getent passwd user 看 NSS 返回什么,再看 /etc/passwd 是否有冲突条目。
UID 范围由 /etc/login.defs 决定:UID_MIN 1000、UID_MAX 60000、SYS_UID_MIN 100、SYS_UID_MAX 999。UID 0 是 root;把普通用户改成 0 等于再造一个 root。UID 重复是严重问题:内核只看数字,两个名字会共享同一套权限与家目录归属。
0.4.18 用户与组管理命令
高层命令(adduser/deluser,Debian/Ubuntu 的 Perl 脚本)与低层命令(useradd/usermod/userdel,shadow-utils)的分工:
| 目的 | 高层(Ubuntu) | 低层(跨发行版) | 差异 |
|---|---|---|---|
| 建用户 | adduser alice | useradd -m -s /bin/bash alice | adduser 交互设密码、建家目录、复制 skel |
| 删用户 | deluser --remove-home alice | userdel -r alice | 都删家目录;deluser 还会处理组 |
| 改用户 | usermod(相同) | usermod | adduser 没有改用户子命令 |
| 建组 | addgroup dev | groupadd dev | addgroup 可同时加成员 |
| 加成员 | adduser alice dev | usermod -aG dev alice | 前者是 Debian 语法 |
useradd 的默认值来自 /etc/default/useradd 与 /etc/login.defs,家目录模板是 /etc/skel。常用选项:
| 选项 | 作用 | 注意 |
|---|---|---|
-m | 创建家目录 | 低层默认不建 |
-M | 不建家目录 | 服务账号常用 |
-d DIR | 指定家目录 | 配合 -m |
-s SHELL | 登录 Shell | 服务账号用 nologin |
-g GROUP | 主组 | 必须是已存在组 |
-G GROUPS | 附加组 | 逗号分隔,替换而不是追加 |
-u UID | 指定 UID | 冲突会失败 |
-c COMMENT | GECOS | 可放全名 |
-e DATE | 账号过期日 | YYYY-MM-DD |
-f DAYS | 密码不活跃天数 | -1 表示不失效 |
-r | 系统账号 | UID 从 SYS_UID 范围取 |
-N | 不创建同名组 | 默认会创建 |
-U | 创建同名组 | 与 -N 相反 |
-p HASH | 直接给哈希 | 不要传明文;用 passwd 或 chpasswd |
usermod 的高频用法与事故:
sudo usermod -aG dev alice # 追加附加组(-a 不能省)
sudo usermod -G dev alice # 错误:把其他附加组全部替换掉
sudo usermod -l alice2 alice # 改名;家目录不会自动改
sudo usermod -d /home/alice2 -m alice2 # 改家目录并搬内容
sudo usermod -s /usr/sbin/nologin alice # 禁止登录
sudo usermod -L alice # 锁密码(在哈希前加 !)
sudo usermod -U alice # 解锁
sudo usermod -e 2025-12-31 alice # 账号过期
-G 不带 -a 会替换附加组,这是把用户踢出 sudo 组的经典事故。执行前先 id alice 记录原组,执行后再核对。
userdel -r alice 删家目录与邮件池,但不会删除该用户在系统中的文件(例如 /srv 下属于她的文件会变成无属主)。删用户前先 find / -xdev -user alice -print 评估影响;删后检查 find / -xdev -nouser -print。
组管理:
sudo groupadd -g 1500 dev
sudo groupadd -r appsvc # 系统组
sudo groupmod -n developers dev # 改名
sudo groupdel dev # 有用户以它为主组时失败
sudo gpasswd -a alice dev # 加成员
sudo gpasswd -d alice dev # 删成员
sudo gpasswd -A alice dev # 设管理员
sudo gpasswd -M alice,bob dev # 设置成员列表(替换)
newgrp dev # 当前 Shell 切换到 dev 主组(新 Shell)
sg dev -c 'id' # 以 dev 组执行一条命令
newgrp 会启动一个新的 Shell,退出后回到原来的组;它不改变已登录会话里其他进程的组。SSH 会话中的补充组在登录时确定,usermod -aG 后要重新登录才生效(newgrp 只对当前 Shell 有效)。
密码与有效期:
sudo passwd alice # 交互设密码
sudo passwd -l alice # 锁
sudo passwd -u alice # 解锁
sudo passwd -e alice # 强制下次登录改密
sudo passwd -S alice # 状态:PS(已设)/LK(锁定)/NP(无密码)
sudo chage -l alice # 看有效期
sudo chage -M 90 -W 7 alice # 最大 90 天,提前 7 天警告
sudo chage -E 2025-12-31 alice # 账号过期
sudo chage -d 0 alice # 下次登录必须改密
批量建账号时不要循环 useradd 后手工 passwd(交互);用 chpasswd 从标准输入读 user:password,并确保输入不落到 shell 历史:
printf '%s:%s\n' "$user" "$password" | sudo chpasswd
密码会出现在进程参数/管道中,生产环境应使用 chpasswd 的文件输入、随机生成密码并强制首次改密。vipw/vigr 在救援时用于安全编辑账号文件(加锁、做一致性检查),不要直接 vim /etc/passwd。
0.4.19 sudo:配置结构与「不是沙箱」的边界
/etc/sudoers 的主结构是「别名定义 + 默认值 + 授权规则」:
# 1. 别名:把一组用户/主机/命令命名
User_Alias ADMINS = alice, bob
Cmnd_Alias SERVICES = /usr/bin/systemctl restart myapp.service, \
/usr/bin/systemctl status myapp.service
Host_Alias PROD = web1, web2
# 2. 默认值:可全局,也可按用户/主机/命令限定
Defaults env_reset, secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults timestamp_timeout=5
Defaults:deploy !requiretty
# 3. 授权规则:谁 在哪台主机 = (以谁的身份) 能跑什么
ADMINS PROD = (root) NOPASSWD: SERVICES
%ops ALL = (ALL:ALL) ALL
deploy ALL = (root) NOPASSWD: /usr/bin/systemctl restart myapp.service
字段读法:用户 主机 = (runas用户:runas组) 标签: 命令。%ops 是组;ALL 是通配;(root) 表示只能以 root 运行;NOPASSWD: 是标签,作用于后面的命令列表。
常用标签:
| 标签 | 作用 | 风险 |
|---|---|---|
NOPASSWD: | 不询问密码 | 任何能触发该命令的路径都直接执行 |
PASSWD: | 恢复询问 | 默认 |
NOEXEC: | 禁止命令再执行子进程 | 对静态链接、脚本、部分程序无效 |
SETENV: | 允许设置环境变量 | 可注入 LD_PRELOAD 等 |
NOSETENV: | 禁止设置环境变量 | 默认 |
LOG_INPUT:/LOG_OUTPUT: | 记录输入/输出 | 需要 sudo I/O 插件 |
CWD: | 指定工作目录 | 路径必须存在 |
CHROOT: | chroot 到目录 | 配置复杂 |
Defaults 的关键项:
| 项 | 默认(Ubuntu) | 作用 |
|---|---|---|
env_reset | 开 | 清空环境,只保留白名单 |
secure_path | 开 | 用固定 PATH,防止用户 PATH 注入 |
timestamp_timeout | 15 分钟 | 免密缓存时长;0 每次问 |
passwd_tries | 3 | 密码尝试次数 |
lecture | 开 | 首次使用的说教 |
requiretty | 关 | 只允许终端使用;旧发行版常开 |
log_output | 关 | 记录命令输出 |
iolog_dir | /var/log/sudo-io | I/O 日志目录 |
修改 sudoers 只能用 visudo(它加锁并在保存前做语法检查);分段文件放 /etc/sudoers.d/,权限 0440,文件名不要带 . 或 ~(sudo 会忽略)。检查:
sudo visudo -c # 检查主文件与 includedir
sudo visudo -c -f /etc/sudoers.d/deploy
sudo -l # 当前用户被允许什么
sudo -l -U alice # 看别人的授权
sudo 的常用形式:
sudo -u postgres psql # 以指定用户
sudo -g dev id # 以指定组
sudo -i # 登录式 root Shell(读 root 的 profile)
sudo -s # 非登录 root Shell
sudo -E cmd # 保留环境(受 env_reset 限制)
sudo -k # 清除时间戳,下次重新认证
sudo -v # 刷新时间戳
sudoedit /etc/nginx/nginx.conf # 用编辑器改,避免 sudo vim
sudo -i 与 su - 的区别:前者按 sudoers 授权、默认重置环境;后者需要目标用户密码(root 密码),并读目标用户的登录文件。生产环境更推荐 sudo,因为授权可审计、可细粒度;但 sudo 的细粒度只到「命令 + 参数模式」,不是沙箱。
为什么命令限制不是沙箱:任何能执行子进程或被用来读写文件的命令,都可能突破 allowlist。
| 允许的命令 | 绕过方式 |
|---|---|
vim/nano | :!sh、:r /etc/shadow、写 authorized_keys |
less/more | !sh、s 保存到文件 |
find | -exec sh -c ... \; |
awk | BEGIN { system("sh") } |
tar | --checkpoint-action=exec=sh |
systemctl | 启动任意 unit、systemctl edit 写文件 |
env | env sh |
python/perl/ruby | -c 'import os; os.system("sh")' |
cp/tee/dd | 覆盖 /etc/sudoers、写 authorized_keys |
mount | 挂载恶意文件系统、覆盖文件 |
因此最小规则的原则是:命令用绝对路径、参数写死、不允许通配符、不允许 shell;能不给 root 就不给,能用专用用户/服务就不给 sudo。需要「编辑某个文件」时用 sudoedit(它以用户身份编辑临时副本,再以 root 复制回去),不要允许 sudo vim。
sudo 的时间戳默认按终端/用户记录在 /run/sudo/ts/,sudo -k 清除;timestamp_timeout=0 每次都问。sudo 的日志在 /var/log/auth.log(Debian/Ubuntu)或 journald,审计要同时记录「谁、以谁、跑了什么、退出码」。
- 「
NOPASSWD只是省一次输入」——它意味着任何能触发该命令的路径都以 root 执行;参数可控时等于给了 root。 - 「限制命令名就安全」——
vim、find、awk、tar、systemctl都能逃逸;参数通配符更危险。 - 「
NOEXEC能防逃逸」——对静态链接、脚本、部分程序无效;它不是沙箱。 - 「
sudo -E能保留所有环境」——env_reset与env_keep会过滤;LD_PRELOAD在安全模式下被忽略。 - 「把用户加进 sudo 组就行」——Ubuntu 的
%sudo ALL=(ALL:ALL) ALL等于全权;要细粒度就写独立规则。 - 「
sudoers.d里放什么都会生效」——文件名含./~会被忽略,权限必须 0440,语法错误会让 sudo 整体拒绝加载。
0.4.20 实验:setuid、ACL 与 capability 的证据
实验一:看 sudo 如何切换凭证
以 root 执行(或在测试机):
strace -f -e trace=setresuid,setresgid,setuid,setgid,execve \
-o /tmp/sudo.trace sudo -u nobody id
grep -E 'setresuid|setresgid|execve' /tmp/sudo.trace | tail -20
预期能看到类似:
setresuid(-1, 0, -1) = 0
setresgid(-1, 0, -1) = 0
execve("/usr/bin/id", ["id"], ...) = 0
setresuid(65534, 65534, 65534) = 0
具体顺序取决于 sudo 版本与 PAM 配置,但关键点是:sudo 先以 setuid root 获得 euid 0,认证后 setresuid 切到目标用户,再 execve。-1 表示「保持该槽位不变」。如果 setresuid 失败,程序必须退出,不能继续以 root 运行。
实验二:ACL 存在哪个 xattr 里
cd /tmp && printf 'x\n' > acl.txt
setfacl -m u:nobody:rw-,m::rw- acl.txt
getfacl -n acl.txt
getfattr -n system.posix_acl_access -e hex acl.txt 2>/dev/null
strace -e trace=setxattr,fsetxattr,getxattr,fgetxattr setfacl -m u:nobody:r-- acl.txt
预期:getfacl 显示 user:nobody:rw- 与 mask::rw-;getfattr 以十六进制显示 system.posix_acl_access 的二进制结构;strace 里能看到 setxattr("acl.txt", "system.posix_acl_access", ...)。这证明 ACL 不是 mode 的另一种写法,而是独立的 xattr,ls -l 只是把 mask 映射到组三位。
实验三:capability 的五组集合
capsh --print | sed -n '1,15p'
grep '^Cap' /proc/self/status
getpcaps $$
sudo setcap cap_net_bind_service=+ep /usr/local/bin/web 2>/dev/null || true
getcap /usr/local/bin/web 2>/dev/null
capsh --decode=$(grep '^CapEff' /proc/self/status | awk '{print $2}')
读法:CapInh 是 inheritable,CapPrm 是 permitted,CapEff 是 effective,CapBnd 是 bounding,CapAmb 是 ambient。普通用户的 Shell 通常 CapEff=0;root 是接近全 1 的掩码。capsh --decode 把十六进制翻译成名字,比数位可读。
0.4.21 两个可运行脚本
已有的 audit-perms.sh 是只读审计;下面两个脚本分别做「共享目录 ACL」与「批量建用户」。都用 -n 试运行,先看计划再执行。
脚本一:acl-share.sh——可继承的团队目录
#!/usr/bin/env bash
# acl-share.sh — 建一个 setgid + 默认 ACL 的共享目录
# 用法: acl-share.sh [-n] -d DIR -g GROUP
set -euo pipefail
usage() { printf 'usage: %s [-n] -d DIR -g GROUP\n' "${0##*/}" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 2; }
dry_run=0 dir='' group=''
while getopts ':nd:g:h' opt; do
case $opt in
n) dry_run=1 ;;
d) dir=$OPTARG ;;
g) group=$OPTARG ;;
h) usage; exit 0 ;;
:) die "option -$OPTARG requires an argument" ;;
\?) die "unknown option -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
[[ -n $dir && -n $group ]] || { usage; exit 2; }
getent group "$group" >/dev/null || die "no such group: $group"
[[ $dir == /* ]] || die "DIR must be absolute"
[[ $dir != / && $dir != /usr && $dir != /etc ]] || die "refusing $dir"
printf 'dir=%s group=%s dry_run=%d\n' "$dir" "$group" "$dry_run"
if (( dry_run )); then
printf 'would: install -d -m 2770 -g %q %q\n' "$group" "$dir"
printf 'would: setfacl -m g:%s:rwx,m::rwx %q\n' "$group" "$dir"
printf 'would: setfacl -d -m g:%s:rwx,m::rwx %q\n' "$group" "$dir"
exit 0
fi
# install 会显式 chmod,不受 umask 影响
install -d -m 2770 -g "$group" -- "$dir"
setfacl -m "g:$group:rwx,m::rwx" -- "$dir"
setfacl -d -m "g:$group:rwx,m::rwx" -- "$dir"
# 验证:访问 ACL、默认 ACL、setgid 位
getfacl -n -- "$dir"
find "$dir" -maxdepth 0 -printf 'mode=%M\n'
关键点:install -d -m 2770 一次完成「建目录 + setgid + 组」,比 mkdir 后 chmod 少一个中间态;setfacl -m 与 -d -m 分开,前者管现有目录,后者管未来新建;-- 防止目录名以 - 开头。getfacl 默认会重算 mask,脚本显式写 m::rwx,避免新增条目把 mask 改小。
脚本二:user-provision.sh——从 TSV 批量建用户
#!/usr/bin/env bash
# user-provision.sh — 从 TSV 读 user<TAB>group<TAB>shell,幂等创建
# 用法: user-provision.sh [-n] FILE
set -euo pipefail
usage() { printf 'usage: %s [-n] FILE\n' "${0##*/}" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 2; }
dry_run=0
while getopts ':nh' opt; do
case $opt in
n) dry_run=1 ;;
h) usage; exit 0 ;;
\?) die "unknown option -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
file=${1:-}
[[ -n $file && -r $file ]] || { usage; exit 2; }
while IFS=$'\t' read -r user group shell; do
[[ -z $user || $user == \#* ]] && continue
[[ $user =~ ^[a-z_][a-z0-9_-]*$ ]] || die "bad user: $user"
[[ $group =~ ^[a-z_][a-z0-9_-]*$ ]] || die "bad group: $group"
[[ $shell == /* ]] || die "bad shell: $shell"
if getent passwd "$user" >/dev/null; then
printf 'exists: %s\n' "$user"
continue
fi
if (( dry_run )); then
printf 'would: groupadd -f %q; useradd -m -s %q -g %q %q\n' \
"$group" "$shell" "$group" "$user"
continue
fi
groupadd -f "$group"
useradd -m -s "$shell" -g "$group" "$user"
passwd -e "$user" >/dev/null # 强制首次登录改密
chage -M 90 -W 7 "$user"
printf 'created: %s group=%s shell=%s\n' "$user" "$group" "$shell"
done < "$file"
关键点:IFS=$'\t' 只按 tab 分列,用户名/组名里的空格不会被误分;getent 先查 NSS,避免和 LDAP 账号冲突;groupadd -f 幂等;useradd -m -s -g 一次成型;passwd -e 强制首次改密,避免脚本里传明文;chage 设有效期。脚本不处理密码,密码应由 chpasswd 或外部密钥系统注入。
这两个脚本加上已有的 audit-perms.sh,覆盖了「建目录、建账号、审计」三条路径。运行前用 -n 看计划,运行后用 getfacl/id/chage -l 验证。
0.4.22 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
| 读出十二位 mode | 能解释 4755、2770、1777 | 0.4.2 |
| 区分目录 r/w/x | 能解释「有 r 无 x」的现象 | 0.4.3 |
| 算 umask | 能从 umask 027 推出 640/750 | 0.4.4 |
| 判定访问顺序 | 能说出属主优先于组 | 0.4.1 |
| 用 ACL 做共享 | 会设具名用户、默认 ACL、看 mask | 0.4.6 |
| 用 capability 替代 setuid | 会 setcap/getcap/回滚 | 0.4.7 |
| 写最小 sudo 规则 | 知道命令限制不是沙箱 | 0.4.8、0.4.19 |
| 说清 mode 位与判定路径 | 能指出 inode_permission 与 capability 兜底 | 0.4.10 |
| 解释删除/重命名的权限 | 能说出父目录 w+x 与 sticky | 0.4.11 |
| 算 umask 并知道来源 | 能解释 mkdir -m 为何不受 umask 影响 | 0.4.12 |
| 写对 ACL 与 mask | 会 getfacl -e、设默认 ACL、备份恢复 | 0.4.15 |
| 解释 capability 五集合 | 能说出 permitted/effective/bounding | 0.4.16 |
| 读账号文件字段 | 能区分 shadow 的 8 个字段与哈希前缀 | 0.4.17 |
| 安全地批量建用户 | 会用 getent、useradd -m、chage | 0.4.18、0.4.21 |
Q1 文件 -rw-r----- alice dev report.txt,进程 euid=bob、egid=dev。bob 能读吗?如果 bob 是文件属主呢?如果进程 euid=0 呢?
Q2 umask 是 027,新建文件与目录的权限分别是什么?touch 创建的可执行脚本为什么没有执行位?怎样让它可执行?
Q3 setfacl -m u:alice:rwx file 后,getfacl 显示 mask::r--。alice 实际能写吗?怎样修正?
Q4 一个服务需要监听 80 端口,但不需要其他 root 权限。给出两种比 setuid root 更好的方案,并说明各自的边界。
答案
A1 bob 不是属主,但在 dev 组,命中组类别,group 位是 r--,所以能读。若 bob 是属主,只看 owner 位 rw-,也能读(但不能再按组类别获得其他权限)。euid=0 时 root 可绕过 DAC 读,普通文件读不需要执行位;但若文件在只读文件系统或受 LSM 限制,仍可能失败。
A2 文件 666 & ~027 = 640,目录 777 & ~027 = 750。touch 按 666 创建,本来就没有执行位;需要执行时用 chmod u+x script 或 chmod 750 script,并确保 shebang 正确。umask 只影响新建,不会给已有文件加执行位。
A3 不能写。mask 是组类权限的上限,rwx & r-- = r--,effective 只有读。修正:setfacl -m m::rwx file(放开 mask),或 setfacl -m u:alice:rwx,m::rwx file。注意放开 mask 也会影响同组其他具名条目。
A4 ① file capability:setcap cap_net_bind_service=+ep /usr/local/bin/web,只给绑定低端口的能力;边界是二进制必须可信,且程序被替换后 capability 仍在,需要配合包校验/只读挂载。② systemd:AmbientCapabilities=CAP_NET_BIND_SERVICE、User=web、NoNewPrivileges=yes,由 unit 限制;边界是只适用于 systemd 管理的服务,且 capability 仍在进程内,程序漏洞仍可滥用这一项能力。
母本对应
| 母本 | 章节 | 本书落点 | 程度 |
|---|---|---|---|
| G 鸟哥 | 第 5 章 文件权限与目录配置 | 0.4.1–0.4.14 | 完整(mode、umask、特殊位) |
| G 鸟哥 | 第 13.1–13.4 节 账号、ACL、身份切换 | 0.4.15–0.4.19 | 完整(passwd/shadow、ACL、sudo) |
| S Shotts | 第 9 章 权限 | 0.4.2–0.4.14 | 完整(chmod/chown/umask/特殊位) |
| B shell-book | 第 7 章 理解 Linux 文件权限 | 0.4.15、0.4.17 | 完整(ACL、账号文件) |
| K Kerrisk | 第 8–9 章 用户与组、进程凭证;第 15–17 章 文件属性、ACL、capability | 0.4.10、0.4.15、0.4.16 | 完整(判定路径、ACL、capability) |
| C CSAPP | 第 10.2 节 文件与权限位 | 0.4.2 | 背景(权限位定义) |
下一站:0.5 重定向、管道与文本流把多个命令接起来;权限决定「能不能打开」,重定向决定「打开后接到哪里」。