Ubuntu的AppArmor默认策略很多时候并不能完全贴合实际业务场景。比如你部署了一个自定义的Web应用,日志需要写入非标目录,或者需要调用一个第三方二进制程序,系统自带的通用配置文件要么过于宽松,要么直接拒绝执行,导致服务异常。直接关闭AppArmor服务是极其危险的做法,这等于把应用暴露在零日漏洞和提权攻击的威胁之下。正确且高效的做法,是手写或修改AppArmor配置文件,为特定应用建立最小权限的沙箱。

理解AppArmor的工作模式与文件结构

在动手写配置之前,必须搞清楚AppArmor的两种模式:强制模式(enforce)和抱怨模式(complain)。强制模式下,违反策略的动作会被内核拦截并记录日志;抱怨模式则只记录违规行为,不实际阻止。调试新配置文件时,务必从抱怨模式开始,否则容易把系统服务卡死。使用aa-complain命令可以将配置文件置为抱怨模式,aa-enforce则切回强制模式。

AppArmor配置文件的存放路径分为两处:/etc/apparmor.d/存放的是正在使用的策略文件,/etc/apparmor.d/disable/存放的是已禁用的策略。文件命名有严格约定,通常把应用程序路径中的斜杠替换为点号。例如,针对/usr/bin/myapp的策略文件,应命名为usr.bin.myapp。这种命名不是随意的,AppArmor解析器依靠文件名来匹配可执行文件路径。

编写自定义配置文件的实战步骤

假设你有一个运行在/opt/customapp/bin/server的自研服务程序,需要读取/etc/customapp/下的配置,向/var/log/customapp/写入日志,并绑定8080端口。以下是完整的操作流程。

第一步,生成初始配置文件。使用aa-genprof工具启动一个交互式生成向导,它会扫描应用启动后的行为并自动生成规则。执行命令:

sudo aa-genprof /opt/customapp/bin/server

该命令会先扫描现有文件,然后提示你在另一个终端启动该应用程序,并尽可能触发所有功能,包括正常请求、错误处理、日志轮转等。完成操作后回到向导,选择扫描,AppArmor会分析系统日志,把检测到的文件访问、网络操作、进程执行等行为转换成策略规则。生成的基础配置通常包含大量的访问权限,此时策略还比较粗糙。

第二步,手工精细化调整配置文件。用编辑器打开/etc/apparmor.d/opt.customapp.bin.server,你会看到类似这样的结构:

#include 

