fapolicyd 应用白名单实战 2026:RHEL 9/10 允许列表、trust 数据库与 STIG 合规完整指南

从零部署 fapolicyd 应用白名单在 RHEL 9/10 上:permissive 观察期、规则语法、trust 数据库运维、STIG/CIS 合规基线,以及 18 个月客户项目里总结的容器、Ansible pull、JIT 编译器等生产陷阱与调优参数。

fapolicyd RHEL 9/10 白名单实战 (2026)

更新时间: 2026 年 9 月 20 日

fapolicyd 是 Red Hat 官方维护的应用白名单(允许列表)守护进程,它拦截内核 execve() 与 mmap() 调用,只允许 RPM 数据库或 trust 数据库中已知的可信二进制文件运行,从而在 RHEL 9/10、CentOS Stream 与 Rocky/Alma Linux 上阻止未知恶意软件、脚本 dropper 与 living-off-the-land 攻击。说实话,这套东西我第一次上生产是在一家股份制银行的核心区,踩坑之惨烈让我后来写了整整一份手册。本文就按我在银行与电信客户 RHEL 8/9 迁移项目中实测过的顺序,拆解 fapolicyd 1.3.x 的安装、规则语法、trust 数据库运维,以及怎样把它嵌进 STIG / CIS 合规基线里而不触发生产事故。

  • fapolicyd 通过内核 fanotify + FAN_OPEN_EXEC_PERM 权限事件在 execve 前 hook 二进制启动,策略拒绝会直接返回 EPERM。
  • 它与 SELinux 是互补关系:SELinux 决定"谁能访问什么"(MAC 标签),fapolicyd 决定"什么代码允许运行"(binary/script provenance)。
  • RHEL 9.4+ 默认 rules.d 已启用,升级到 fapolicyd 1.3 后 fagenrules --load 与 fapolicyd-cli --check-config 是每次改规则的强制流程。
  • trust 数据库有两个来源:RPM 数据库(rpmdb 后端)与本地 file trust 列表(/etc/fapolicyd/fapolicyd.trust + trust.d/)。生产上永远同时启用两者。
  • DISA STIG RHEL 9 V-257936 与 CIS Benchmark 1.6.1 都强制要求启用 fapolicyd,并且规则集必须处于 enforcing(非 permissive)模式。
  • 开启 debug-deny 模式在 /var/log/fapolicyd-access.log 里只记录被拒事件,是上线前发现"漏白名单"的唯一可靠办法。

fapolicyd 是什么以及它解决什么问题

fapolicyd(File Access Policy Daemon,文件访问策略守护进程)是 Red Hat 自 RHEL 8.1 起随发行版一起交付的用户空间应用允许列表引擎。它的定位其实很简单:操作系统层面的 allowlist,决定"哪些可执行文件与解释器脚本可以运行",与之相对的黑名单式反病毒完全是两种哲学。

它解决的攻击面在我做 STIG 加固时反复出现。攻击者拿到普通用户 shell,把静态编译的 chisel 或 ligolo-ng 从 /tmp 直接 chmod +x 后运行;或者用 Python/Perl one-liner 拉一个 dropper 到 /dev/shm 执行。fapolicyd 默认策略会拒绝所有非 RPM 数据库覆盖的可执行文件,直接把这类 living-off-the-land 手法卡在 execve() 前。

说白了这不算新概念。Bit9(现 Carbon Black App Control)、AppLocker(Windows)、Solaris trusted execution 都做过类似事情。fapolicyd 的独特之处是:与 RPM 深度集成、开源、无需内核模块、单进程守护、性能开销通常 < 2% CPU。上游代码在 linux-application-whitelisting/fapolicyd 维护,2026 年 3 月发布的 1.3.4 版本引入了改进的 mmap 处理与 IMA 签名后端。

在 RHEL 9/10 与 Rocky/Alma 上安装 fapolicyd

在 RHEL 9.4+ 与 RHEL 10 上,fapolicyd 已经在 BaseOS 仓库中。安装本身不复杂,但启用前必须先跑一次 permissive 观察期,否则你会在第一次重启时锁死运维通道(别问我怎么知道的)。

# 安装守护进程与 CLI 工具
sudo dnf install -y fapolicyd fapolicyd-selinux

# 先切到 permissive 模式:只记录,不拒绝
sudo sed -i 's/^permissive = 0/permissive = 1/' /etc/fapolicyd/fapolicyd.conf

