Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 的判定顺序

定义 0.4.1 · 访问判定(DAC)

进程访问文件时,内核用进程的有效 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 给八进制,可能多出一位特殊位。

位(八进制)名称对文件对目录
4000setuid执行时 euid 变为文件属主在部分系统上有特殊语义,Linux 上一般忽略
2000setgid执行时 egid 变为文件属组新建文件继承目录的组;新建子目录继承 setgid
1000sticky历史上表示「常驻内存」,现代 Linux 忽略只有文件属主、目录属主或 root 能删除/重命名其中的文件
0400/0200/0100owner r/w/x属主读/写/执行属主列出/增删/进入
0040/0020/0010group r/w/x属组读/写/执行属组列出/增删/进入
0004/0002/0001other r/w/x其他人读/写/执行其他人列出/增删/进入

九位权限的八进制换算是把每组 rwx 看成三位二进制:r=4、w=2、x=1。rwxr-x--- = 750;rw-r--r-- = 644;rwxrwxrwt = 1777。

特殊位在 ls -l 里的显示规则:

有执行位无执行位含义
s(owner)Ssetuid
s(group)Ssetgid
t(other)Tsticky
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文件目录适用
022644755系统默认,其他用户可读
027640750团队共享但不对其他人开放
077600700私有,仅属主
002664775协作目录,同组可写
$ 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。

定义 0.4.2 · POSIX ACL 与 mask

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/passwdwang:x:1000:1000:Wang:/home/wang:/bin/bash用户名、密码占位、UID、GID、注释、主目录、登录 Shell
/etc/shadowwang:$y$j9T$...:20000:0:99999:7:::用户名、哈希、最后修改天数、最小/最大天数、警告天数、不活跃天数、过期日、保留
/etc/groupsudo: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 devgpasswd -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,它们会加锁并做一致性检查。

关于权限的五个误解
  1. 「chmod 777 能解决所有权限问题」——它让所有人可读可写可执行,既扩大攻击面,也可能让程序拒绝启动(如 SSH 私钥要求 600)。先判断是哪一类权限缺失。
  2. 「root 什么都能做」——root 受只读文件系统、immutable 属性、LSM 策略、capability 边界限制;容器里的 root 还受 seccomp 与 capability 集合限制。
  3. 「ACL 是 mode 之外的独立机制」——mode 的 group 三位在 ACL 存在时显示 mask,二者是同一套判定的一部分。改 chmod g+rw 可能同时改 mask。
  4. 「给脚本 setuid 就能提权」——Linux 忽略脚本上的 setuid 位,解释器不会以脚本属主身份运行。需要提权时用二进制包装器或 systemd。
  5. 「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 或删除。
修权限问题的顺序
  1. 确认身份:id、sudo -l,确认 euid/egid 与补充组。
  2. 确认路径:namei -l /path/to/file,逐级看目录的 x。
  3. 确认判定类别:stat -c '%A %a %U:%G %n' file,看你是属主、组还是其他。
  4. 确认 ACL:getfacl -p file,看具名条目与 mask;ls -l 末尾是否有 +。
  5. 确认 LSM:sudo aa-status 或 getenforce,看是否被 AppArmor/SELinux 拒绝;日志在 dmesg 或 journalctl -k。
  6. 最后才改权限:用最小改动(加组、加 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):

位(八进制)宏含义
0170000S_IFMT类型掩码
0140000S_IFSOCKUnix socket
0120000S_IFLNK符号链接
0100000S_IFREG普通文件
0060000S_IFBLK块设备
0040000S_IFDIR目录
0020000S_IFCHR字符设备
0010000S_IFIFOFIFO
0004000S_ISUIDsetuid
0002000S_ISGIDsetgid
0001000S_ISVTXsticky
0000700S_IRWXUowner rwx
0000070S_IRWXGgroup rwx
0000007S_IRWXOother 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。

定义 0.4.3 · 有效凭证与文件凭证

内核判定用的是进程的有效 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 dirr—无
ls -l dirr + x—对每个条目 stat 需要 x
cat dir/file路径上每级 x文件 r无
创建 dir/neww + x—目录不可 immutable
删除 dir/filew + x不需要文件写权限sticky 检查
重命名 dir/a源与目标父目录都 w + x—sticky 检查、跨挂载 EXDEV
修改文件内容路径 x文件 wO_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。

目录权限的五个误解
  1. 「目录的 r 就能读文件」——r 只允许列名字;读文件要路径上的 x 与文件的 r。
  2. 「目录的 w 就能删文件」——还需要 x 到达目录 inode;只有 w 没有 x 时创建/删除都失败。
  3. 「文件只读就删不掉」——删除看父目录写权限,不看文件 mode。
  4. 「/tmp 是 777,所以能删别人的文件」——sticky 位阻止;能删的是自己的文件。
  5. 「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 & ~umaskumask -S典型场景
022644755u=rwx,g=rx,o=rxroot、系统服务
002664775u=rwx,g=rwx,o=rxUbuntu 普通用户(UPG)
027640750u=rwx,g=rx,o=团队共享、不对其他
077600700u=rwx,g=,o=私有、临时目录
007660770u=rwx,g=rwx,o=完全信任同组
000666777u=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
systemdunit 的 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 权限。

