Linux 基础体系 · 第 6/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 权限完整指南:UID/GID、mode、umask、ACL 与 capabilities

Linux 权限控制不是单一的“文件有几个 rwx”规则,而是多个层次共同决定的结果:

  1. 进程以哪个 UID、GID 和附加组运行;
  2. 文件或目录的所有者、所属组和 mode 是什么;
  3. 新对象创建时 umask 或默认 ACL 如何参与;
  4. POSIX ACL 是否覆盖了简单的 owner/group/other 判断;
  5. 进程是否拥有绕过部分 DAC(Discretionary Access Control,自主访问控制)的 capability;
  6. SELinux/AppArmor、挂载选项、用户命名空间等更高或更低层的约束是否拒绝操作。

因此,“这个用户是 root”或“文件显示为 777”都不足以单独解释一次访问的成败。


一、先区分认证、身份与授权

Linux 中有三个容易混淆的概念:

  • 认证(authentication):确认“你是谁”,例如密码、SSH 公钥、PAM、硬件令牌;
  • 身份(identity):内核实际用于权限判断的 UID、GID 和附加组;
  • 授权(authorization):这个身份能否对目标对象执行读、写、执行、创建、删除等操作。

SSH 登录成功只说明认证阶段通过。sshd 随后会创建或切换到某个用户身份,之后访问文件时仍由内核根据进程凭据、文件元数据和安全模块执行授权检查。sudo 也不是“让某次命令自动成功”,而是通常通过 PAM 和 sudoers 完成授权,再启动一个具有不同凭据的进程。

可以把一次访问粗略表示为:

认证成功
    ↓
建立进程凭据:UID/GID/附加组/capabilities
    ↓
路径解析:每级目录需要 traversal 权限
    ↓
文件 DAC:mode 或 ACL
    ↓
内核 capability 检查
    ↓
LSM、挂载选项、文件系统策略
    ↓
成功或返回 errno

其中任意一层拒绝,最终都可能表现为 EACCESEPERM


二、UID、GID 与进程凭据

2.1 UID 和 GID 是数字身份

文件系统中保存的是数字 UID 和 GID,而不是用户名和组名。/etc/passwd/etc/group 或 LDAP/NSS 只是把数字映射为可读名称。

id alice
getent passwd alice
getent group developers

可能得到:

uid=1001(alice) gid=1001(alice) groups=1001(alice),2000(developers)

对内核而言,真正重要的是:

  • UID 1001
  • 主 GID 1001
  • 附加组中是否包含 2000

如果删除或修改 /etc/passwd 中的用户名,已经存在的文件不会自动改变其 UID。此时 ls -l 可能显示数字:

-rw------- 1 1001 1001 12 file

这不表示权限消失了,只表示当前名称服务无法将数字映射为名字。

2.2 一个进程可能有多个 UID

Linux 进程通常涉及以下 UID:

  • real UID(真实 UID):最初启动该进程的用户;
  • effective UID(有效 UID):大多数权限检查使用的身份;
  • saved set-user-ID:用于保存某个可恢复的有效 UID;
  • filesystem UID(fsuid):Linux 文件系统权限检查使用的 UID,通常与 effective UID 相同,但历史上的线程和提权程序可能改变它。

GID 也有类似的 real、effective、saved 和 filesystem GID 概念,并且进程还拥有附加组列表。

查看当前 shell 的身份:

id
grep '^Uid:\|^Gid:\|^Groups:' /proc/$$/status

/proc/<pid>/status 中的 UidGid 通常会显示多列数字。工程诊断时,不能只看 ps 显示的用户名;应确认实际的 effective UID、附加组以及 capability。

2.3 附加组如何参与判断

假设文件如下:

-rw-r----- 1 alice project 100 report.txt

其含义是:

  • owner:UID 对应的 alice,权限 rw-
  • group:GID 对应的 project,权限 r--
  • other:其他身份,权限 ---

一个既不是 alice、又不属于 project 的进程,即使它属于另一个有管理员含义的组,也只能使用 other 类,无法读取该文件。

