AppArmor配置文件的核心是“路径+权限”的组合规则。你不需要理解复杂的强制访问控制理论,只需要知道每个可执行程序需要访问哪些文件、具备哪些能力,然后把这些规则写进一个文本文件。Ubuntu系统里,这些配置文件存放在/etc/apparmor.d/目录下,文件名通常是将程序的绝对路径中的斜杠替换为点号,例如针对/usr/sbin/nginx的配置文件名为usr.sbin.nginx。配置文件的语法极其直白,用“profile 程序路径 flags=(complain)”定义配置文件的入口,花括号内是具体的规则条目。flags可以设置为enforce(强制执行)或complain(仅记录不阻止),调试阶段强烈建议先用complain模式,否则一个错误的规则可能直接导致服务启动失败。

配置文件的核心语法与规则编写

每条规则的基本格式是“文件路径 权限类型”,权限类型包括r(读)、w(写)、m(内存映射可执行)、k(文件锁定)、l(创建硬链接)以及它们的组合,例如“rw”表示可读可写。路径支持通配符,星号匹配任意字符但不跨越目录,双星号匹配任意字符且跨越目录边界。比如“/var/log/nginx/* rw”允许读写nginx日志目录下的所有文件,但无法访问子目录中的文件;“/var/log/nginx/ rw”则允许递归访问所有子目录。规则中还可以使用变量,AppArmor预置了@{HOME}、@{PROC}、@{sys}等变量,你也可以在配置文件顶部用“@{变量名}=值”自定义变量。一个典型的nginx配置文件片段如下:

#include <tunables/global>

/usr/sbin/nginx {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/openssl>

  capability net_bind_service,
  capability setgid,
  capability setuid,

  /etc/nginx/ r,
  /var/log/nginx/* w,
  /var/www/ r,
  /run/nginx.pid rw,
  /usr/sbin/nginx mr,
  /etc/ssl/openssl.cnf r,
}

上面的规则中,include指令引用了系统预置的抽象层,这些抽象层封装了常见的基础权限,比如base抽象层允许程序读取动态链接器、基本系统库等,nameservice抽象层允许DNS解析所需的文件访问。capability关键字用于授予特定的Linux能力,net_bind_service允许绑定低于1024的端口,setgid和setuid允许切换用户身份。编写规则时务必遵循最小权限原则,只授予程序完成其功能所必需的访问权限,任何多余的权限都可能成为安全隐患。

抽象层与包含文件的合理利用

Ubuntu的AppArmor安装包自带大量预定义的抽象层文件,位于/etc/apparmor.d/abstractions/目录。这些文件把常见场景的权限需求打包成可复用的模块,比如abstractions/base提供最基本的系统交互权限,abstractions/php提供PHP解释器运行所需权限,abstractions/mysql提供MySQL客户端访问权限。使用抽象层有两个好处:一是避免重复编写相同的规则,二是当系统库路径发生变化时,只需更新抽象层文件即可影响所有引用它的配置文件。你可以用文本编辑器打开这些抽象层文件查看具体内容,理解它们授予了哪些权限,再决定是否引用。如果某个抽象层授予的权限过宽,可以单独编写更严格的规则覆盖它,因为AppArmor规则遵循“最后匹配生效”的原则。

子配置文件与hat机制处理复杂应用

当主程序会调用其他可执行文件或脚本时,你需要为这些子进程定义独立的子配置文件或hat。子配置文件使用“profile 主程序路径//子进程路径”的命名方式,hat则使用“^hat名称”在主配置文件内部定义。例如Apache的配置文件通常包含多个hat,分别处理不同模块的权限需求。定义hat的语法是在主profile内部使用“^hat名称 { 规则 }”,子进程通过change_hat()系统调用切换进入对应的hat。实际编写时,你可以先用aa-genprof工具生成一个基础配置,然后手动调整hat的规则。如果子进程是独立启动的,而不是由主进程fork出来的,那么子配置文件必须单独定义,并在主配置文件中使用“px 子进程路径 -> 子配置文件名称”的规则显式指定转换关系。px表示需要独立的配置文件转换,Px表示转换后使用独立配置文件且清理环境变量,ix表示子进程继承父进程的配置,cx表示转换到同一个配置文件内的子配置文件。

故障排查的系统化方法

AppArmor规则出错时的典型症状是服务启动失败、功能异常或日志中出现大量拒绝记录。排查的第一步永远是查看系统日志,Ubuntu系统中AppArmor的审计信息会写入/var/log/syslog或/var/log/kern.log,使用grep apparmor /var/log/syslog可以快速过滤相关条目。日志中“DENIED”表示被阻止的操作,“ALLOWED”表示在complain模式下本应被阻止但实际放行的操作。每条拒绝记录都包含操作名称、被拒绝的配置文件名称、进程ID、请求的文件路径和权限类型,这些信息直接告诉你哪条规则缺失。如果你不确定某个拒绝是否影响程序正常运行,可以临时将配置文件切换到complain模式:使用aa-complain /etc/apparmor.d/配置文件名称命令,然后重启服务,观察日志中的ALLOWED记录,这些记录就是你需要在enforce模式下补充的规则。

使用aa-logprof工具半自动化修复规则

aa-logprof是AppArmor工具集中最实用的命令,它会扫描系统日志中的拒绝记录,逐条询问你是否允许该操作,并自动更新配置文件。操作流程是:先将目标配置文件设为complain模式,运行程序触发所有功能路径,然后执行sudo aa-logprof,工具会列出所有未被现有规则覆盖的访问请求,你可以选择Allow(允许)、Deny(拒绝)、Glob(使用通配符匹配)、Edit(手动编辑路径)等操作。aa-logprof的智能之处在于它能识别常见的路径模式,建议使用通配符或变量来合并相似规则,避免配置文件过于冗长。处理完所有条目后,工具会重写配置文件并自动加载。最后将模式切换回enforce:aa-enforce /etc/apparmor.d/配置文件名称。这个工具不能完全替代人工审查,它可能会建议过于宽松的规则,你需要结合程序的实际需求判断是否接受每条建议。

手动分析拒绝日志与规则调试技巧

当aa-logprof无法解决问题,或者你需要深入理解程序的访问模式时,手动分析日志是必须的技能。一条典型的拒绝日志格式如下:

kernel: [12345.678] audit: type=1400 audit(1234567890.123:456): apparmor="DENIED" operation="open" profile="/usr/sbin/myservice" name="/etc/myservice/config.d/custom.conf" pid=12345 comm="myservice" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0

从中可以提取出关键信息:配置文件名称是/usr/sbin/myservice,被拒绝的操作是open,目标文件是/etc/myservice/config.d/custom.conf,请求的权限是r(读),进程的fsuid是1000,文件所有者ouid是0。根据这些信息,你需要决定是否在配置文件中添加“/etc/myservice/config.d/custom.conf r”规则,或者使用通配符“/etc/myservice/config.d/* r”覆盖整个目录。如果日志中出现大量对/proc文件系统的拒绝,通常意味着程序需要读取进程信息,可以添加“@{PROC}/@{pid}/ r”规则。对于/tmp目录下的临时文件创建需求,规则可以写成“/tmp/myapp-* rw”。调试过程中,建议使用aa-status命令查看当前加载的配置文件及其模式,确认修改是否已经生效。

处理常见棘手场景

场景一:程序需要执行外部命令。如果程序通过system()或exec()调用外部命令,必须使用px、Px或ix规则明确指定子进程的权限转换方式。缺少这条规则时,子进程会继承父进程的配置,可能因权限不足而失败。规则示例:“/usr/bin/php Px -> php-cgi”,表示执行/usr/bin/php时切换到名为php-cgi的独立配置文件。场景二:程序使用动态库或插件。很多程序在运行时加载.so文件,这些文件需要m(内存映射可执行)权限。如果日志中出现对/usr/lib/目录下.so文件的拒绝,添加“/usr/lib/ mr”规则通常可以解决,但要注意限制范围,避免授予不必要的库文件执行权限。场景三:网络服务绑定端口失败。除了capability net_bind_service之外,某些程序还需要对/etc/hosts.allow和/etc/hosts.deny的读权限,或者对/etc/protocols的读权限。场景四:容器或沙箱环境。如果你在LXC、LXD或Docker容器中使用AppArmor,需要理解宿主机的AppArmor策略会应用到容器进程上,容器内部的配置文件路径需要与宿主机的文件系统视图一致。

配置文件的测试与验证

修改配置文件后,使用apparmor_parser命令进行语法检查和加载。apparmor_parser -Q /etc/apparmor.d/配置文件名称可以静默检查语法错误,apparmor_parser -r /etc/apparmor.d/配置文件名称重新加载指定配置文件,apparmor_parser -R /etc/apparmor.d/配置文件名称则完全卸载该配置。加载新配置前,建议先用apparmor_parser -p /etc/apparmor.d/配置文件名称进行预解析,它会输出解析后的规则列表,方便你确认通配符展开和变量替换的结果是否符合预期。对于生产环境,务必先在测试机器上以complain模式运行足够长的时间,覆盖所有业务场景,收集完整的日志后再切换到enforce模式。可以使用aa-unconfined命令检查系统中哪些进程没有受到AppArmor约束,确保关键服务都已被配置文件覆盖。

性能影响与规则优化

AppArmor的规则匹配基于路径名而非inode,每次文件访问都会触发路径解析和规则匹配,因此规则数量和复杂度直接影响性能。优化规则的方法包括:将最常匹配的规则放在前面,因为AppArmor按顺序匹配规则;使用精确路径替代通配符,减少匹配开销;合并相邻的规则条目,例如将多个单独文件的读权限合并为对整个目录的读权限,前提是这样做不会违反最小权限原则;避免在规则中使用过多的双星号通配符,它们会导致递归遍历目录树。实际测试表明,一个包含数百条规则的配置文件对系统性能的影响通常在1%到3%之间,对于绝大多数应用场景完全可以接受。如果遇到极端性能敏感的场景,可以考虑使用SELinux的type enforcement机制,但那是另一个话题了。

备份与回滚策略

每次修改配置文件前,务必备份原文件。AppArmor的配置文件是纯文本,使用cp命令即可备份。Ubuntu的apparmor软件包在升级时会自动备份/etc/apparmor.d/目录下的文件,但手动修改不会触发自动备份。建议使用版本控制系统管理配置文件,例如将/etc/apparmor.d/目录初始化为git仓库,每次修改后提交并添加详细的提交信息,记录修改原因和测试结果。如果新配置导致系统异常,可以立即用git checkout恢复之前的版本,然后执行apparmor_parser -r重新加载。对于远程服务器,修改AppArmor配置前务必确保你有控制台访问权限,因为错误的网络相关规则可能导致SSH连接中断。一个保险的做法是使用at命令设置一个定时任务,在几分钟后自动恢复备份的配置文件并重新加载,如果修改成功则取消定时任务。