暗色模式

AppArmor 强制访问控制:从内核集成到自定义限制策略

技术教程
2026-08-29
5
0

AppArmor 强制访问控制:从内核集成到自定义限制策略

本文要点
  • 传统权限体系(DAC)连 root 都能读 /etc/shadow,AppArmor 用基于路径的"白名单"对进程做强制访问控制(MAC),root 也不例外。
  • Ubuntu 默认启用且 AppArmor 已编译进内核:aa-status 查看状态,三档模式 enforce / complain / unconfined
  • /etc/apparmor.d/ 写一个 profile 配置文件 + apparmor_parser 加载,就能限制任意二进制能访问哪些文件。
  • 未授权访问会被内核拒绝,并在 dmesg 留下 apparmor="DENIED" 审计记录(type=1400)。
  • 你天天在用的 snap、Docker 沙箱底层正是 AppArmor(aa-status 里的 docker-default)。

从 chmod 到 MAC:权限体系只解决了一半问题

接触 Linux 的第一天我们就学了 chmodchown:每个文件有所有者、属组和 bits(rwx),进程以"以何用户身份运行"来决定能不能碰它。这套体系叫 DAC(自主访问控制,Discretionary Access Control)——资源所有者自己决定谁能访问

但 DAC 有个天生漏洞:root 是超级用户,理论上能读一切。生产环境里大量"用户态漏洞利用 + 拿到 root 权限 → 直接读到 /etc/shadow、数据库密码、API 密钥"的攻击链,就是吃准了这一点。你写的程序是 root 在跑,它就拥有了 root 的文件访问能力,哪怕它大部分时候只需要读自己的两个配置文件。

MAC(强制访问控制,Mandatory Access Control) 则把决定权从"文件所有者"和"进程身份"手里拿走,交给系统管理员预先定义的策略:进程被允许做什么、不允许做什么,由策略统一裁决,被限制的进程即使跑在 root 下也越不过去

Linux 生态里主流的两个 MAC 实现是:

AppArmorSELinux
判定模型基于路径:把"程序 + 它能访问的路径规则"配对基于标签:给所有进程/文件打安全上下文,判断主体与客体类型是否允许
配置语言接近自然语言,/usr/bin/cat mr, 这样一行一条规则策略语言复杂、学习曲线陡
默认发行版Ubuntu、Debian、openSUSE、SUSERHEL、CentOS、Fedora、Rocky
上手难度

AppArmor 的思路更直白:"这个程序能碰哪些路径,白名单写清楚,没写的一律拒绝"。它不需要给成千上万个文件打标签,而是专注在"限制具体程序"。这正是运维给"某个自己部署的二进制"做最小权限时最顺手的工具。

AppArmor 在 Ubuntu 里默认就在工作

在 Ubuntu 24.04 上,AppArmor 默认安装并启用,无需任何额外配置:

$ systemctl is-active apparmor          # apparmor 服务
active
$ aa-enabled                            # 内核是否支持并启用
Yes
$ apparmor_parser --version             # 用户态解析工具版本
AppArmor parser version 4.0.1

它甚至不是"可加载内核模块",而是直接编译进了内核(这也是 lsmod 里看不到它的原因):

$ ls /sys/module/apparmor/parameters
enabled
$ cat /sys/module/apparmor/parameters/enabled
Y

Y 表示 AppArmor 在内核里已可用。平时你可能感觉不到它,但它其实在默默保护好几类东西:

  • snap:Ubuntu 的 snap 软件包沙箱底层就是 AppArmor,每个 snap 应用都被强制包在一个命名空间 profile 里;
  • Docker:Ubuntu 上 Docker 默认会给每个容器套上 docker-default profile;
  • 系统服务与浏览器chronydrsyslogd、Firefox/Chromium 等都有自己的 profile。

第一眼看状态:aa-status

aa-statusapparmor_status 是同一个命令,用法完全一样,用它查看 AppArmor 当前全貌:

$ aa-status
apparmor module is loaded.
160 profiles are loaded.
66 profiles are in enforce mode.
   /snap/snapd/27591/usr/lib/snapd/snap-confine
   ...
   docker-default
   lsb_release
   ...
4 profiles are in complain mode.
   transmission-cli
0 profiles are in prompt mode.
0 profiles are in kill mode.
90 profiles are in unconfined mode.
   ...
10 processes have profiles defined.
10 processes are in enforce mode.
   /usr/sbin/chronyd (pid) 
   /usr/sbin/nginx (pid) docker-default
   /usr/sbin/rsyslogd (pid) rsyslogd
   ...
0 processes are in complain mode.

(上面是节选,实际还有很长的列表。)输出的信息其实就四大块:加载的 profile 总数、按运行模式分组的 profile 列表、处于各模式下的进程数、以及当前进程实际被哪个 profile 约束

aa-status 概览:模块加载与各模式 profile 列表

截图里能看到 enforce 模式列表的头部:snap-confine/usr/bin/manchronyddocker-defaultlsb_release 等,以及一大串 snap.chromium.*snap.cups.* 命名空间——这些就是上文说的 snap 沙箱。列表越长,说明系统里受保护的进程越多。

上面完整命令输出的末尾是"进程"部分,同样很有信息量:/usr/sbin/nginx (pid) docker-default 说明 Nginx 跑在 Docker 容器里,被约束的 profile 正是 docker-defaultrsyslogd 用的是自己的 rsyslogd profile。每个在跑的进程被哪条策略管着,一目了然。

三种运行模式:enforce / complain / unconfined

aa-status 把 profile 按模式分组,这是理解 AppArmor 的关键:模式决定"发现违规访问时怎么办"。

模式越界访问如何处理是否记录日志适用场景
enforce(强制)拦截记录正式上线、坚决执行策略(默认目标态)
complain(抱怨)放行记录试运行、收集程序真实访问路径
unconfined(未约束)放行不记录没配 profile 的程序、临时排障

AppArmor 4.0 还新增了两种模式:prompt(遇到越界访问不直接拦截,而是提醒管理员决定)和 kill(越界直接终止进程)。绝大多数场景,用前三档就足够。

从开发到上线,推荐的节奏是:complain 观察一段时间 → 根据日志补全规则 → 确认覆盖完整后切 enforcecomplain 时代价极低,是 AppArmor 比 SELinux 友好得多的地方。

动手写一个 profile:让 root 也读不到机密文件

理论讲完,直接实战。设想一个场景:你的服务二进制是 root 在跑,它的配置目录 /opt/demo/ 里躺着一份含数据库密码的文件 secret.env。你希望它能正常工作(使用已有配置),但绝对读不到这份密码文件

第 0 步:看基线——没约束时 root 想读就读

先造一个演示用的假"密码文件",并确认当前能读到:

$ mkdir -p /opt/demo
$ printf 'DATABASE_PASSWORD=ChangeMe_DoNotLeak\nAPI_TOKEN=sk-fake-0001-xxxx\n' > /opt/demo/secret.env
$ cat /opt/demo/secret.env
DATABASE_PASSWORD=ChangeMe_DoNotLeak
API_TOKEN=sk-fake-0001-xxxx

没有任何限制,root 想读就读。这就是 DAC 的边界。

第 1 步:写 profile 文件

AppArmor 的配置放在 /etc/apparmor.d/,文件命名规则很直观:把可执行文件的绝对路径去掉开头的 /,再把 / 替换成 .。要限制 /usr/bin/cat,文件名就是 usr.bin.cat(生产环境把这个"演示二进制"换成你自己的服务即可)。

$ vim /etc/apparmor.d/usr.bin.cat

内容如下:

#include <tunables/global>

profile usr.bin.cat /usr/bin/cat flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  #include <abstractions/consoles>

  /usr/bin/cat mr,
  /opt/demo/ r,
}