附加组通常在登录时由 NSS、PAM 等组件建立。修改 /etc/group 后,已存在的登录会话不一定立刻获得新组;重新登录或使用正确的组初始化流程后才会生效。诊断时可以先确认:

id
getent group project

三、路径访问不只检查目标文件

访问 /srv/app/config.yaml 时,内核必须依次解析:

/
└── srv
    └── app
        └── config.yaml

对每一级目录,进程都需要 执行(x)权限,这里的执行含义是“穿越(search/traverse)目录”,不是运行目录。

目录权限的含义如下:

权限 对目录的含义
r 列出目录项名称
w 创建、删除、重命名目录项
x 穿越目录,并访问已知名称的目录项

典型反例:

mkdir -p /tmp/demo/private
touch /tmp/demo/private/data
chmod 600 /tmp/demo/private/data
chmod 700 /tmp/demo/private

另一个用户即使知道 data 的准确路径,也无法访问它,因为无法穿越 private。反过来,目录有 x 但没有 r 时,用户可能可以访问已知名称的文件,却无法列出目录内容:

chmod 100 /tmp/demo/private

目录中的删除和重命名主要取决于父目录wx,而不是目标文件本身是否可写。这是一个常见反例:

touch /tmp/demo/private/data
chmod 000 /tmp/demo/private/data
chmod 700 /tmp/demo/private

目录所有者仍可能删除 data,因为删除操作修改的是父目录中的目录项。

诊断路径权限时,namei 很有用:

namei -l /srv/app/config.yaml

它会逐级显示路径组件的所有者和权限,比只执行 ls -l /srv/app/config.yaml 更容易定位“哪一级目录无法穿越”。


四、mode:传统 Unix 权限位

4.1 mode 的组成

stat 可以显示文件的完整 mode:

stat -c '%A %a %f %U %G %n' file

典型输出:

-rw-r----- 640 81a0 alice project file

ls -l 开头的 10 个字符可以分为:

- rw- r-- ---
│  │   │   └── other
│  │   └────── group
│  └────────── owner
└─────────────文件类型

第一位不是普通权限位:

  • -:普通文件;
  • d:目录;
  • l:符号链接;
  • c:字符设备;
  • b:块设备;
  • s:套接字;
  • p:FIFO。

随后是 9 个基本权限位:

owner: rwx
group: rwx
other: rwx

数字表示法使用位值:

r = 4
w = 2
x = 1

例如:

0640 = owner 6(rw-)+ group 4(r--)+ other 0(---)
0755 = owner 7(rwx)+ group 5(r-x)+ other 5(r-x)

前面的 0 表示八进制。它不是“权限为 0”,而是为了明确数字解释方式。

4.2 Linux 的权限匹配是“选择一个类”,不是叠加

对没有特殊情况的普通文件,内核通常按以下顺序选择权限类:

  1. 进程身份匹配文件 owner:使用 owner 位;
  2. 否则,进程的 effective GID 或附加组匹配文件 group:使用 group 位;
  3. 否则:使用 other 位。

选择 owner 类后,不会再把 group 和 other 的权限叠加进来。

完整算例:

文件:
UID = 1000
GID = 2000
mode = 0640

进程 A:
UID = 1001
附加组 = {2000}

进程 A 不是 owner,但属于文件组,因此使用 groupr--,读取成功,写入失败。

再看进程 B:

进程 B:
UID = 1000
附加组 = {}

即使 owner 位只有 r--,进程 B 也只使用 owner 位;不能因为它还“可能属于某个组”就叠加 group 位。

这也解释了一个常见错误:把一个用户加入文件所属组后,发现仍无法写入。若该用户是文件 owner,则检查已经在 owner 类结束,不会使用 group 类的写权限。

4.3 文件和目录的 x 不同

普通文件的 x 表示允许执行。它并不表示“可以读取源代码”:

chmod 111 program

一个二进制程序可能可以执行但不能读取;脚本则通常需要解释器读取脚本内容,因此“只给脚本 x”不一定能运行,具体还受到解释器、内核执行路径和文件系统实现影响。

目录的 x 表示穿越和搜索,不是运行目录。目录通常需要 r-x 才能“列出并进入”。