# 启用 syslog 输出(默认写到 /var/log/fapolicyd-access.log)
sudo sed -i 's/^syslog_format = .*/syslog_format = rule,dec,perm,auid,pid,exe,:,path,ftype,trust/' \
    /etc/fapolicyd/fapolicyd.conf

# 启动并观察至少 48 小时
sudo systemctl enable --now fapolicyd
sudo systemctl status fapolicyd --no-pager

观察期是最重要的一步。我一般会让它跑一个业务日加一个批处理窗口(通常是周日晚间的备份 + patch 窗口),然后用 grep 'dec=deny' /var/log/fapolicyd-access.log | awk '{print $NF}' | sort -u 把所有"如果切到 enforce 就会被杀掉"的路径拉出来。典型漏网之鱼是 Zabbix Agent 的自定义脚本、Ansible pull 模式落地的 Python venv、以及运维私自扔在 /opt/scripts 里的 shell 脚本。

fapolicyd 工作原理:fanotify、trust 数据库与规则引擎

fapolicyd 内部由三个部分构成,理解它们的关系才能写出不误伤业务的规则。

fanotify 权限事件

守护进程通过 fanotify_init(FAN_CLASS_CONTENT | FAN_UNLIMITED_QUEUE) 拿到内核 fanotify 句柄,随后订阅 FAN_OPEN_EXEC_PERM(execve 前)与 FAN_OPEN_PERM(mmap 前)两类权限事件。内核在每次进程创建或库加载时会 阻塞 调用者,直到守护进程回写 FAN_ALLOW 或 FAN_DENY。这意味着 fapolicyd 崩溃或响应超时会导致系统级卡顿。1.3 版本在 fapolicyd.conf 中提供了 q_size = 800 与 rq_num = 4 参数用于调优队列。

trust 数据库

