在实际生产环境里,NFSv4 ACL 是个很容易被低估的东西。RHEL 8 默认已经支持 NFSv4.2,很多管理员却还在用老一套的 POSIX ACL 挂载共享,等到出现“能删别人文件但看不到列表”“子目录权限怎么都继承不下去”这类问题时,才回头来找 NFSv4 ACL 的解法。这篇文章不是给你抄一条命令就完事,而是把 RHEL 8 上配置和优化 NFSv4 ACLs 的完整链路过一遍,包括为什么选它、ID 映射怎么处理、权限表达式怎么写、服务端和挂载参数怎么调、以及权限不生效时怎么一步步排查。
如果你正在搭一个多用户文件共享系统,或者想把现有 NFS 共享的权限控制升级到更细粒度,这篇文章可以直接照着做。我会以 RHEL 8 为环境,覆盖服务端和客户端两端操作,也包含不少我实际维护中踩过的坑。
1. 为什么生产环境要切换到 NFSv4 ACL 而不是继续用 POSIX ACL
1.1 POSIX ACL 在 NFS 共享场景下的表达力瓶颈
传统 NFS 共享大多依赖 POSIX ACL,也就是 setfacl 设置的那类权限。POSIX ACL 的模型其实只有三条基本规则:一个 owner、一个 owning group、一组其他人,再加上若干命名用户和命名组。它的最大问题是权限位不够语义化,比如”能往目录里创建文件但不允许删别人的文件”这种需求,POSIX ACL 很难干净地表达出来。目录的“写”权限在 POSIX 模型里同时决定了创建、删除和改名,你给了用户某个目录的写权限,就基本等于给了他在这目录里“横冲直撞”的资格,想用 ACE 去阻止删除操作?对不起,POSIX ACL 里根本没有 DELETE_CHILD 这种权限位。
NFSv4 ACL 则完全是另一套模型。它参照 Windows 的 ACL 设计,把权限拆成了 READ_DATA、WRITE_DATA、APPEND_DATA、DELETE_CHILD、DELETE、READ_ACL、WRITE_ACL、WRITE_OWNER 等十几类操作,并且每个 ACE 可以明确是 allow 还是 deny。也就是说,你可以允许一个开发组往共享目录里写文件,同时单独拒绝某个人执行删除子目录内文件的操作。放到文件共享场景里,这种控制粒度才是真正够用的。
1.2 NFSv4 ACL 的 ACE 结构与权限位详解
NFSv4 ACL 的基本单位是 ACE(Access Control Entry),格式大致如下:
text复制type:flags:principal:permissions
type 是 A 表示允许,D 表示拒绝。principal 可以是 OWNER@、GROUP@、EVERYONE@,也可以是具体的 user@domain 或 group@domain。permission 部分是 NFSv4 权限位的组合,我在配置时会高频用到下面这几个:
| 短权限位 | 对应权限 | 典型用途 |
|---|---|---|
| r | READ_DATA | 读取文件内容,列目录 |
| w | WRITE_DATA | 创建/覆盖文件 |
| a | APPEND_DATA | 追加写文件,创建子目录 |
| x | EXECUTE | 执行文件,进入目录 |
| D | DELETE_CHILD | 删除目录内子项 |
| d | DELETE | 删除文件本身 |
| t | READ_ATTRIBUTES | 查看基本属性 |
| T | WRITE_ATTRIBUTES | 修改属性 |
| n | READ_NAMED_ATTRS | 读扩展属性 |
| N | WRITE_NAMED_ATTRS | 写扩展属性 |
| c | READ_ACL | 读取 ACL |
| C | WRITE_ACL | 修改 ACL |
| o | WRITE_OWNER | 变更属主 |
这套结构的优势在于“控制权限本身”的权限也被单独拆开了。比如你可以给一个管理员 C(写ACL)和 o(改属主)权限,但不需要给他文件内容读写的权限,这在 POSIX ACL 下几乎不可能优雅实现。
1.3 RHEL 8 上 NFSv4 ACL 的实际状态
RHEL 8 默认使用 NFSv4.2,服务端内核已经支持 NFSv4 ACL,不需要额外编译内核模块。也就是说,只要你在 /etc/exports 里导出目录,并且客户端用 nfs4 挂载,就天然具备 NFSv4 ACL 能力。但有个容易忽略的点:RHEL 8 的命令行工具 nfs4_getfacl 和 nfs4_setfacl 归属于 nfs4-acl-tools 包,默认是不一定装的,需要用 dnf install nfs4-acl-tools 提前装好。
另外要注意,NFSv4 ACL 在服务器上真正生效时,会和传统的 Unix mode bits 做一次映射。你如果之前用 chmod 777 或者 POSIX setfacl 设置了权限,那么 NFSv4 ACL 会在这些权限之上叠加运行,而不是完全替代。这个特性在后面排错时非常重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 落地前的准备:ID 映射、导出选项与安全模型
2.1 ID 映射域是 NFSv4 ACL 的“主心骨”
NFSv4 协议在网络上传输的不再是 uid/gid,而是 user@domain 形式的字符串。内核拿到字符串后,要通过 idmapd 把名字映射回本地 uid/gid。很多 NFSv4 ACL 权限看着设置了,但客户端一访问就变成 nobody,根源几乎都在 ID 映射域不匹配。
RHEL 8 上默认的 ID 映射服务是 nfs-idmapd,需要关注 /etc/idmapd.conf 里的 Domain 字段。比如你所有客户端和服务端都在 example.com 这个域下,配置就得写成:
ini复制[General]
Verbosity = 0
Pipefs-Directory = /run/nfsidmap
Domain = example.com
[Mapping]
Nobody-User = nobody
Nobody-Group = nobody
这里的 Domain 必须在整个 NFS 环境中统一。如果服务端写 example.com,客户端写 corp.example.com,表面上都能挂载,但 ACL 里的 user@example.com 在客户端会被当成未知用户,最终映射成匿名 nobody,权限自然就乱了。建议在动手配置前先规划好域名,不要图省事忽略。
2.2 选择 sec= 安全模式:sys 还是 Kerberos
NFS 导出项的 sec= 参数决定了 RPC 层的安全模型。最常用的是 sec=sys,它直接靠 uid/gid 作为凭据,配置简单、性能好,但安全性最低,客户端可以用 root 伪造任意 uid。NFSv4 ACL 本身做得再精细,如果底层凭据可伪造,权限控制就等于裸奔。
更严谨的方案是 sec=krb5、sec=krb5i 或 sec=krb5p。这三个分别表示:只做身份认证、认证加完整性校验、认证加密载荷。在 RHEL 8 接入 FreeIPA 或 AD 域的环境里,我通常会建议至少用 sec=krb5i,既能保证用户身份可信,又比全加密的 krb5p 性能开销小。如果只是实验环境或者完全隔离的内网,sec=sys 可以先跑通功能,但正式文件共享系统我会直接设计成 Kerberos 认证。
2.3 /etc/exports 导出选项的取舍
在 RHEL 8 的 NFS 服务端,一个典型的安全导出项会是这样:
bash复制/data/shared 192.168.10.0/24(rw,sync,wdelay,no_subtree_check,root_squash,sec=sys)
几个关键选项我解释一下:
rw:允许读写,不需要改文件的共享只导出ro。sync:服务端必须把数据落盘后才响应客户端请求。虽然比async慢一点,但掉电不会大面积丢数据,生产环境我坚持用 sync。no_subtree_check:关闭子树检查,减少请求处理开销。代价是导出父目录的子目录时,可能无法及时感知子目录是否被重命名。普通文件共享场景收益远大于风险,可以用。sec=sys或者sec=krb5i/krb5p:根据上一节选型来定。root_squash保持默认开启,把客户端 root 映射为匿名用户。除非你有强理由,否则不要关。
导出后执行 exportfs -arv 重新加载即可。NFSv4 服务端还需要开启并启动相关服务:
bash复制systemctl enable --now rpcbind nfs-server
firewall-cmd --permanent --add-service=nfs
firewall-cmd --permanent --add-service=rpc-bind
firewall-cmd --permanent --add-service=mountd
firewall-cmd --reload
防火墙这一步很容易漏,但漏了客户端就会一直卡在挂载超时。
3. 把 NFSv4 ACL 跑起来的完整配置路径
3.1 服务端和客户端的安装清单
在 RHEL 8 上先确认 nfs-utils 和 ACL 工具都就位:
bash复制dnf install -y nfs-utils nfs4-acl-tools
服务端如果走 Kerberos,还要安装 krb5-workstation 并完成 kinit。客户端同样需要 nfs-utils 和 nfs4-acl-tools。这个包很小,别省。
3.2 服务端 export 与客户端挂载实例
假设服务端 IP 是 192.168.10.5,导出目录是 /data/shared,客户端挂在 /mnt/shared。服务端 /etc/exports 写入:
bash复制/data/shared 192.168.10.0/24(rw,sync,no_subtree_check,root_squash,sec=sys)
然后:
bash复制systemctl restart nfs-server
exportfs -arv
showmount -e 192.168.10.5
客户端执行:
bash复制mkdir -p /mnt/shared
mount -t nfs4 -o rw,nconnect=4,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 192.168.10.5:/data/shared /mnt/shared
这里我用了 nconnect=4,让客户端和服务端之间建立 4 个 TCP 连接,提高并发吞吐。挂载完成后,用 nfs4_getfacl /mnt/shared 能看到当前 ACL 的初始状态。默认情况下几乎所有权限都映射给 owner、group 和 everyone,这和传统 mode 是对应的。
3.3 用 nfs4_setfacl 设置第一条真正细粒度的 ACE
先做一个需求:让 staff 组对共享目录有读取和创建文件权限,但禁止删除目录内已有文件。
bash复制nfs4_setfacl -a "A:g:staff@example.com:rwaDxtTnNcCy" /data/shared
nfs4_setfacl -a "D:g:staff@example.com:rwaDxtTnNcCy" /data/shared
等一下,这里有个很容易犯迷糊的地方:D 有两种含义。ACE 字符串开头的大写 D 表示 Deny 类型;权限位里的小写 d 表示 DELETE 权限。所以示例里 A:g:staff@example.com:rwaDxtTnNcCy 是允许读、写、追加、执行、读取属性、读取 ACL 等;如果我要拒绝删除,可以在前面加一条 Deny 类型的 ACE。
更好的做法是只删掉 d 和大写 D 的授权,也就是写权限里不给 DELETE_CHILD。要拒绝“删除他人文件”又不影响创建文件,应使用权限位的组合精确控制。命令如下:
bash复制nfs4_setfacl -a "A:g:staff@example.com:rwatTnNcCy" /data/shared
这条 ACE 给了读取、创建/追加、执行和读取属性,但没有给 D(删除子项)和 d(删除文件)。实际效果就是用户可以创建新文件,但无法删除已有内容。POSIX ACL 写不出这种配置。
3.4 继承规则:让新文件和子目录自动带上权限
NFSv4 ACL 继承靠 ACE flags 实现,常用三个标志:
| 标志 | 含义 |
|---|---|
| f | 允许继承到文件 |
| d | 允许继承到子目录 |
| i | 只用于继承,不作用于当前目录 |
| n | 继承后不再继续向下传播 |
假设要让 /data/shared/project 下所有新建文件都由 devs@example.com 继承读写权限,可以这样:
bash复制nfs4_setfacl -R -a "A:fd:devs@example.com:rwaDxtTnNcCy" /data/shared/project
A:fd: 表示这个 ACE 允许继承到文件和子目录,-R 表示递归应用到当前目录及已有子对象。-a 是追加 ACE,不会覆盖现有条目,这样比 -s 整体覆盖 ACL 更安全。
仔细看会发现我通常保留 c 和 C 权限。中大型共享环境里经常要有人负责目录权限运维,但不想给服务器 root。把 C(写ACL)授权给指定的运维组,他们就能在 NFS 挂载点上直接调整权限,这个操作在传统 NFS 场景下需要服务端登录,现在完全可以在客户端完成。
3.5 常用 ACL 策略设计
我维护的共享系统通常按下面这么拆分:
- 顶层目录给
OWNER@完全控制,GROUP@读写,EVERYONE@只读。 - 项目子目录给项目组“读写加删除子项”,但是 Deny 特定成员删除。
- 归档目录给所有人只读,仅仅
arch-admin组允许写入。 - 审计目录不在 ACL 层放行,而是通过导出项和防火墙隔离。
这样的策略可以先用 nfs4_setfacl 逐层建立,最后用 nfs4_getfacl -R 导出一份快照,方便核对和备份。
4. 优化:既要控制严格,又不能牺牲太多 IO 性能
4.1 服务端 nfsd 线程数和网络参数
NFSv4 ACL 毕竟要在每次文件访问时做权限判断,虽然内核里是线性遍历 ACE,但当 ACL 条目很多、文件访问频繁时,CPU 开销会上升。最直接的优化是把 nfsd 线程数调到一个合理值。默认 RHEL 8 的 nfsd 线程数偏保守,可以改 /etc/sysconfig/nfs:
bash复制RPCNFSDCOUNT=64
线程数怎么判断?一般从 CPU 核数的 4 倍起步,比如 16 核机器先给 64,用 nfsstat 和 top 看 nfsd 线程的 CPU 占用率。如果全部持续打满,就继续加到 8 倍;如果线程大部分空闲,说明瓶颈不在服务端计算。
网络方面,如果走 TCP,可以调整客户端挂载参数。RHEL 8 对高并发挂载推荐加入 nconnect=4 或更高,但不能盲目调太大,连接数太多反而会造成服务端多线程上下文切换开销。建议 4 到 8 之间压测选择。rsize 和 wsize 在 NFSv4.2 下通常直接给 1048576,也就是 1 MiB,吞吐量比默认的小块要好看不少。
4.2 挂载参数优化与安全性权衡
我常用的客户端挂载模板如下:
bash复制mount -t nfs4 -o rw,nconnect=8,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noatime,actimeo=30,sec=sys 192.168.10.5:/data/shared /mnt/shared
hard 表示服务端恢复前客户端挂载点不返回 IO 错误,避免进程读到半截数据。timeo=600 把超时时间调高到 60 秒,retrans=2 限制重传次数。noatime 可以明显减少每次读文件产生的属性写回,这个对共享目录性能帮助很大。actimeo=30 表示客户端属性缓存 30 秒,适合大多数文件改动不频繁的共享场景,能显著减少 GETATTR 请求。
如果想要更高安全性,可以把 sec=sys 换成 sec=krb5i。但注意,Kerberos 模式下 ID 映射和 ticket 缓存都会成为新瓶颈,客户端需要提前 kinit 或通过 SSSD 拿到 ticket。我一般只在真正需要防止伪造用户身份的环境里打开,普通办公文件共享还是用 sec=sys 配 NFSv4 ACL 就已足够。
4.3 ACE 顺序对访问评估性能的影响
NFSv4 ACL 的访问检查是从第一条 ACE 开始顺序评估,第一条匹配就不再往后看。这个机制是一把双刃剑。配置顺序不好时,每次文件访问可能要做多次 ACE 比较,权限列表越长越明显。
我曾经在一台共享服务器上见过某个项目目录的 ACL 有三十多条 ACE,每次打开文件都要线性扫描。后来把最通用的允许规则放到前面,把特殊例外放到后面,平均延迟立刻降了下来。操作上没有特殊命令,就是先把现有 ACL 导出,整理好顺序,再用 nfs4_setfacl -s 整体覆盖:
bash复制nfs4_getfacl /data/shared > /tmp/acl_backup.txt
nfs4_setfacl -s "A:g:staff@example.com:rwatTnNcCy,A:g:devs@example.com:rwaDxtTnNcCy,A::OWNER@:rwaDxtTnNcCy" /data/shared
这里的 -s 是把整条 ACL 列表当成一个字符串设置,多条 ACE 用逗号拼接。个人经验是每个文件或目录的 ACE 控制在 10 条以内,不仅访问快,也好维护。
4.4 安全加固组合
优化不能只盯着性能。权限控制再精细,如果导出面过大也没意义。我通常会做几层收敛:
- 按网段限制 exports,不要在
/etc/exports里写/24之外的宽松网段,更不要写全网。 - 对纯程序读取的目录,在导出项后加
ro,从协议层拦住写入。 - 对不需要执行文件的共享目录,客户端挂载时加
noexec。即使 ACL 意外放开了x,文件系统层面也不会执行,多一道保险。 - 对匿名用户,保持
root_squash开启,并在 idmapd.conf 里把 Nobody-User 指向一个完全没有文件权限的账户。
这些措施和 ACL 不是互相替代,而是叠加关系。ACL 负责“谁能做什么”,导出选项和文件系统挂载参数负责“整个共享允许什么”,两层都管住才叫安全。
5. 真实现场排错:ACL 不生效时的排查链路
5.1 客户端看到 nobody 或权限乱掉,先查 ID 映射
NFSv4 ACL 最常见的问题就是 ACL 设置了,但客户端看到文件 owner 是 nobody。我在排错时第一步永远不会去改 ACL,而是先看 ID 映射。
客户端执行:
bash复制nfsidmap -d
这个命令会输出当前缓存的 id 映射情况。如果出现 uid 0 对应 nobody@example.com 之类,说明服务端返回的 user@domain 在客户端映射失败。排查顺序是:
- 看服务端和客户端的
/etc/idmapd.conf的 Domain 是否一致。 - 看导出的 NFS4 域是否正确,可以直接在服务端用
nfs4_getfacl看输出里的域名后缀。 - 重启两端的
nfs-idmapd和nfs-server,清缓存。
如果只是改 idmapd.conf,不用重启整个 NFS,可以执行 nfsidmap -c 清空当前缓存,再重新访问测试。
5.2 ACL 命令本身的几个坑
nfs4_setfacl 上手容易,但有几类错误很隐蔽。
第一个坑是权限字符串大小写。小写 d 是删除文件,大写 D 是删除目录子项;小写 c 是读取 ACL,大写 C 是写 ACL。写错一个字符,权限语义完全不一样。
第二个坑是 principal 里的域名必须和 idmapd 配置一致。比如在 example.com 域下,写 A:g:staff@example.com 没问题,但写成 A:g:staff@local 会被当作未知实体,访问者依然无法获得预期权限。
第三个坑是 -a 追加和 -s 覆盖的区别。很多人从头配 ACL 时用 -a 一直追加,忘记旧规则还在,最后规则叠加得莫名其妙。一次全新配置用 -s 更可控;在已有 ACL 上增加规则才用 -a。
5.3 日志层面的定位方法
如果客户端行为还是不对,我会上服务器开 RPC 调试日志:
bash复制rpcdebug -m nfsd -s all
日志会打到 /var/log/messages 或 journald 里,重点看 nfsd 的授权日志。它会记录每次访问命中的 ACE 或者拒绝原因。查看完记得关掉:
bash复制rpcdebug -m nfsd -c all
另外可以配合 nfsstat 看服务端收到哪些类型的请求,判断是权限拒绝还是客户端缓存导致的旧属性。如果客户端一直看到旧的权限,可以先尝试重新挂载,或者用 mount -o remount 刷新。
5.4 NFSv4 ACL 与老式 chmod/POSIX ACL 的兼容陷阱
NFSv4 ACL 和传统模式位之间不是“非此即彼”,系统会尽量维持两者同步,但这种同步有时会给人意外。比如你在客户端对某个文件执行 chmod 750,内核会重写 NFSv4 ACL 中与 mode 相关的部分,可能导致之前精心设置的细粒度 ACE 被覆盖或隐藏。反过来,你设置完 NFSv4 ACL 后,用传统 getfacl 看,也只是看到 mode 映射后的简略结果,看不到完整 ACE。
所以维护 NFSv4 ACL 的文件共享时,一定要坚持用 nfs4_getfacl 和 nfs4_setfacl 作为权限操作工具,并明确告诉团队不要再对 NFS 挂载点下 chmod。如果发生事故,可以从定期备份的 ACL 快照恢复,这也是我把 ACL 快照纳入备份脚本的原因。
6. 基于大量维护后的几点个人体会
6.1 ACL 快照与变更留痕
在多人维护的 NFS 环境里,ACL 变更没有审计基本等于裸奔。我每隔一段时间会导出全量 ACL 快照存档:
bash复制nfs4_getfacl -R /data/shared > /backup/nfs4acl_$(date +%F).txt
恢复时不用依赖手工一条条敲,把备份文件里的 ACE 按路径重新应用即可。注意不同版本的 nfs4-acl-tools 在导出格式上有细微差异,恢复前一定要先检查文件头部注释,功能验证没通过不要直接上生产。
6.2 不要把所有权限都塞进 ACL
我见过一些人把所有业务规则全部用 ACE 硬编码,结果权限列表长得像论文。NFSv4 ACL 再强大,也不是设计用来做复杂业务审批流的。更合理的做法是通过目录结构规划权限:公共区、项目区、归档区各自独立导出或独立子目录,ACL 只负责小块边界控制。结构清晰比权限灵活更重要,出问题时也好定位。
6.3 保持内核补丁和 nfs-utils 版本更新
RHEL 8 的 NFSv4 ACL 行为会随内核小版本和 nfs-utils 更新而调整,修掉过不少与 idmapping 和 ACL 继承相关的 bug。长期运行的生产环境,我会每季度评估一次 NFS 相关补丁,而不是一直停留在初始版本上。安全和稳定性都是持续维护的结果,任何一次配置优化都不能一劳永逸。