4.4 特殊位:setuid、setgid 和 sticky

setuid 文件

普通可执行文件设置 setuid 后,执行时可能把 effective UID 切换为文件 owner:

chmod u+s program
ls -l program
# -rwsr-xr-x ...

例如某些系统程序需要以 root 身份执行少量受控操作。setuid 是权限边界,程序中的任意内存破坏、命令注入或路径竞争都可能变成提权漏洞。

以下因素可能使 setuid 不生效:

  • 文件系统以 nosuid 挂载;
  • 安全策略禁用或限制;
  • 用户命名空间中的 UID 映射不允许对应的宿主身份;
  • 程序本身主动丢弃权限。

Linux 通常不会可靠地对解释器脚本应用 setuid 语义,内核对脚本的解释器处理也存在竞态风险,因此不要用“setuid shell 脚本”实现特权程序。

setgid 文件和目录

setgid 对可执行文件的含义类似:执行时可能切换 effective GID。

对目录而言,setgid 更常用于协作目录:

mkdir shared
chown root:developers shared
chmod 2770 shared

在支持该语义的本地文件系统上,新建对象通常继承父目录的组 developers,而不是创建进程的主组。这可以避免多人协作目录中产生错误的组归属。

sticky 目录

目录设置 sticky 位后:

chmod 1777 /some/shared-dir

目录内的文件通常只能由以下主体删除或重命名:

  • 文件 owner;
  • 目录 owner;
  • root 或具备相应 CAP_FOWNER 的进程。

/tmp 常见的 1777 就是“所有人可创建,但不能随意删除其他人的文件”。sticky 位并不阻止读取文件,也不阻止文件 owner 修改自己的文件。


五、umask:创建对象时的默认收紧规则

5.1 基本公式

应用程序创建对象时会传入一个请求 mode。常见默认值是:

  • 普通文件:0666,应用通常不主动请求执行位;
  • 目录:0777

没有默认 ACL 时,可以近似表示为:

实际权限 = 请求 mode & ~umask

其中:

  • 请求 mode 是程序传给 open(..., O_CREAT, mode)mkdir(..., mode) 等接口的 mode;
  • umask 是进程的屏蔽位;
  • ~umask 表示保留未被屏蔽的位。

例如:

普通文件请求 mode = 0666
umask           = 0027

按八进制位计算:

  0666
& 0750
= 0640

因此新文件通常为 0640

目录:

  0777
& 0750
= 0750

因此新目录通常为 0750

查看和设置当前 shell 的 umask:

umask
umask -S
umask 027

umask 是进程属性,会被 fork 继承,通常也会随 exec 保留。shell 中设置的 umask 只影响该 shell 及其后代进程,不会修改系统中所有用户的 umask。

5.2 umask 不是强制访问控制

umask 只影响创建时的初始 mode:

chmod 666 existing-file

创建者或有权限的进程仍可以随后改变 mode。因此,不能把 umask 当成防止泄密的完整策略。

此外,程序可以请求更严格的 mode,也可以在后续调用 chmod。服务管理器、容器运行时和应用框架还可能分别设置自己的默认权限,最终结果应通过 stat 验证,而不是只查看 shell 的 umask。

5.3 并发和临时文件风险

下面这种流程存在竞态:

open("file", O_CREAT | O_WRONLY, 0666)
chmod("file", 0600)

openchmod 之间,其他进程可能已经访问或替换该文件。创建敏感文件时,应在创建阶段就传入正确的 mode,或使用安全的临时文件 API,例如 openat 配合 O_CREAT|O_EXCL,并正确检查符号链接、目录权限和错误码。

不要通过临时修改全局 shell 环境来“解决”并发服务的权限问题:

umask 077
some-command

这只改变当前 shell 的后代进程,且不能替代程序自身的安全创建方式。systemd 服务通常可使用 UMask=,但应同时确认服务的 User=Group=、目录预创建逻辑和应用实际传入的 mode。

5.4 默认 ACL 会改变 umask 的直觉

