Ubuntu系统上AppArmor和Docker默认配置之间的冲突,本质上是两套安全机制在同一内核层面的"打架"。AppArmor通过强制访问控制(MAC)限制进程的文件访问权限,而Docker容器默认会绕过AppArmor的部分规则,导致容器内的进程要么被错误拦截,要么完全失去保护。最直接的解决思路有三条:一是在Docker daemon配置中禁用AppArmor对容器的限制,二是为Docker容器编写专门的AppArmor profile,三是在Ubuntu上将Docker的默认AppArmor策略从"enforce"改为"complain"模式。下面我会把每种方案的操作细节、适用场景和潜在风险全部讲清楚。

问题的根源:AppArmor和Docker的设计冲突

Ubuntu从10.04开始默认启用AppArmor,它的工作方式是给每个进程绑定一个安全profile,规定这个进程能读写哪些文件、能执行哪些操作。Docker容器本质上是宿主机上的一个进程,按理说也应该受AppArmor约束。但Docker的默认设计是让容器拥有相对宽松的权限,容器内的root用户在宿主机上实际上映射为一个普通用户(默认是uid 1000),而且Docker daemon会主动为容器加载一个名为"docker-default"的AppArmor profile。

问题就出在这里。这个"docker-default" profile在较新版本的Ubuntu(22.04及以上)和较新版本的Docker(24.x及以上)中,规则变得越来越严格,甚至会阻止容器正常挂载volume、访问/proc文件系统、执行某些系统调用。你会看到容器启动报错,日志里出现"Permission denied"或者"apparmor: denied"之类的信息。反过来,如果你完全禁用AppArmor,又会让容器失去一层重要的安全隔离。

方案一:在Docker daemon中禁用AppArmor限制(最简单但安全性最低)

如果你是在开发环境或者内网隔离环境中使用Docker,最快的解决办法就是直接告诉Docker不要加载AppArmor profile。操作步骤如下:

编辑Docker daemon的配置文件:

sudo nano /etc/docker/daemon.json

在文件中添加或修改以下内容:

{
  "security-opt": ["apparmor:unconfined"]
}

保存后重启Docker服务:

sudo systemctl restart docker

这样所有新创建的容器都不会受AppArmor约束。但要注意,这意味着容器进程可以访问宿主机上AppArmor原本保护的文件路径,如果容器被攻破,攻击者的横向移动范围会更大。这个方案只适合非生产环境。

方案二:修改默认AppArmor策略为complain模式(折中方案)

如果你不想完全禁用AppArmor,但又不想让它阻断容器的正常操作,可以把策略从"enforce"(强制执行)改成"complain"(仅记录不阻断)。这样AppArmor会记录所有违规行为但不会实际拦截,你既能看到安全日志,又不影响容器运行。

首先找到Docker的AppArmor profile文件:

sudo find /etc/apparmor.d -name "*docker*"

通常会找到类似/etc/apparmor.d/docker-default或/etc/apparmor.d/usr.sbin.dockerd的文件。编辑它,把开头的:

profile docker-default flags=(attach_disconnected,mediate_deleted) {

改成:

profile docker-default flags=(attach_disconnected,mediate_deleted) complain {

或者直接把整个profile文件中所有的"deny"规则注释掉。然后重新加载AppArmor:

sudo apparmor_parser -r /etc/apparmor.d/docker-default

这个方案的好处是保留了审计能力,坏处是你失去了实际的强制保护。建议在生产环境中配合其他安全措施一起使用。

方案三:为特定容器编写自定义AppArmor profile(最安全但最复杂)

如果你需要在生产环境中同时保证Docker正常运行和AppArmor的安全隔离,最正确的做法是为你的容器编写定制化的AppArmor profile。这需要你清楚知道容器里的进程需要访问哪些路径、执行哪些系统调用。

创建一个自定义profile文件:

sudo nano /etc/apparmor.d/my-container-app

写入基本框架:

#include 

profile my-container-app flags=(attach_disconnected,mediate_deleted) {
  #include 
  #include 
  #include 

  # 允许容器访问自己的rootfs
  /var/lib/docker/overlay2//merged/ rw,

  # 允许访问必要的系统文件
  /proc/ r,
  /sys/ r,
  /etc/resolv.conf r,

  # 允许挂载的volume
  /data/ rw,

  # 允许执行容器内的应用
  /usr/bin/python3 mr,
  /usr/local/bin/ mr,
}

然后在Docker run时指定使用这个profile:

docker run --security-opt apparmor=my-container-app -v /data:/data myimage

编写自定义profile的难点在于你需要用"aa-logprof"工具来逐步分析容器的实际访问需求。先让profile处于complain模式运行容器,然后:

sudo aa-logprof

这个工具会分析AppArmor的日志,告诉你哪些访问被拒绝了,然后你可以选择允许或继续拒绝。反复运行几次,直到容器所有正常操作都被允许,再把profile切换回enforce模式。

Ubuntu不同版本的差异和注意事项

Ubuntu 18.04、20.04、22.04和24.04在AppArmor与Docker的兼容性上表现不同。18.04时代Docker的AppArmor集成还比较粗糙,很多时候需要手动处理。20.04开始Docker官方改善了默认profile,但仍然存在volume挂载权限不足的问题。22.04之后AppArmor的规则更加细化,对/proc/sysrq-trigger、/proc/kcore等敏感路径的限制更严格,导致一些监控类容器(比如需要读取宿主机内核信息的容器)无法正常工作。24.04则引入了更新的AppArmor 3.x版本,profile语法有变化,旧的profile文件可能需要适配。

另外要注意,snap安装的Docker和apt安装的Docker在AppArmor处理上也有区别。snap版Docker自带自己的AppArmor profile(snap.docker.dockerd),和系统级的AppArmor是两套东西,有时候会产生双重限制。如果你用的是snap版Docker,需要额外处理snap的AppArmor接口:

sudo snap connect docker:apparmor-support

如何判断你的环境是否受到影响

最直接的检测方法是查看Docker容器的启动日志和系统日志。运行一个容器后执行:

docker logs  2>&1 | grep -i "permission\|apparmor\|denied"

同时查看系统日志:

sudo dmesg | grep -i apparmor
sudo journalctl -u apparmor --since "1 hour ago"

如果看到大量"apparmor: DENIED"的记录,说明AppArmor确实在拦截容器的操作。这时候你需要根据上面的三种方案选择适合你的处理方式。

安全建议和最佳实践

从安全角度来说,我的建议是:开发测试环境可以用方案一或方案二快速解决问题;预生产环境用方案二加监控;生产环境必须用方案三,为每个关键容器编写定制profile。同时要注意几点:第一,不要把容器的AppArmor profile设置得过于宽松,"rw"权限能给"r"就不要给"rw";第二,定期用aa-logprof审查profile的变更,防止权限蠕变;第三,Docker的--privileged模式会完全绕过AppArmor,生产环境绝对不要使用;第四,考虑配合Seccomp和Capabilities一起做多层安全控制,不要把所有安全赌注都押在AppArmor上。

总结一下,AppArmor和Docker的兼容问题不是一个简单的开关问题,而是需要根据你的实际使用场景、安全需求和Ubuntu版本来做针对性调整。核心原则是:在保证容器正常运行的前提下,尽可能收紧AppArmor的权限范围,而不是为了省事直接关掉它。