AppArmor 是 Linux 内核提供的强制访问控制框架,它不关心进程属于哪个用户,而是根据可执行文件路径为进程划出一块可以访问的边界。Ubuntu、Debian、SUSE 等发行版默认启用它,很多服务启动后看似权限正确却仍然报 Permission denied,通常就是 AppArmor 配置没有放行对应路径。配置文件默认存放在 /etc/apparmor.d/ 目录,文件名一般将二进制的绝对路径中的 / 替换为 .,例如 /usr/sbin/nginx 对应 usr.sbin.nginx。要编写一份可靠配置,需要理解 profile 结构、权限字母、路径匹配、变量与抽象层,以及调试加载流程。

一、AppArmor 配置文件的基本结构
每个 AppArmor 配置文件的核心是一个 profile 块,块头写可执行文件的绝对路径,块内写入具体规则。文件头部通常会通过 include 指令引入全局可调参数,例如 #include <tunables/global>,这样可以在统一位置定义变量。下面是一个最小化的 Nginx 配置示例,它允许绑定网络端口、写日志和 PID 文件,同时显式拒绝读取 /etc/shadow。
#include <tunables/global>
/usr/sbin/nginx {
#include <abstractions/base>
capability net_bind_service,
/var/log/nginx/*.log w,
/run/nginx.pid w,
deny /etc/shadow r,
}
profile 名称后面可以追加一对括号写入标志,例如 flags=(complain)。在 complain 模式中,AppArmor 只记录越权行为而不真正阻止,适合新配置上线前的观察期;enforce 模式是默认策略,会立即阻断未授权访问。多个标志用逗号分隔,例如 flags=(complain,attach_disconnected)。
如果同一个可执行文件需要不同启动参数或不同运行环境,可以在一个文件中定义多个 profile,并用 profile 关键字加名称区分。每个 profile 独立加载、独立切换 enforce 或 complain 模式,便于灰度测试和回滚。
二、权限规则与路径匹配
权限规则是 profile 的主体内容,每条规则通常由路径和权限字母组成,并以逗号结束。r 表示读取,w 表示写入,m 表示允许将文件映射到内存,k 表示允许锁定文件,l 表示允许创建硬链接,x 表示执行。权限字母可以组合出现,例如 rw 表示读写。
执行权限需要单独说明,因为 AppArmor 对执行进行了细分。ix 表示继承当前 profile,适合执行同一应用内部的脚本;px 表示切换到目标文件的独立 profile;Px 在切换前清理环境变量;ux 表示以不受 AppArmor 限制的方式执行;Ux 在 ux 基础上同样清理环境。如果希望执行一个不被 AppArmor 管理的程序,使用 ux 或 Ux,但安全风险较高。
/opt/app/bin/worker px, /usr/bin/python3 ix, /opt/app/tools/backup.sh Ux,
路径匹配支持多种通配符。* 匹配当前目录下的任意字符但不会跨目录,** 匹配任意层目录,? 匹配单个字符,[abc] 匹配字符类,{a,b,c} 表示花括号展开。例如 /var/www/** r 可以覆盖 /var/www 下所有子目录和文件;/home/*/.ssh/authorized_keys r 则只匹配一级用户目录下的 SSH 公钥文件。
除了允许规则,deny 用于显式拒绝。当 allow 规则较宽时,deny 可以精确屏蔽敏感文件。例如允许 /etc/** r 但拒绝 /etc/shadow r。AppArmor 在处理规则时更具体的路径优先,但明确 deny 会覆盖宽泛允许,因此把 deny 规则放在文件开头或靠近相关 allow 规则位置都是常见做法。owner 关键字可以进一步限制只有进程属主与目标文件属主一致时才匹配,适合多用户环境中的家目录访问。
deny /etc/shadow r, owner /home/*/tmp/** rw, /var/www/** r,
三、变量、别名与抽象层
当多个规则反复使用同一路径前缀时,可以用变量减少重复。变量定义使用 @{变量名}=值 的形式,引用时写成 @{变量名}。变量可以定义在当前 profile 文件内,也可以放在 /etc/apparmor.d/tunables/global 中统一维护。典型场景是应用根目录或用户家目录。
@{APP_HOME}=/srv/myapp
@{APP_HOME}/** rw,
@{APP_HOME}/tmp/** rw,
include 指令用于引入可复用的规则片段。系统自带的抽象文件位于 /etc/apparmor.d/abstractions/ 目录,例如 base 抽象中包含读取 /etc/localtime、/proc/meminfo 等常见权限,nameservice 抽象允许 DNS 解析所需的库和套接字访问。自定义抽象也可以放在同一目录,通过 #include <abstractions/自定义名称> 引用。
模块化组织的好处是降低维护成本。一个复杂服务可能会依赖数据库、缓存、日志目录,把这些权限抽象成独立文件后,多个 profile 可以复用同一组规则。例如创建 /etc/apparmor.d/abstractions/myapp-base,写入日志和 PID 文件权限,然后在主 profile 中直接 include。
#include <abstractions/base> #include <abstractions/nameservice> #include <abstractions/myapp-base>
四、编写与调试流程
写完配置文件后,需要先用 apparmor_parser 检查语法。常用的 -Q 选项表示静默模式,语法正确时不输出任何内容;-r 用于替换已加载的 profile;-R 用于移除。首次加载一个不存在的 profile 时,使用 -a 添加,但在 Debian/Ubuntu 中直接 -r 也能完成替换并加载。
sudo apparmor_parser -Q /etc/apparmor.d/usr.sbin.nginx sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx
如果语法错误,apparmor_parser 会输出带行号的提示,常见错误包括规则末尾缺少逗号、变量未定义、通配符位置非法等。修复后重新加载,不需要重启系统。查看当前加载状态可以使用 aa-status,它会列出处于 enforce 和 complain 模式的 profile 数量。
规则不够用时,最好的方法不是手工猜测,而是通过 aa-logprof 读取系统日志中被 AppArmor 拒绝的操作,并交互式生成规则。日志通常位于 /var/log/syslog 或 /var/log/audit/audit.log,关键字段包括 operation、profile、name 和 requested_mask。例如下面的日志表示 /usr/sbin/nginx 想要打开 /data/www/index.html 但被拒绝。
apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" pid=1234 comm="nginx" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
运行 sudo aa-logprof -f /path/to/logfile 后,工具会给出建议规则,确认后自动更新对应 profile 文件。需要注意的是,仅在 complain 模式下产生的日志会更完整,因此新配置可以先切换到 complain,运行一段时间业务后再切换回 enforce。
五、常见错误与最佳实践
第一个常见错误是规则漏掉逗号。AppArmor 解析器对行尾是否空格并不敏感,但逗号是规则结束符,缺失会导致下一行被误解或解析失败。开发时可以用编辑器插件辅助定位,保存后立即执行 apparmor_parser -Q 验证。
第二个常见错误是路径通配符的层级理解错误。* 不跨目录,** 跨目录,很多人希望用 * 匹配所有子目录下的文件,结果只匹配到了当前目录。例如 /var/log/* r 只能读取 /var/log 下的直接文件,不会放行 /var/log/nginx/error.log,应写成 /var/log/** r。
第三个常见错误是把服务临时迁就规则,使用 ux 或 Ux 执行外部程序,甚至直接为 profile 设置为 unconfined。这样虽然能解决 Permission denied,却完全失去了强制访问控制的意义。正确做法是持续使用 aa-logprof 收集需要的最小权限,必要时为被调用的外部程序创建独立 profile。
最佳实践上,建议从最小权限开始,初始只放行必要目录和 capability,然后通过 complain 模式观察日志补齐权限。对于敏感文件如 /etc/shadow、/root/.ssh 等,即使有较宽允许也要显式 deny。使用 include 抽象减少重复,并定期用 aa-status 检查是否有 profile 意外处于 complain 模式。生产环境变更后,应把配置纳入版本管理,便于审计和回滚。