Ubuntu系统中AppArmor(Application Armor)是一套基于内核的强制访问控制(MAC)安全机制,它通过配置文件对每个应用程序的文件访问、网络权限、信号能力等进行细粒度限制。自定义AppArmor配置文件的核心操作就是在/etc/apparmor.d/目录下创建或修改以应用名命名的profile文件,然后通过apparmor_parser命令加载生效。下面我直接从实操角度,把整个流程、语法细节、常见场景和排错方法一次性讲透。
AppArmor的工作原理很简单:每个被保护的程序都绑定一个profile(配置文件),内核在程序执行系统调用时会检查该profile中定义的规则,不符合规则的操作直接拒绝并记录日志。Ubuntu默认已经为大量常用服务(如Apache、MySQL、Nginx、Docker等)预置了profile,但当你自己编译安装软件、运行自定义脚本或部署非标准应用时,就必须手动编写配置文件。
AppArmor配置文件的基本结构与语法
一个标准的AppArmor profile文件通常包含以下几个部分:变量定义、能力限制、文件权限规则、网络规则、挂载规则和信号规则。文件头部必须声明profile名称和要保护的可执行文件路径。下面是一个最基础的模板:
#include <tunables/global>
/usr/local/bin/myapp flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# 允许读取配置文件
/etc/myapp/config.conf r,
# 允许读写数据目录
/var/lib/myapp/ rw,
/var/lib/myapp/ rwk,
# 允许写入日志
/var/log/myapp/ rw,
/var/log/myapp/ rw,
# 允许网络访问(TCP和UDP)
network inet tcp,
network inet udp,
# 允许特定信号
signal (receive) set=(term),
# 允许挂载tmpfs
mount fstype=tmpfs,
}这里有几个关键点需要注意。第一行的#include <tunables/global>是必须的,它引入全局变量定义。flags=(complain)表示该profile处于"抱怨模式",也就是只记录违规行为但不拦截,这是调试阶段的最佳选择。等测试通过后改成flags=(enforce)就切换为强制拦截模式。路径后面的权限字母含义:r=读、w=写、k=锁定文件、x=执行、l=链接、m=允许内存映射执行。
实战案例一:为自定义Python脚本创建AppArmor profile
假设你有一个Python数据采集脚本/opt/scraper/collect.py,它需要读取/opt/scraper/目录下的配置和数据文件,写入/var/log/scraper/日志,并且需要访问网络。具体操作如下。
首先创建profile文件:
sudo nano /etc/apparmor.d/usr.bin.python3.scraper
文件内容如下:
#include <tunables/global>
/opt/scraper/collect.py flags=(complain) {
#include <abstractions/base>
#include <abstractions/python>
# 脚本自身及目录
/opt/scraper/ r,
/opt/scraper/ r,
# Python解释器
/usr/bin/python3* mr,
# 日志目录
/var/log/scraper/ rw,
/var/log/scraper/ rw,
# 临时文件
/tmp/ r,
/tmp/ rw,
# 网络权限
network inet tcp,
network inet udp,
# 允许接收终止信号
signal (receive) set=(term kill),
# 允许读取DNS配置
/etc/resolv.conf r,
/etc/hosts r,
}保存后执行加载命令:
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.python3.scraper
然后验证是否加载成功:
sudo aa-status | grep scraper
如果看到该profile处于complain模式,说明配置文件语法正确且已生效。接下来运行脚本观察/var/log/syslog或/var/log/audit/audit.log中的AppArmor日志,确认没有意外的权限拒绝。测试一段时间没问题后,把flags改成(enforce)重新加载即可。
实战案例二:为Docker容器中的自定义服务定制profile
Docker默认会给容器施加AppArmor限制,但如果你在容器内运行自定义服务(比如一个Go语言编写的API服务),需要在宿主机上创建对应的profile。假设容器内服务路径为/usr/local/bin/api-server。
#include <tunables/global>
/usr/local/bin/api-server flags=(complain) {
#include <abstractions/base>
# 主程序
/usr/local/bin/api-server mr,
# 配置文件
/etc/api-server/ r,
/etc/api-server/ r,
# 数据持久化目录
/data/ rw,
/data/ rwk,
# 监听端口(假设是8080)
network inet tcp,
network inet6 tcp,
# 允许绑定低端口(如果需要)
capability net_bind_service,
# 进程间通信
/run/api-server.sock rw,
# 读取系统时区等信息
/etc/localtime r,
/usr/share/zoneinfo/ r,
signal (receive) set=(term kill hup),
}这里特别要注意capability net_bind_service这一行。如果你的服务需要绑定1024以下的端口(比如80或443),必须显式声明这个能力,否则即使网络规则允许也会被拒绝。另外/run/api-server.sock rw是给Unix socket用的,很多本地服务通信都依赖这个。
AppArmor常用抽象文件(abstractions)详解
编写profile时不需要每次都从零开始,AppArmor提供了大量预定义的抽象文件放在/etc/apparmor.d/abstractions/目录下,通过#include引入即可复用通用规则。常用的包括:
abstractions/base:最基础的抽象,包含标准的文件系统访问、信号处理、基本能力等,几乎所有profile都要引入。
abstractions/bash:如果你的程序会调用bash脚本或子shell,必须引入这个。
abstractions/python:Python程序专用,处理Python解释器路径、.pyc文件、site-packages等。
abstractions/mysql:MySQL相关的数据目录、socket、端口等规则。
abstractions/nginx:Nginx的配置目录、日志目录、缓存目录等。
合理使用抽象文件可以大幅减少配置量,同时降低出错概率。你可以用cat命令查看这些文件的内容来了解具体规则。
配置文件的调试与排错技巧
AppArmor最容易出问题的地方就是权限不足导致程序莫名其妙失败。排错的标准流程是:
第一步,确认profile是否处于complain模式。如果已经是enforce模式,先改成complain再测试,这样不会直接阻断程序。
第二步,查看日志。Ubuntu中AppArmor的拒绝日志通常在:
sudo grep -i apparmor /var/log/syslog | tail -50
或者如果系统启用了auditd:
sudo ausearch -m avc -ts recent
第三步,使用aa-logprof工具进行交互式调试。这是AppArmor自带的神器,它会实时分析日志并提示你添加哪条规则:
sudo aa-logprof
运行后它会列出所有被拒绝的操作,你可以选择Allow(允许)、Deny(拒绝)、Audit(仅记录)或Abort(放弃)。选择Allow后它会自动生成对应的规则建议,你确认后就会追加到profile文件中。
第四步,语法检查。在加载任何profile之前,先用parser的验证模式检查语法:
sudo apparmor_parser -T /etc/apparmor.d/your-profile
如果没有报错输出,说明语法正确。这个步骤能避免因一个拼写错误导致整个profile加载失败。
多个profile之间的关系与继承
AppArmor支持profile之间的嵌套和引用。比如你可以创建一个通用的"myapp-common"profile,然后在具体的profile中通过include引用它:
# /etc/apparmor.d/myapp-common #include <tunables/global> /opt/myapp/ r, /var/log/myapp/ rw, network inet tcp,
# /etc/apparmor.d/usr.bin.myapp-worker #include <tunables/global> #include "myapp-common" /usr/bin/myapp-worker mr, capability dac_override,
这种方式在管理多个相关服务时特别有用,修改通用规则只需改一处。但要注意include的路径写法,本地文件用双引号,系统抽象用尖括号。
AppArmor与其他安全机制的配合
在Ubuntu生产环境中,AppArmor通常不是单独使用的。它和SELinux(虽然Ubuntu默认不启用但可以安装)、系统防火墙ufw、内核参数sysctl硬化等形成多层防护。特别要注意的是,AppArmor和Docker/LXC容器的安全隔离是天然配合的——容器启动时可以通过--security-opt apparmor=profile-name指定profile,实现容器级别的强制访问控制。
另外,如果你的系统同时安装了snap包,snap自带的AppArmor隔离机制可能会和你手动编写的profile产生冲突。遇到这种情况,先用snap list查看已安装的snap,再用snap connections检查其AppArmor接口,必要时在profile中为snap的路径添加相应权限。
生产环境部署的最佳实践
最后总结几条在生产环境中使用AppArmor自定义配置的核心建议。第一,永远先用complain模式跑至少一周,收集完整的访问模式后再切换enforce。第二,profile文件权限设为644,属主root,不要随意放宽。第三,把自定义profile纳入版本控制(比如git),方便回滚和审计。第四,定期用aa-status --json导出当前所有profile状态做备份。第五,对于不需要网络的服务,明确删除network规则,遵循最小权限原则。
AppArmor是Ubuntu安全体系中非常实用但容易被忽视的一环。它不像防火墙那样直观,但在防止提权、限制横向移动、约束恶意程序行为方面效果显著。掌握自定义profile的编写和调试能力,是每一个Ubuntu系统管理员和安全运维人员的必备技能。
