setfacl 实战:Linux 目录精细权限配置

setfacl 精细权限控制解决的是 chmod 解决不了的问题:Linux 传统权限位只有属主、属组、其他人三组,无法表达「同一个目录下,运维 A 可读写、外包 B 只读、备份账号可读不可执行」这类需求。POSIX ACL 在权限位之外增加了按用户和按组的独立条目,配合目录的默认 ACL 还能让新建文件自动继承。本文给出从环境确认、单用户授权、默认 ACL 继承、mask 收敛到批量审计与回滚的完整配置。

很多团队的做法是把所有需要访问的人塞进同一个属组,再 chmod 775 一把放权,结果任何一个账号被拿下就等于整个目录沦陷。ACL 的正确用法恰恰相反:属组留空,权限按人授予,mask 兜底。

适用场景

  • 多租户共享主机:同一台机器跑多个站点,各站点的 Web 目录之间必须互相隔离。
  • Web 上传目录:只允许写入、不允许执行,防止上传的脚本被直接运行。
  • 备份进程只读访问:备份只能读不能改,避免备份期间误删或篡改源文件。
  • 外包或临时运维授权:给单个账号开目录权限,到期用一条命令收回,不必改组结构。
  • 本地目录与 NFS 共享混用:需要在客户端和服务端表达同一套最小权限。

前置条件

  • 文件系统支持 ACL:ext4、xfs、btrfs 均支持,ext4 需挂载选项包含 acl,多数发行版默认已带。
  • acl 包已安装,提供 getfacl 与 setfacl 两个命令。
  • root 或具备 CAP_FOWNER 能力的账号执行授权操作。
  • 若涉及 NFS,客户端与服务端需同为 NFSv4,挂载参数建议显式写 nfsvers=4.2。
  • 变更前先 getfacl -R 导出基线,便于出问题时回滚。

原理说明

ACL 由若干条目组成,每条是「类型:限定符:权限」的三段式结构:

  • u:alice:rwx 是命名用户条目,只对 alice 生效。
  • g:ops:r-x 是命名组条目,对 ops 组的全体成员生效。
  • m::r-x 是 mask,所有命名用户、命名组以及属组权限的上限。
  • o::--- 是其他人条目,语义等价于 chmod 的 other 位。

这里最容易踩的坑是 mask。命名条目上写的权限不是最终生效权限,而是「条目权限与 mask 的交集」。也就是说 u:alice:rwx 配上 m::r-x,alice 实际只有读和执行。getfacl 输出里带 #effective: 注释的行,就是被 mask 削弱后的真实权限。排查「明明给了写权限却写不进去」这类问题时,先看这一行,能省掉大量无用排查。

目录还可以携带 默认 ACL,即所有以 d: 开头的条目。默认 ACL 不控制目录本身,只决定在该目录下新建的文件和子目录继承什么权限。没有默认 ACL 的目录,新建对象只按 umask 走,之前设置的 ACL 全部丢失——这是「设了 ACL 但第二天失效」的头号原因。

还需要区分 POSIX ACL 与 NFSv4 ACL 的能力差异。POSIX ACL 只能授权、不能显式拒绝,且权限只有读、写、执行三种;NFSv4 ACL 支持 deny 条目和十余种细粒度权限(追加写、删除子项、写属主等),在 NFSv4 共享上表达力更强,但工具链完全不同(nfs4_getfacl 与 nfs4_setfacl)。两者不要混用同一套自动化脚本,否则会出现「本地生效、挂载后失效」的怪现象。

操作步骤

步骤 1:确认文件系统与工具支持

# 查看根分区挂载参数是否包含 acl
findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS / | tr ',' '\n' | grep -E 'acl|ext4|xfs'

# ext4 可显式开启 acl 支持(xfs/btrfs 默认已开启)
sudo tune2fs -o acl /dev/sda1

# 工具是否就绪
which getfacl setfacl || sudo apt install -y acl

# 查看目录当前权限与 ACL
getfacl -p /var/www/html

如果 findmnt 输出里没有 acl 字样但文件系统是 ext4,先确认内核支持(grep -i acl /boot/config-$(uname -r)),再决定是改 fstab 还是用 tune2fs 开默认选项。