/opt/customapp/bin/server {
  #include 
  
  /opt/customapp/bin/server mr,
  /lib/x86_64-linux-gnu/ mr,
  
  /etc/customapp/ r,
  /var/log/customapp/* w,
  
  network inet stream,
}

这个配置需要逐段理解。第一行#include <tunables/global>引入了全局可调参数,通常包含HOME目录变量定义等。花括号内是具体的规则块。#include <abstractions/base>引入了基础抽象层,允许应用加载动态链接库、读取时区信息等基本操作,不建议删除。

文件权限规则是核心。每一条规则由路径和权限掩码组成。权限掩码包括:r(读)、w(写)、m(内存映射执行)、k(文件锁定)、l(硬链接)、x(执行)。特别注意,执行权限x有几种变体:px表示执行时需要切换到子进程自己的策略文件;cx表示切换到子配置文件,通常用于同一个策略包内的子进程;ix表示继承当前策略执行,不切换;ux表示不加限制地执行,这通常是个安全隐患,应尽量避免。对于/opt/customapp/bin/server本身,通常需要mr权限,因为程序自身可能被内存映射。

网络规则同样重要。network inet stream允许TCP连接,network inet dgram允许UDP。如果你需要绑定特定端口,AppArmor本身不直接支持端口级别的控制,需要结合网络抽象层或使用更细粒度的网络规则,但在大多数场景下,限制协议族已经足够。如果你的应用只做HTTP服务,只保留inet stream即可。

处理子进程与脚本执行

很多应用并非单一二进制文件,可能会调用Python脚本、shell命令或者其他辅助工具。如果主配置文件中没有声明子进程的执行权限,AppArmor会直接拦截。例如,你的服务需要调用/usr/bin/python3来执行一个辅助脚本,就需要在主配置文件中加入:

/usr/bin/python3 ix,
/opt/customapp/scripts/helper.py r,

这里使用ix权限,意味着Python解释器将在当前策略限制下运行,无法突破沙箱限制。这是一种安全且实用的做法。如果子进程有自己独立的配置文件,比如你为辅助脚本也写了策略,那就用px或cx切换过去。

变量与抽象层的妙用

当配置文件中出现大量重复路径时,可以自定义变量。在主配置块开头使用@{变量名}定义,例如:

@{APP_HOME}=/opt/customapp
@{LOG_DIR}=/var/log/customapp

@{APP_HOME}/bin/server mr,
@{APP_HOME}/lib/ mr,
@{LOG_DIR}/* w,

这样修改路径时只需更改变量值,维护效率大幅提升。AppArmor还提供了丰富的抽象层文件,位于/etc/apparmor.d/abstractions/目录。比如abstractions/nameservice允许DNS解析,abstractions/ssl_certs允许读取TLS证书,abstractions/php允许PHP解释器运行。合理引用这些抽象层可以避免重复造轮子,同时保持策略的可读性。但要注意,不要盲目引入过多抽象层,否则会放大攻击面。

调试与验证策略的有效性

配置文件写完后,不要立即投入生产。先在抱怨模式下运行一段时间,观察审计日志。AppArmor的拒绝日志通常出现在/var/log/syslog或通过journalctl -xe查看,关键字段是apparmor="DENIED"。每一条拒绝记录都包含被拒绝的操作、目标文件路径和请求的权限。根据这些日志,逐条判断是正常业务需求还是异常行为。如果是正常需求,就在配置文件中补充对应规则;如果是可疑行为,就要排查应用是否被入侵或存在漏洞。

使用aa-logprof工具可以半自动化地处理抱怨日志。它会读取日志中的拒绝事件,并交互式地询问你是否允许这些操作。这比手工分析日志效率高得多,但依然需要人工判断每一项的合理性,不能无脑同意。

还有一个容易被忽略的细节:文件路径的glob匹配。AppArmor支持通配符*匹配任意字符但不包括目录分隔符,匹配包括#任意字符且包括目录分隔符。写规则时尽量精确,避免使用过于宽泛的/ r这样的规则,这会让沙箱形同虚设。例如,日志文件通常只需要匹配/var/log/customapp/*.og就够了,没必要开放整个目录的写权限。

容器化环境中的AppArmor配置

在Docker或LXD容器场景下,AppArmor依然可以发挥作用。容器运行时通常会自动生成一个基于容器ID的策略文件,但如果你需要自定义,可以用apparmor_parser命令加载自定义策略到容器命名空间。对于使用snap包管理的应用,其AppArmor策略位于/var/lib/snapd/apparmor/profiles/,修改这些文件后需要重新生成并加载策略。snap应用的策略修改相对复杂,因为每次更新可能会覆盖自定义规则,需要通过snap connections接口或系统级补丁机制来持久化。

常见陷阱与最佳实践

一个常见错误是忘记给动态链接器权限。几乎所有的动态链接程序都需要读取/lib64/ld-linux-x86-64.so.2和/etc/ld.so.cache,这些通常包含在abstractions/base中,但如果你的应用是静态编译的,或者使用了非标准库路径,就需要显式声明。另一个坑是应用程序的当前工作目录,如果应用需要在其安装目录下创建临时文件,必须显式赋予该目录写权限。

最佳实践方面,始终坚持最小权限原则,每次只添加应用实际需要的权限。定期审计配置文件,移除不再使用的规则。使用版本控制工具管理/etc/apparmor.d/目录,每次修改都留下记录。对于关键业务应用,可以考虑在测试环境先用aa-status检查当前所有配置文件的加载状态和模式,确认无误后再推送到生产环境。

AppArmor配置文件自定义并不是一次性工作,而是随着应用迭代持续演进的。一旦掌握了手写策略的技能,你就能够为任何自研软件构建量身定制的安全沙箱,把潜在的内核漏洞利用和横向移动攻击拦截在应用层之外。这种纵深防御的价值,在如今供应链攻击频发的背景下怎么强调都不过分。