如果父目录设置了 default ACL,Linux POSIX ACL 文件系统实现会使用该默认 ACL 作为新对象的权限模板。此时不能简单地只用 请求 mode & ~umask 推导最终结果;请求 mode 仍会限制继承权限中相应的类别,但 umask 不再是唯一控制因素。

因此,在发现“umask 是 077,新文件却有额外 ACL”时,应检查:

getfacl parent-dir
getfacl new-file

六、ACL:在 mode 之外授予特定用户或组

6.1 为什么需要 ACL

传统 mode 只能表达:

一个 owner
一个 group
其他所有人

如果需求是:

alice 可以读写;
bob 只能读;
developers 组可以读;
其他人无权限

单个 group 位无法同时表达多个独立主体,这时可使用 POSIX ACL。

确认工具是否安装:

command -v getfacl
command -v setfacl

文件系统和发行版必须支持 POSIX ACL;现代主流 Linux 的常见本地文件系统通常支持,但挂载方式、网络文件系统和特殊存储实现可能不同。

6.2 ACL 的基本条目

创建示例:

mkdir acl-demo
touch acl-demo/report
chmod 640 acl-demo/report
setfacl -m u:alice:rw,u:bob:r acl-demo/report
getfacl acl-demo/report

可能看到:

user::rw-
user:alice:rw-
user:bob:r--
group::r--
mask::rw-
other::---

各条目含义:

  • user:::文件 owner;
  • user:alice::指定用户;
  • group:::文件所属组;
  • group:name::指定组;
  • mask:::ACL group class 的上限;
  • other:::其他用户。

6.3 ACL mask 是最容易误读的部分

对 named user、named group 和 owning group,实际权限还要与 mask 相交:

实际权限 = 条目权限 & ACL mask

例如:

user:alice:rwx
mask::r-x

Alice 的实际权限是:

rwx & r-x = r-x

因此 getfacl 可能显示:

user:alice:rwx            #effective:r-x

完整算例:

user::rw-
user:alice:rwx
group::r--
mask::r--
other::---
  • owner 的实际权限:rw-
  • Alice 的条目请求:rwx
  • mask:r--
  • Alice 最终只有 r--
  • owning group 的 r-- 也受到 mask 限制;
  • other 为 ---

ACL 中的 mask 不是“某一个额外用户”,而是 group class 的权限上限。使用 chmod g-w file 时,Linux 通常会修改 ACL 的 mask,而不是只修改 group:: 条目:

chmod g-w acl-demo/report
getfacl acl-demo/report

这可能同时降低多个 named user 和 named group 的有效权限。因此,修改带 ACL 文件的 mode 后必须重新查看 ACL。

6.4 ACL 的匹配顺序

对请求者进行 ACL 判断时,可以按以下逻辑理解:

  1. 如果 UID 匹配 user:: 的 owner,使用 owner 条目;
  2. 否则如果存在匹配的 named user,使用该条目并受 mask 限制;
  3. 否则合并 owning group 和所有匹配的 named group 条目,再受 mask 限制;
  4. 如果没有任何匹配的用户或组,使用 other::

关键点是:如果请求者是文件 owner,不会因为其同时属于某个 named group 就叠加组权限。

6.5 默认 ACL:让新对象继承访问规则

目录可以设置 default ACL:

mkdir project
setfacl -m u:alice:rwx,u:bob:r-x project
setfacl -d -m u::rwx,u:alice:rwx,u:bob:r-x,g::r-x,m::rwx,o::--- project
getfacl project

之后在目录中创建:

touch project/new-file
mkdir project/new-dir
getfacl project/new-file
getfacl project/new-dir

新对象会继承父目录的 default ACL,并结合创建时请求的 mode 形成访问 ACL。目录还会继续拥有自己的 default ACL,以便后代对象继续继承。

风险在于:只执行 chmod 750 project 并不能说明目录下所有对象都符合预期。应使用:

getfacl -R project

审查 ACL 时,尤其要注意:

  • mask 是否意外放宽;
  • 是否存在不再需要的 named user;
  • 删除或复用 UID 后,旧 ACL 是否仍指向同一个数字 UID;
  • 备份、打包和跨文件系统复制工具是否保留了 ACL。