步骤 2:给单个用户授权(不加入属组)

# 让运维账号 alice 对站点目录可读写执行,其他人保持不变
sudo setfacl -m u:alice:rwx /var/www/html

# 目录下已有内容一并递归授权(-R 作用于所有子项)
sudo setfacl -R -m u:alice:rwX /var/www/html

# 确认结果:应出现 user:alice 条目与 mask 行
getfacl -p /var/www/html

递归时用大写 X 而不是小写 x。X 只对目录和原本已有执行位的文件追加执行权限,普通数据文件不会被误加 x,避免把一批文本文件变成可执行文件——这是权限加固里非常常见的自伤操作。

步骤 3:用默认 ACL 让新建文件自动继承

# 默认 ACL:之后在该目录新建的任何内容都继承这套条目
sudo setfacl -d -m u:alice:rwx /var/www/html
sudo setfacl -d -m g:www-data:r-x /var/www/html
sudo setfacl -d -m o::--- /var/www/html

# 验证继承是否生效
sudo touch /var/www/html/_test_inherit.txt
getfacl -p /var/www/html/_test_inherit.txt
sudo rm -f /var/www/html/_test_inherit.txt

默认 ACL 一旦设置,getfacl 输出里会出现整组 default: 开头的条目。注意两点:默认 ACL 只影响新创建的对象,对已存在的文件不生效,存量需要单独用 -R -m 处理;子目录会继承父目录的默认 ACL 作为自己的默认 ACL,但子目录自身的 access ACL 仍需单独授予。

步骤 4:mask 收敛,把权限压到最小

# 只保留读与执行,命名用户即使被授予 w 也无法真正写入
sudo setfacl -m m::r-x /var/www/html

# 确认 effective 已被削弱
getfacl -p /var/www/html | grep -E 'mask|effective'

这一步是 ACL 从「能用」到「安全」的分界线。日常授权容易越给越多,用 mask 统一压上限,可以在完全不动每条命名条目的前提下一次性收权;需要临时放开时只改 mask,事后改回即可,不必逐条回滚。

步骤 5:典型场景——上传目录只写不执行

# 上传目录:属主 root,Web 进程可写,其他人无任何权限
sudo chown root:root /var/www/html/uploads
sudo chmod 730 /var/www/html/uploads
sudo setfacl -m u:www-data:rwx /var/www/html/uploads
sudo setfacl -m g:www-data:rwx /var/www/html/uploads
sudo setfacl -m m::rwx /var/www/html/uploads
sudo setfacl -d -m u:www-data:rw- /var/www/html/uploads

# Web 层兜底:目录内禁止解析脚本
echo 'php_flag engine off' | sudo tee /var/www/html/uploads/.htaccess

注意 730 里最后一位 0 是 other 位。上传目录最常见的失守方式不是权限给大了,而是目录被设成 777 且 Web 服务器又能解析其中的脚本——两者叠加等于把上传通道和命令执行合成一条链。ACL 收敛权限、Web 层关闭解析,这两步必须同时做,只做一步都是纸面加固。

步骤 6:NFSv4 ACL(跨主机场景)

# 确认客户端走的是 NFSv4
mount | grep nfs
# 期望看到 nfsvers=4.2 以及 acl 相关参数

# 查看 NFSv4 ACL(工具来自 nfs4-acl-tools 包)
nfs4_getfacl /mnt/share/project

# 授予 alice 读与追加写,但不允许删除已有文件
nfs4_setfacl -a A::alice@example.com:rwaRtcy /mnt/share/project

# 显式拒绝某外部组遍历目录(POSIX ACL 无法表达)
nfs4_setfacl -a D::contractor@example.com:xtcy /mnt/share/project

权限串里 A 前缀表示 allow、D 前缀表示 deny;字母含义为 r 读、w 写、a 追加、x 执行、t 读属性、c 读 ACL、y 同步。需要「允许大多数人、排除少数人」的场景只能用 NFSv4 ACL,POSIX ACL 做不到显式拒绝。

步骤 7:批量审计与回滚