给共享目录定权限的步骤
  1. 定组:groupadd dev,把成员加入 dev(usermod -aG dev alice)。
  2. 定目录:install -d -m 2770 -g dev /srv/shared;2770 = setgid + rwxrwx---。
  3. 定默认 ACL:setfacl -d -m g:dev:rwx,m::rwx /srv/shared。
  4. 定 umask:成员在 ~/.profile 写 umask 007,避免新文件对 other 开放。
  5. 验证:用另一个组成员创建文件,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
ssetuid(u)或 setgid(g)
tsticky(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 4000execve 后 euid = 文件属主Linux 上忽略
setgid 2000execve 后 egid = 文件属组新建文件继承目录组;新建子目录继承 setgid
sticky 1000历史上表示常驻内存,现代忽略只有文件属主/目录属主/root 能删改

setuid 的执行语义比「变成 root」细:

  1. execve 检查文件 mode 的 S_ISUID,若挂载点有 nosuid 则忽略。
  2. 进程的 euid 设为文件属主;如果文件属主是 root,euid=0。
  3. AT_SECURE 置位,动态链接器进入安全模式:忽略 LD_PRELOAD、LD_LIBRARY_PATH,glibc 的 secure_getenv() 返回 NULL。
  4. dumpable 被清(fs.suid_dumpable 控制 core),/proc/PID 的访问受限,ptrace 受限。
  5. 程序可以 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 不保留。

ACL 的六个误解
  1. 「getfacl -p 是数字 UID」——-p 是保留绝对路径;数字 UID 用 -n。
  2. 「mask 是额外权限」——mask 是上限,只会削减组类条目,不会增加。
  3. 「ls -l 的组三位就是 group::」——有 ACL 时它是 mask,getfacl 才看得到 group::。
  4. 「设了默认 ACL 新文件就一定有全部权限」——还要过 umask 与请求 mode,三者取交集。
  5. 「setfacl -k 能删所有 ACL」——-k 只删默认 ACL;-b 删除所有扩展条目(含默认),保留 owner/group/other 的基础位。
  6. 「ACL 能替代目录权限」——路径上每一级目录的 x 仍是前提,ACL 只作用于目标 inode。

0.4.16 capability:五个集合与文件能力

capability 把 root 的特权拆成约 40 项。一个进程有五组:

集合含义如何变化
Permitted进程允许持有的能力上限capset 只能在其内增减
Effective当前生效的能力capset 从 permitted 提升
Inheritableexecve 时可继承的能力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 限制。
例 0.4.1 · 为什么 `ping` 不再 setuid

传统 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旧系统可能直接放哈希(危险)
UID1000用户 ID0 是 root;重复 UID 等于同一用户
GID1000主组 ID必须在 group 中存在
GECOSWang Wei注释/全名逗号分隔,可放办公室、电话
家目录/home/wang登录后的 cwd不存在时登录可能失败
登录 Shell/bin/bashexecve 的程序/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
最后修改200001970-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
GID27组 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 aliceuseradd -m -s /bin/bash aliceadduser 交互设密码、建家目录、复制 skel
删用户deluser --remove-home aliceuserdel -r alice都删家目录;deluser 还会处理组
改用户usermod(相同)usermodadduser 没有改用户子命令
建组addgroup devgroupadd devaddgroup 可同时加成员
加成员adduser alice devusermod -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 COMMENTGECOS可放全名
-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_timeout15 分钟免密缓存时长;0 每次问
passwd_tries3密码尝试次数
lecture开首次使用的说教
requiretty关只允许终端使用;旧发行版常开
log_output关记录命令输出
iolog_dir/var/log/sudo-ioI/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 ... \;
awkBEGIN { system("sh") }
tar--checkpoint-action=exec=sh
systemctl启动任意 unit、systemctl edit 写文件
envenv 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,审计要同时记录「谁、以谁、跑了什么、退出码」。

sudo 的六个误解
  1. 「NOPASSWD 只是省一次输入」——它意味着任何能触发该命令的路径都以 root 执行;参数可控时等于给了 root。
  2. 「限制命令名就安全」——vim、find、awk、tar、systemctl 都能逃逸;参数通配符更危险。
  3. 「NOEXEC 能防逃逸」——对静态链接、脚本、部分程序无效;它不是沙箱。
  4. 「sudo -E 能保留所有环境」——env_reset 与 env_keep 会过滤;LD_PRELOAD 在安全模式下被忽略。
  5. 「把用户加进 sudo 组就行」——Ubuntu 的 %sudo ALL=(ALL:ALL) ALL 等于全权;要细粒度就写独立规则。
  6. 「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、17770.4.2
区分目录 r/w/x能解释「有 r 无 x」的现象0.4.3
算 umask能从 umask 027 推出 640/7500.4.4
判定访问顺序能说出属主优先于组0.4.1
用 ACL 做共享会设具名用户、默认 ACL、看 mask0.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 与 sticky0.4.11
算 umask 并知道来源能解释 mkdir -m 为何不受 umask 影响0.4.12
写对 ACL 与 mask会 getfacl -e、设默认 ACL、备份恢复0.4.15
解释 capability 五集合能说出 permitted/effective/bounding0.4.16
读账号文件字段能区分 shadow 的 8 个字段与哈希前缀0.4.17
安全地批量建用户会用 getent、useradd -m、chage0.4.18、0.4.21
即时自测 · 0.4

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、capability0.4.10、0.4.15、0.4.16完整(判定路径、ACL、capability)
C CSAPP第 10.2 节 文件与权限位0.4.2背景(权限位定义)

下一站:0.5 重定向、管道与文本流把多个命令接起来;权限决定「能不能打开」,重定向决定「打开后接到哪里」。