trust 数据库是 SQLite 文件,位于 /var/lib/fapolicyd/fapolicyd.trust.db。它记录每个"可信"文件的路径、大小与 SHA-256 摘要。查找命中需要三者都对得上,防止攻击者用同名文件替换。数据库来源有两个 backend,由 trust = rpmdb,file 配置项控制:

  • rpmdb backend:自动从 /var/lib/rpm 提取所有已安装 RPM 中标记为可执行(%files 中带 attr(0755, ...) 之类)的文件。RPM 升级后 fapolicyd 会自动通过 rpm-plugin-fapolicyd 触发数据库更新。
  • file backend:管理员手工维护的 /etc/fapolicyd/fapolicyd.trust 与 /etc/fapolicyd/trust.d/*.trust 片段。第三方软件、内部 CI 部署的二进制、自研 agent 都应该写到这里。

规则引擎

规则文件位于 /etc/fapolicyd/rules.d/,按数字前缀顺序加载(类似 sudoers.d)。fagenrules --load 会把所有 *.rules 拼成 /etc/fapolicyd/compiled.rules,守护进程运行时只读这一份。规则匹配是首命中优先,顺序至关重要。

规则语法详解与生产模板

每条规则的形式是 DECISION PERM SUBJECT : OBJECT。DECISION 是 allow、deny、allow_audit、deny_audit、allow_syslog、deny_syslog 之一。PERM 是 execute、open 或 any。SUBJECT 描述发起动作的进程,OBJECT 描述被访问的文件。

# /etc/fapolicyd/rules.d/40-custom.rules
# 允许来自 /opt/myapp/bin 的可信内部工具执行任何文件
allow perm=execute exe=/opt/myapp/bin/orchestrator : all

# 拒绝在世界可写目录中执行的一切,先于 rpmdb 规则命中
deny_audit perm=any all : dir=/tmp/
deny_audit perm=any all : dir=/var/tmp/
deny_audit perm=any all : dir=/dev/shm/

# 允许 root 从 RPM 覆盖的路径执行可信二进制
allow perm=execute uid=0 : trust=1

# 允许 Python/Perl/Bash 解释器打开可信脚本
allow perm=open exe=/usr/bin/python3.11 : ftype=text/x-python trust=1
allow perm=open exe=/usr/bin/bash : ftype=text/x-shellscript trust=1

# 默认拒绝所有剩余情况
deny_audit perm=any all : all

几个容易踩坑的字段:

  • trust=1 表示对象必须在 trust 数据库中并且 hash 一致。写规则时把它作为最后一道过滤,可以显著降低误报。
  • ftype= 使用 libmagic 判定 MIME 类型。text/x-python 与 text/x-python3 在不同 magic 数据库里不一致(RHEL 10 已经统一为 text/x-script.python),升级时要审查。
  • exe= 必须写绝对路径,不接受通配符;dir= 支持前缀匹配,末尾的 / 是强制的。

trust 数据库运维:fapolicyd-cli 与 fagenrules

日常运维八成时间花在维护 trust 数据库上。fapolicyd-cli 是官方 CLI,支持增删查改与完整性校验。

# 把 CI 部署的 agent 加入本地 trust 列表
sudo fapolicyd-cli --file add /opt/datadog-agent/bin/agent
sudo fapolicyd-cli --file add /opt/datadog-agent/embedded/bin/python3

# 一次性把整个目录加进去(1.3 新增 --recursive)
sudo fapolicyd-cli --file add /opt/myapp/ --recursive

# 列出所有本地 trust 条目
sudo fapolicyd-cli --list-trust-db | head -20

# 校验 trust 数据库中记录的 hash 是否仍然一致
# (如果部署脚本改了文件但忘记刷新 trust,这里会报 MISMATCH)
sudo fapolicyd-cli --check-trustdb

# 每次改完 rules.d 或 trust.d 都要重新加载
sudo fagenrules --check
sudo fagenrules --load
sudo systemctl reload fapolicyd

fagenrules --check 只做语法检查而不会真正加载,所以 CI 流水线里必须先跑它再 --load,避免语法错的规则文件把守护进程逼进 fail-closed 状态。1.3 之前的版本没有 --check,只能靠 fapolicyd --debug --no-details 前台跑一遍。

对于 Ansible 部署的场景,我在systemd 服务安全加固实战指南那篇里提到的 Ansible role 结构可以直接复用:trust.d 片段作为 template 下发,handler 里链式触发 fagenrules --load 与 systemctl reload fapolicyd,失败时立即回滚。

fapolicyd 与 SELinux 有什么区别?

这是客户方最常问的问题,尤其在同时启用两者时。答案是:两者控制的维度不同,应该同时启用,配合使用。

维度fapolicydSELinux
控制对象可执行文件与解释器脚本的 身份(hash + path)进程与文件的 标签(security context)
决策依据trust 数据库(RPM + 本地 hash)MAC 策略模块(.te/.cil)
阻止手段execve/mmap 前返回 EPERM任何 syscall 前返回 EACCES
典型防御场景未知 dropper、静态编译红队工具已授权进程越权访问、容器逃逸
误报排查/var/log/fapolicyd-access.logausearch -m AVC + audit2allow
性能开销1–2% CPU,首启动加载 trust db 稍慢< 1% CPU,策略加载在 boot

举个我在客户现场亲眼见过的例子。攻击者上传了一个静态编译的 chisel 隧道到 /tmp,并且成功利用一个 setuid 提权漏洞跑起来。SELinux 的 user_t 或 unconfined_t 可能允许这次 execve,但 fapolicyd 会因为这个二进制不在 trust 数据库里直接拒绝,攻击链在第一步就断掉。反过来,如果攻击者利用了一个可信 RPM 包中的日志解析器 SSRF,fapolicyd 无能为力,但 SELinux 的 httpd_t 策略会阻止它访问 /etc/shadow。深度防御的价值就在这里。

如果你还没有把 SELinux 从 permissive 切到 enforcing,先看SELinux 强制访问控制 CIL 与容器加固实战 2026,把两者一起规划比单独部署省得多。

如何排查 fapolicyd 拒绝阻塞?

fapolicyd 的排障有一套固定套路,顺序错了会浪费很多时间。

1. 确认日志到底是不是 fapolicyd 拒绝的

用户报"命令执行报 Permission denied",第一件事不是看 fapolicyd,而是先看 strace -f -e execve command 的返回码。EPERM(1)通常是 fapolicyd 或 SELinux;EACCES(13)大概率是文件模式或 ACL。

# 在 access log 里找匹配的拒绝
sudo grep 'dec=deny' /var/log/fapolicyd-access.log | tail -20

# 输出示例
# rule=13 dec=deny_audit perm=execute auid=1000 pid=48521 \
#   exe=/usr/bin/bash : path=/tmp/exploit ftype=application/x-executable trust=0

# rule=13 就是命中的规则序号,对应 compiled.rules 里的第几条
sudo grep -n '' /etc/fapolicyd/compiled.rules | sed -n '13p'

2. 用 debug-deny 模式定位漏白名单

fapolicyd 1.2+ 支持 --debug-deny,只输出被拒事件到 stderr,不会污染日志。在灰度环境跑一遍业务测试,拿到完整拒绝列表:

sudo systemctl stop fapolicyd
sudo /usr/sbin/fapolicyd --debug-deny 2>&1 | tee /tmp/fapd-deny.log
# 在另一个终端跑业务测试,Ctrl-C 结束

# 提取所有被拒的 exe/path 组合
awk '/dec=deny/ {for(i=1;i<=NF;i++) if($i ~ /^(exe=|path=)/) printf "%s ", $i; print ""}' \
    /tmp/fapd-deny.log | sort -u

3. 使用 fapolicyd-cli --check-status

1.3 新增的 --check-status 会打印守护进程内部统计:队列深度、trust 数据库命中率、被拒事件数。生产环境把这个数据 export 到 Prometheus(通过 textfile collector)是发现规则退化的有效方法。

STIG 与 CIS 合规基线集成

让 fapolicyd 从"技术手段"变成"合规交付物",是很多客户上这个技术的直接驱动力。

DISA STIG for RHEL 9

DISA STIG for RHEL 9(V1R4,2026 年 6 月发布)明确要求(V-257936、V-257937、V-257938):

  • fapolicyd 必须已安装且 systemctl is-enabled 为 enabled;
  • 配置文件 permissive = 0(enforcing);
  • 规则集必须至少包含一条 catch-all deny。通过 fapolicyd-cli --list 检查最后一条是否为 deny_audit perm=any all : all。

OpenSCAP 扫描规则 xccdf_org.ssgproject.content_rule_package_fapolicyd_installed 与配套的 enable_fapolicyd、fapolicyd_default_deny 三条会自动化上述检查。SCAP Security Guide 0.1.73 与更高版本已经把它们捆进 stig 与 cis profile。参考 ComplianceAsCode/content 项目文档可以自定义 tailoring。

CIS Benchmark for RHEL 9

CIS RHEL 9 v2.0.0(2026 年 4 月)第 1.6 节新增了 fapolicyd 建议,包括 1.6.1(安装)、1.6.2(启用)、1.6.3(启用 rpmdb backend)。CIS 的措辞比 STIG 温和,但审计交付时同样需要 fapolicyd-cli --check-config 无告警。

FIPS 与 fapolicyd 的关系

fapolicyd 本身不做加密,但它计算 trust hash 时会用到 OpenSSL 的 SHA-256。因此在 FIPS 模式(fips-mode-setup --enable)开启的机器上要确保 fapolicyd 版本 >= 1.3(旧版会调用非 FIPS-approved MD5 做 fallback)。查看 Red Hat Enterprise Linux 9 安全强化官方文档确认最新最佳实践。

生产陷阱与性能调优

在过去 18 个月我在客户那边遇到的、fapolicyd 相关的事后复盘里,反复出现下面几个问题。老实说,前两个我自己也在灰度阶段被咬过。

陷阱 1:容器工作负载被误杀

Podman/Docker 会把镜像层解包到 /var/lib/containers/storage/overlay/,其中的可执行文件不在宿主机 rpmdb 里。默认策略会拒绝所有容器内的 execve。解决办法有两种:一是给 runc、crun 加豁免规则;二是干脆把容器目录从 fapolicyd 监控范围排除:

# /etc/fapolicyd/rules.d/30-containers.rules
allow perm=any exe=/usr/bin/runc : all
allow perm=any exe=/usr/bin/crun : all
allow perm=any exe=/usr/bin/conmon : all
allow perm=any dir=/var/lib/containers/ : all
allow perm=any dir=/run/containers/ : all

陷阱 2:Ansible pull + venv 触发大量拒绝

Ansible 拉取 role 时会在 /root/.ansible/tmp/ 创建临时 Python 脚本,execve 时被拒。生产环境要么把 tmp 目录纳入 trust(不推荐,攻击面变大),要么切到 Ansible 推送模式 + pipelining = True,让脚本直接通过 stdin 进 Python 解释器,绕开磁盘 execve。

陷阱 3:JIT 编译器与 mmap

Java(尤其是 HotSpot 的 codecache)、V8、LuaJIT 通过 mmap(PROT_EXEC) 写入 JIT 代码。fapolicyd 默认监控 FAN_OPEN_PERM 但不监控匿名 mmap,因此 JIT 一般不会被拒。但要注意:如果规则集里有 deny perm=open 覆盖 JVM 加载 .jar 的路径,整个应用会启动失败。

性能调优

大型系统(每秒数千次 execve)可以调整以下参数:

# /etc/fapolicyd/fapolicyd.conf
q_size = 4096            # fanotify 事件队列,默认 800
rq_num = 8               # 后端处理线程数,匹配 vCPU
integrity = sha256       # 或 size(更快但更弱)
subj_cache_size = 8192   # 主体缓存,减少 /proc 查询
obj_cache_size = 65536   # 对象缓存
allow_filesystem_mark = 0  # 禁用挂载点级别标记以避免 NFS 抖动

对于跨 NFS 的持久化 volume,务必设 allow_filesystem_mark = 0,否则 NFS 挂载重连会触发大量假事件,把 fapolicyd 逼进 fail-open。参考上游 fapolicyd GitHub releases 页面,每次升级都要看 CHANGELOG。

常见问题解答

fapolicyd 会显著拖慢系统吗?

在典型企业工作负载(Web 应用 + 数据库)下,fapolicyd 的 CPU 开销通常在 1–2% 之间。真正会被感知到的场景是启动瞬间 trust 数据库加载(RHEL 9 大约 500 ms)与短生命周期批处理脚本(每秒数百次 fork/exec)。生产上把 subj_cache_size 与 obj_cache_size 调大后,批处理性能可以恢复到无 fapolicyd 的 95%+。

如何临时禁用 fapolicyd 以便调试?

最安全的方式是 sudo systemctl stop fapolicyd,守护进程一停,所有 fanotify hook 立即解除,不会残留任何拒绝规则。切勿用 rm /etc/fapolicyd/rules.d/* 的方式"清空",空规则集会加载失败并让守护进程进入 fail-closed,直接锁死系统。

fapolicyd 支持 Ubuntu 或 Debian 吗?

技术上可以,上游源码支持任意基于 fanotify 的内核(>= 5.0)。但 Debian/Ubuntu 官方仓库没有打包,需要从源码编译并自己实现 dpkg backend(rpmdb 后端仅覆盖 RPM 系发行版)。生产建议还是留在 RHEL/Rocky/Alma 生态,Debian 上更成熟的替代是 AppArmor + fanotify 自研策略。

fapolicyd 能防御 Log4Shell 那类 RCE 吗?

可以拦下 后续 步骤但拦不了 初次 触发。Log4Shell 利用点是 JVM 内存中反序列化执行,fapolicyd 无法介入(没有 execve)。但如果 payload 试图从 /tmp 下载并运行 miner,fapolicyd 会在 execve 前拒绝,这就是"深度防御"的价值,把攻击链切断在能拦的一环。

fapolicyd 与 EDR(CrowdStrike Falcon、SentinelOne)冲突吗?

不冲突,但要注意 fanotify 全局队列容量。EDR 也通过 fanotify 或 eBPF 监听 execve,双方各自维持事件消费者。RHEL 内核对 fanotify 消费者数量没有硬限制,但 q_size 加起来超过内核 fs.fanotify.max_queued_events(默认 16384)时会丢事件。部署前用 sysctl -a | grep fanotify 检查并按需调高。

关于作者 Mateusz Wojciechowski

Mateusz spent eight years on the Red Hat consulting bench before going independent in 2024, embedded with banks and telcos rolling out RHEL 8 and 9 across regulated estates. Most of that work was SELinux policy debugging, FIPS-mode enablement, and cleaning up the kind of sudoers files that grow organically over a decade. He holds OSCP and RHCE, and maintains a small set of Ansible roles for STIG-hardened RHEL builds that a few European credit unions now run in production. Before Red Hat he was a junior sysadmin at Allegro in Poznan, mostly babysitting Postfix and learning why you don't run updatedb on an NFS root. Mateusz writes about the boring half of Linux security: package signing, audit daemon tuning, and the unglamorous work of actually reading journalctl output before paging anyone.