七、capabilities:把 root 的部分权限拆开

7.1 为什么需要 capabilities

传统 Unix 模型中,UID 0 拥有大量特殊权限。Linux capabilities 将这些特权拆成若干独立能力,使服务可以不以 root 身份运行,却保留完成特定任务所需的一小部分权限。

常见 capability 包括:

  • CAP_DAC_OVERRIDE:绕过部分文件读、写、执行 DAC 检查;
  • CAP_DAC_READ_SEARCH:绕过部分读取文件和搜索目录的 DAC 检查;
  • CAP_CHOWN:改变文件 owner 和 group 的部分限制;
  • CAP_FOWNER:绕过某些必须是 owner 的检查;
  • CAP_SETUID:改变 UID;
  • CAP_SETGID:改变 GID 和组列表的部分限制;
  • CAP_NET_BIND_SERVICE:绑定小于 1024 的 TCP/UDP 端口;
  • CAP_NET_ADMIN:执行部分网络管理操作;
  • CAP_SYS_ADMIN:范围很大,常被认为接近“新的 root”,不应轻率授予。

capability 不是普通文件权限的替代品,也不是强制访问控制。一个进程拥有 CAP_NET_BIND_SERVICE,并不意味着它可以读取任意文件;一个进程拥有 CAP_DAC_OVERRIDE,也不代表它绕过 SELinux 的所有规则。

7.2 进程 capability 集合

Linux 进程通常涉及:

  • Permitted(P):允许进入 Effective 的上限;
  • Effective(E):当前实际用于 capability 检查;
  • Inheritable(I):允许在 execve 时参与继承;
  • Ambient(A):现代 Linux 中用于让非特权 exec 保留部分能力;
  • Bounding(B):系统或进程允许通过 exec 获得的上限。

查看当前 shell:

grep '^Cap' /proc/$$/status
capsh --print

/proc 中通常显示十六进制位图,capsh --print 更容易阅读,但需要安装 libcap 相关工具。

这些集合不是静态全局变量,而是每个线程的凭据状态。一个特权程序可以在启动后主动删除不需要的能力,以缩小攻击面。

7.3 文件 capability

Linux 可以把 capability 写入文件的 extended attribute,常见工具是 setcapgetcap

cp /usr/bin/python3 /tmp/python-test
setcap cap_net_bind_service=ep /tmp/python-test
getcap /tmp/python-test

=ep 中:

  • e:执行该文件时把对应 capability 放入 Effective;
  • p:执行该文件时把对应 capability 放入 Permitted。

文件 capability 的实际语义还受以下因素影响:

  • 文件系统是否支持 security.capability xattr;
  • 挂载是否使用 nosuid
  • capability 是否在当前 user namespace 中有意义;
  • exec 的目标是否为“特权文件”;
  • 进程 bounding set 是否已经删除该能力。

不要把 capability 直接加给解释器或功能复杂的程序。给 Python、Perl、通用编辑器或网络工具授予 CAP_DAC_OVERRIDE,实际上可能允许其运行任意代码并绕过文件 DAC,效果接近扩大 root 权限。

另外,文件 capability 的复制、打包和部署行为依赖工具和文件系统。发布后应验证:

getcap /path/to/program
getfattr -n security.capability /path/to/program

7.4 execve 时 capability 如何变化

为了理解 capability 的生命周期,设:

  • P:exec 前进程的 permitted;
  • I:exec 前进程的 inheritable;
  • A:exec 前进程的 ambient;
  • B:bounding set;
  • F_P:文件 permitted;
  • F_I:文件 inheritable;
  • F_E:文件 effective 标志。

在简化模型下,exec 后的 permitted 集合可表示为:

P' = (F_P & B) | (I & F_I) | A'

其中:

  • 文件 permitted 受 bounding set 限制;
  • 进程 inheritable 与文件 inheritable 相交后可以贡献能力;
  • 非特权文件 exec 时,ambient 能力可以继续贡献;
  • 执行特权文件时,ambient 通常被清空。

exec 后的 effective 集合通常取决于文件的 effective 标志:

E' = F_E ? P' : A'

这不是应用程序可以随意“凭空获得能力”的机制。进程必须已经在相应的 permitted、inheritable 或 ambient 路径上拥有能力,并且还要受到 bounding set、namespace 等约束。

设置了 setuid、文件 capability 或其他特权属性的文件被视为特权 exec 对象。对这类对象,环境变量和 ambient 能力会受到更严格处理,这是为了避免普通用户通过环境继承把能力带入特权程序。


八、root 不是绕过一切的万能身份

UID 0 通常拥有大量 capability,但现代 Linux 中实际特权仍受多种机制限制:

  1. 普通文件没有执行位时,root 也通常不能通过 execve 直接执行它;
  2. nosuid 可以阻止 setuid 和文件 capability 生效;
  3. SELinux/AppArmor 可以拒绝 root 进程;
  4. 用户命名空间中的 UID 0 不一定对应宿主机 UID 0;
  5. NFS root squash 可以把客户端 root 映射为匿名用户;
  6. 加密、设备策略、LSM 和容器隔离可能进一步限制访问;
  7. 文件系统故障、只读挂载和 quota 可能导致即使权限正确也无法写入。

需要区分错误类型:

  • EACCES:通常表示权限检查拒绝;
  • EPERM:通常表示操作不被允许,可能是 capability、所有权、LSM 或特定操作限制;
  • EROFS:文件系统只读;
  • ENOSPC:空间或 inode 耗尽;
  • EDQUOT:quota 超限;
  • ENOENT:路径组件不存在,或某一级路径无法正常解析。

sudo command 失败时,不能只继续增加权限。应先确认命令到底失败在路径穿越、文件 DAC、ACL、LSM、挂载状态还是资源限制。


九、DAC、ACL、capability 与 SELinux/AppArmor 的关系

mode 和 ACL 属于 DAC:文件 owner 或管理员可以改变它们。capability 则决定进程能否绕过某些 DAC 检查。

但在启用了 LSM 的系统上,最终访问还可能经过 SELinux、AppArmor 等强制访问控制机制。典型结果是:

mode 允许
    ↓
ACL 允许
    ↓
进程 capability 足够
    ↓
SELinux/AppArmor 拒绝

因此:

ls -l file
getfacl file

只能证明 DAC 层面的状态,不能证明访问一定成功。

在 SELinux 系统上可以检查:

getenforce
ls -Z file
ausearch -m avc -ts recent

在 AppArmor 系统上,应检查 profile 状态和内核日志。不同发行版启用的 LSM、日志位置和诊断命令存在差异,生产环境应以实际安全策略为准。

挂载选项也属于重要边界:

findmnt -T /path/to/file
mount | grep nosuid

常见影响包括:

  • ro:只读;
  • nosuid:禁止 setuid/setgid 和文件 capability 的特权效果;
  • noexec:限制从该挂载点直接执行程序;
  • nodev:不解释设备节点;
  • idmap 或用户命名空间相关映射:改变看到的 UID/GID 关系。

十、用户命名空间与容器中的 UID 误区

容器内显示的 UID 0,不一定是宿主机 UID 0。用户命名空间可以把容器内 UID 映射为宿主机上的非零 UID。例如:

容器 UID 0  → 宿主机 UID 100000
容器 UID 1  → 宿主机 UID 100001

此时容器内的 root capability 通常只在该 user namespace 内有效,不能自动获得宿主机全局 root 权限。

但容器挂载宿主目录时,映射错误仍可能导致严重问题:

  • 宿主机文件显示为容器内某个用户;
  • 容器进程创建的文件以意外 UID 留在宿主机;
  • 使用 --privileged 或大量 capability 会显著削弱隔离;
  • 共享目录的 ACL、setuid 和设备节点可能产生跨边界影响。

诊断容器权限时,要同时查看:

id
cat /proc/self/uid_map
cat /proc/self/gid_map
grep Cap /proc/self/status
findmnt

不能只依据容器内的 ls -l 判断宿主机真实权限。


十一、权限检查的完整算例

假设有:

文件 /srv/data/report
UID = 1000
GID = 2000
mode = 0660
ACL:
user::rw-
user:alice:r--
group::rw-
group:auditors:r--
mask::rw-
other::---

考虑四个进程:

情况 A:UID 1000 的 owner

进程 UID = 1000

匹配 user::rw-,拥有读写。即使该进程不属于 GID 2000,也不影响 owner 类选择。

情况 B:UID 1001,属于 auditors

进程 UID = 1001
附加组 = {auditors}

匹配 named group auditors:r--,再与 mask rw- 相交:

r-- & rw- = r--

因此只能读。

情况 C:UID alice,匹配 named user

ACL user:alice:r--

如果 alice 同时属于一个有 rw- 的组,仍优先使用 named user 条目,最终只有 r--,不会和组权限叠加。

情况 D:不匹配任何 ACL 条目

使用:

other::---

访问失败。

如果此时给进程授予 CAP_DAC_OVERRIDE,部分 DAC 读写检查可能被绕过,但 SELinux、只读挂载、文件系统错误等仍可能拒绝。这个结果不能简单归纳为“有 capability 就肯定成功”。


十二、创建安全目录和协作目录的端到端示例

12.1 私密服务目录

以下命令需要拥有创建目录和设置 owner 的权限:

sudo install -d -o app -g app -m 0750 /var/lib/myapp
sudo install -d -o app -g app -m 0700 /var/lib/myapp/secrets
sudo -u app sh -c 'umask 077; printf "%s\n" secret > /var/lib/myapp/secrets/token'
sudo stat -c '%A %a %U %G %n' /var/lib/myapp/secrets/token

预期类似:

-rw------- 600 app app /var/lib/myapp/secrets/token

每一步的作用是:

  • install -d 在创建目录时直接指定 owner、group 和 mode,避免“先宽后改”的窗口;
  • umask 077 使应用创建的普通文件默认不向 group 和 other 暴露;
  • printf 创建普通文件时通常请求 0666,经 077 收紧后为 0600
  • stat 验证实际结果,而不是假设结果。

若应用自行调用 chmod,最终权限仍可能不同;还应检查父目录是否被其他用户替换或可写。

12.2 共享项目目录

sudo install -d -o root -g developers -m 2770 /srv/project
sudo setfacl -m g:auditors:rx /srv/project
sudo setfacl -d -m u::rwx,g::rwx,g:auditors:r-x,m::rwx,o::--- /srv/project
getfacl /srv/project

设计含义:

  • 27702 设置目录 setgid,使新对象继承 developers 组;
  • owner 和 group 可读写穿越,other 无权限;
  • named group auditors 可以读取和穿越;
  • default ACL 让后续对象继承协作规则;
  • default ACL 中的 mask rwx 允许 group class 的权限生效。

随后验证:

sudo -u developer touch /srv/project/a.txt
getfacl /srv/project/a.txt

不要只检查目录本身。应确认新文件的 owner、group、ACL 和应用实际创建的 mode 是否符合要求。


十三、常见失败表现与诊断顺序

13.1 “我属于这个组,但仍然不能写”

先检查:

id
stat -c '%A %a %u %g %U %G %n' file
getfacl file

重点判断:

  1. 当前进程是否真的拥有该附加组;
  2. 文件 owner 是否恰好是当前用户,导致使用 owner 位;
  3. ACL 的 mask 是否去掉了写权限;
  4. 父目录是否有 wx
  5. SELinux/AppArmor 是否拒绝;
  6. 文件系统是否只读或 quota 已满。

13.2 “文件是 777,仍然 Permission denied”

可能原因包括:

  • 路径上某个父目录没有 x
  • SELinux/AppArmor 拒绝;
  • 挂载点为 noexec
  • 文件系统只读;
  • NFS 身份映射或 root squash;
  • 目标不是普通文件,操作语义不同;
  • 进程在不同 user namespace 中;
  • 应用访问的是另一个路径或符号链接目标。

可以使用:

namei -l /path/to/file
findmnt -T /path/to/file
getfacl /path/to/file
ls -Z /path/to/file
strace -e trace=%file,chmod,fchmod,fchown command