# 全量导出 ACL(含默认 ACL),作为变更前基线
sudo getfacl -R -p /var/www > /root/acl-baseline-$(date +%F).txt

# 列出所有带命名条目的路径,快速定位越权授权
sudo getfacl -R -p /var/www 2>/dev/null \
  | awk '/^# file:/{f=$3} /^user:[^:]+:/{print f, $0}'

# 回滚到基线
sudo setfacl --restore=/root/acl-baseline-2026-10-28.txt

# 找出 other 位可写的目录(高危,应当为零)
sudo find /var/www -type d -perm -0002 -exec ls -ld {} \;

--restore 会按文件里记录的路径逐条重放,因此基线文件必须保留完整路径(-p 参数的作用是保留绝对路径,去掉它会输出相对路径导致回滚错位)。

配置验证

ACL 的验证必须在目标用户身份下做,用 root 测等于没测——root 几乎无视所有权限位。

# 1. 以 alice 身份读(应成功)
sudo -u alice cat /var/www/html/index.php >/dev/null && echo "read OK"

# 2. 以 alice 身份写(mask 为 r-x 时应失败)
sudo -u alice touch /var/www/html/_acl_test 2>&1 | head -n1
# 期望输出 Permission denied

# 3. 确认 mask 与 effective 状态
getfacl -p /var/www/html | sed -n '1,12p'

# 4. 确认新建文件继承了默认 ACL
sudo -u www-data touch /var/www/html/uploads/t.txt
getfacl -p /var/www/html/uploads/t.txt

第 2 步如果写成功了,八成是 mask 没收敛,或者 alice 恰好还在属组里——属组权限与 ACL 是叠加关系,只看 ACL 会漏掉属组这条通路。验证时同步跑一次 id alice,确认目标账号的组列表里没有意外的属组。

常见问题

FAQ 1:设置好的 ACL 第二天就消失了,为什么?

两个高频原因。一是没有配置默认 ACL,新建文件只按 umask 走,原有授权不继承;二是有人执行了 chmod -R,chmod 会重算 mask,在部分实现下还会清除命名条目。排查顺序:先看 getfacl 输出里还有没有整组 default: 条目,再查变更时间点附近有没有 chmod 类操作记录,最后确认是否有配置管理工具在周期性重置权限。

FAQ 2:为什么明明给了 rwx,用户还是写不进去?

先看 getfacl 输出里的 #effective: 行。mask 是命名条目的权限上限,u:alice:rwx 配 m::r-x 时 alice 实际只有 r-x。除此之外还要逐层确认三件事:父目录是否有执行位(没有 x 无法进入,也就无法操作其下的文件)、SELinux 或 AppArmor 是否拦截、NFS 场景下服务端是否命中了 root_squash。

FAQ 3:ACL 和 chmod 会互相覆盖吗?

会。目录一旦带有任何 ACL 命名条目,ls -l 的权限位末尾会出现 + 号,此时 chmod 修改的对象是 mask 而不是属组权限位。运维脚本里同时使用 chmod 与 setfacl 时,务必把 setfacl 放在 chmod 之后执行,否则前面设好的授权会被重算掉。判据很简单:看到 + 号,就以 getfacl 的输出为准。

FAQ 4:ACL 条目太多会影响性能吗?

会有轻微影响但通常在可接受范围。ext4 把 ACL 存放在扩展属性里,内核会对权限判定结果做缓存,单个 inode 十几条条目对日常 IO 几乎没有可测差异。真正需要注意的是递归操作:对几十万文件的目录跑 setfacl -R 会产生大量元数据写入,建议放到维护窗口执行,避免和业务高峰期的备份任务叠加。

总结

ACL 不是 chmod 的替代品,而是补丁:chmod 负责粗粒度的属主属组划分,ACL 负责按人按事的例外授权,mask 负责兜底上限。三者配合才能逼近「最小权限」这个目标。落地时记住三条纪律:属组留空、按人授权;目录必须配默认 ACL,否则新文件全部漏掉;每次授权后用目标用户身份实测一次。做完这三条,就不会再出现「权限明明配了却不管用」或者「一个账号被拿下整台机器沦陷」的局面。