逐行解读一下这个配置:

  • profile usr.bin.cat /usr/bin/cat { ... }:声明一个名为 usr.bin.cat 的 profile,并把它绑定到 /usr/bin/cat 这个程序。profile 内声明的名字要跟文件名一致,否则系统加载时会告警。
  • #include <tunables/global>:必须,提供了 @{PROC} 这类变量定义,几乎每个 profile 都要有。
  • #include <abstractions/base>:引用"基础抽象"。它是一组最小公用的路径规则(动态库、时区、locale、/dev/null 等),避免你从头手写几百行。
  • /usr/bin/cat mr,:允许读取并内存映射运行 cat 这个二进制本身(m=可被 mmap 执行,r=读)。
  • /opt/demo/ r,:允许读取 /opt/demo/ 目录本身(列目录需要),但注意:没有给它下面的文件加任何读规则

AppArmor 的路径规则是 glob 风格的/opt/demo/ 指目录本身,/opt/demo/* 指下一级,/opt/demo/** 才递归覆盖所有层级。权限位用单字母:r 读、w 写、a 追加、m 内存映射执行、k 文件锁、l 创建链接。多条规则可以逗号写在一行;没写进规则的访问,一律拒绝(默认拒绝模型)——这正是我们要的效果。

第 2 步:加载并验证 enforce 模式

解析并加载配置(-r 表示"重载/加载",-a 是纯新增,-R 是移除):

$ apparmor_parser -r /etc/apparmor.d/usr.bin.cat
$ aa-status | grep usr.bin.cat
   usr.bin.cat

再数一下 enforce 列表:加载前后从 65 变成 66 个 profile,usr.bin.cat 已经在列。

现在同样一个 root 用户、同一个 /usr/bin/cat 二进制,再读机密文件:

enforce 模式:未授权访问被拒绝

$ cat /opt/demo/secret.env
cat: /opt/demo/secret.env: Permission denied

被拒绝了。这就是 MAC 和 DAC 的本质区别:文件权限(DAC)允许 root 读一切,但 AppArmor 策略(MAC)说"这个程序不能读它",root 也无可奈何。而"读目录本身"、读被 base 抽象放行的 /etc/os-release 这类路径依然正常——白名单只砍掉你明确不想给的,不搞一刀切

$ cat /etc/os-release | head -1
PRETTY_NAME="Ubuntu 24.04.4 LTS"

第 3 步:去内核日志里看到证据

拒绝并不是偷偷发生,内核审计会把每一次拦截记下来,dmesg 直接可见:

内核审计日志记下 DENIED 记录

$ dmesg | grep secret.env | grep DENIED | tail -2
[2294428.662940] audit: type=1400 audit(1787961870.037:348): apparmor="DENIED" operation="open" class="file" profile="usr.bin.cat" name="/opt/demo/secret.env" pid=3168831 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
[2294461.082401] audit: type=1400 audit(1787961902.458:358): apparmor="DENIED" operation="open" class="file" profile="usr.bin.cat" name="/opt/demo/secret.env" pid=3168955 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0

抓着 audit: type=1400 这条审计记录,逐字段解读:

  • apparmor="DENIED":这是拒绝事件(在 complain 模式会是 ALLOWED);
  • operation="open"+requested_mask="r":请求的操作是"打开文件来读";
  • profile="usr.bin.cat":是哪条策略拦的;
  • name="/opt/demo/secret.env":想碰的是哪个文件;
  • fsuid=0发起访问的进程是 rootfsuid=0 就是文件系统层面的 UID 0)——再次印证"rule 优先于 root"。

生产中的调试套路:complain 先行,日志辅助

手动写规则一次写对很难。生产环境的正确姿势是让程序自己告诉你它需要什么

  1. 先切 complain:所有越界访问一律放行、只记日志,程序不受影响:
$ aa-complain /usr/bin/cat
Setting /usr/bin/cat to complain mode.
  1. 正常跑一轮业务,然后看记录,确认程序真正需要哪些路径。complain 模式下被记录的事件在 dmesg 里标记为 ALLOWED,同样带 denied_mask 字段指出它想要但没被授权的权限:
$ dmesg | grep apparmor | grep 'comm="cat"' | tail -2
[2294429.239354] audit: type=1400 audit(1787961870.614:350): apparmor="ALLOWED" operation="open" class="file" profile="usr.bin.cat" name="/opt/demo/secret.env" pid=3168838 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
  1. 把日志里出现的合法路径补进 profile(这就是 ${name} 那列派上用场的地方)。嫌手写麻烦,aa-logprof 能自动读取日志逐条询问"要不要放行";aa-genprof 则可以对一个指定程序从零生成配置。两者都是交互式问答,很适合快速搭底稿。
  2. 确认放行列表完整后切回 enforce,正式生效:
$ aa-enforce /usr/bin/cat
Setting /usr/bin/cat to enforce mode.
  1. 想彻底移除这套限制:aa-disable /usr/bin/cat(或 apparmor_parser -R /etc/apparmor.d/usr.bin.cat 后删除该文件)。

另外,aa-unconfined 值得定期跑一次,它扫描当前所有进程,揪出在生产环境裸奔(没有任何 profile 约束)的程序,是"看看谁还没被保护"的便捷工具。

日常生态:snap 与 Docker 都在吃这套

AppArmor 不是冷门配置,你已经在用它,只是没察觉:

  • snap 沙箱:snap 强调"隔离",隔离的底层实现之一就是 AppArmor。每个 snap 应用装好后,/etc/apparmor.d/ 里就会多出 snap.* 系列,aa-status 里那一大串 snap.chromium.*snap.cups.* 就是它们。你可以把整套"一键安装、自动沙箱"理解为 AppArmor + namespace 等机制的组合拳。
  • Docker 容器:在 Ubuntu(以及 Debian/openSUSE)上,Docker 默认就用 AppArmor 的 docker-default profile 把每个容器框起来。前面 aa-status 的进程列表里,容器里跑的 nginx 显示的就是 docker-default。如果出于某种原因想关掉(如跑需要特权的代理工具),可以 docker run --security-opt apparmor=unconfined ...,但代价是容器失去 AppArmor 保护,非必要不关

了解这一点后,你再看到"容器逃逸""snap 崩溃"之类的问题,第一反应就知道该把 dmesgaa-status 纳入排查清单了。

总结

AppArmor 给"root 大包大揽"的 Linux 权限体系补上了强制访问控制这一环:基于路径 + 白名单 + 默认拒绝,把"程序能碰什么"这件事从程序和用户手里收归到系统管理员定义的策略里。它比 SELinux 好上手的多,Ubuntu 上零配置即用。

实操要点回顾:

  1. 看状态aa-statusaa-enabledcat /sys/module/apparmor/parameters/enabled
  2. 写策略:在 /etc/apparmor.d/ 放一个绑定到目标可执行文件的 profile,路径即规则,没写即拒绝;
  3. 换模式apparmor_parser -r 加载、aa-complain 试跑观察、aa-enforce 收紧上线、aa-disable 卸载;
  4. 查证据dmesg / journalctl -k 里的 type=1400 审计记录,DENIED/ALLOWED 一目了然;
  5. 记生态:snap 隔离与 Docker 默认安全配置都依赖 AppArmor。

给对外暴露的服务二进制(Web 服务、数据库同步程序、备份脚本)配上 AppArmor profile,是成本极低的"纵深防御"——就算程序被攻破、拿到 root,也只是被白名单框住的最小实体。根因防御从策略开始:别把所有赌注押在"我的程序不会出漏洞"上。

参考链接

发表评论

暂无评论,快来抢沙发吧!