strace 的价值在于确认应用实际访问了哪个路径,以及内核返回了什么 errno;它不能绕过安全策略,也不应在包含敏感参数的生产命令上无条件启用。

13.3 “chmod 后 ACL 权限变了”

这是因为 chmod 与 POSIX ACL 不是两套完全独立的权限。尤其是 group 类 mode 位通常对应 ACL mask。修改后立即检查:

chmod g-w file
getfacl file

如果目标是精确管理 ACL,应使用 setfacl 明确修改条目和 mask,而不是混用大量 chmod 操作。

13.4 “服务启动后权限和手工执行不同”

服务通常具有不同的:

  • User=Group=
  • supplementary groups;
  • UMask=
  • capability bounding/effective 集合;
  • 工作目录和环境变量;
  • mount namespace;
  • SELinux/AppArmor 域或 profile。

应比较服务进程与交互式 shell:

ps -o pid,user,group,egroup,cmd -p "$PID"
cat /proc/"$PID"/status | grep -E '^(Uid|Gid|Groups|Cap)'
cat /proc/"$PID"/mountinfo

服务启动成功不代表其后续访问都成功;很多程序会延迟打开日志、证书、socket 或数据文件,错误可能在运行一段时间后才出现。


十四、生产中的权限取舍

14.1 最小权限应落实到对象和 capability

“以非 root 用户运行”只是起点。还要明确:

  • 服务需要访问哪些目录;
  • 哪些目录只需穿越,哪些需要列出;
  • 哪些文件需要读,哪些需要写;
  • 是否需要创建、删除、重命名;
  • 是否需要绑定低端口或操作网络;
  • 是否可删除不需要的 permitted、effective 和 bounding capabilities。

例如,一个只需监听 443 端口的服务,通常比直接以 root 运行更适合评估 CAP_NET_BIND_SERVICE。但不能把这个 capability 扩展为 CAP_DAC_OVERRIDE,也不能忽略服务自身的文件、网络和 LSM 策略。

14.2 不要把 ACL 当作审计记录

ACL 表示当前授权状态,不记录“谁在什么时候改过权限”。权限变更审计需要 auditd、文件完整性监控、配置管理系统或版本化部署流程。类似地,sudo 的授权规则和执行日志属于提权审计范畴,不等同于文件 ACL。

14.3 备份和迁移必须验证元数据

普通复制命令可能不保留所有权限元数据。根据工具和参数不同,以下内容可能丢失:

  • owner 和 group;
  • mode 特殊位;
  • POSIX ACL;
  • extended attributes;
  • 文件 capability;
  • SELinux label。

迁移后至少检查:

stat file
getfacl file
getcap file
ls -Z file

对跨主机、NFS、容器卷和对象存储尤其要验证,因为这些介质不一定提供与本地 POSIX 文件系统相同的语义。


十五、应建立的权限判断模型

遇到权限问题时,可以按这个顺序推导,而不是直接尝试 chmod 777

  1. 目标是什么:普通文件、目录、符号链接、设备、socket,还是路径中的某一级目录;
  2. 进程是谁:effective UID、GID、附加组、user namespace 映射;
  3. 路径能否穿越:每一级父目录是否具备所需的 x
  4. 是否存在 ACL:owner、named user/group、mask、other 如何匹配;
  5. 传统 mode 如何选择:owner、group、other 三类不会叠加;
  6. 创建还是访问:创建权限受请求 mode、umask、default ACL 影响;
  7. 是否存在 capability:尤其是 CAP_DAC_*CAP_FOWNERCAP_CHOWN
  8. 挂载和文件系统是否限制ronosuidnoexec、NFS 映射、quota;
  9. LSM 是否拒绝:SELinux/AppArmor 日志通常能给出额外证据;
  10. 应用实际访问路径和系统调用是什么:必要时用 strace 验证。

权限的核心不是记住某个命令,而是能够从进程凭据、路径解析、ACL/mode 选择、capability 和安全策略逐层推导出最终结果。这样才能解释“看起来权限正确却失败”的边界,也才能在生产环境中用最小授权替代不可控的全局放权。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。