容器逃逸是云原生环境中最具破坏力的安全威胁之一。攻击者一旦突破容器边界,就能获得宿主机权限,进而控制整个集群或窃取核心数据。Ubuntu作为主流容器宿主操作系统,其内置的AppArmor强制访问控制系统,提供了一道比默认Seccomp和Capability限制更细粒度的防线。问题的关键不在于要不要用AppArmor,而在于如何编写精准的规则文件,让容器内的进程只能执行预期操作,连提权或挂载宿主机敏感目录的机会都没有。

理解AppArmor在容器场景下的工作模式

AppArmor不是简单的黑名单机制,它基于路径和能力的白名单策略,默认拒绝一切未明确允许的操作。当Docker或containerd启动容器时,如果指定了AppArmor配置文件,Linux内核的安全模块就会把进程树纳入该配置的管辖范围。容器内即使以root身份运行,也无法绕过AppArmor施加的文件访问、网络操作、挂载、信号发送等限制。这与Kubernetes的PodSecurityPolicy形成互补,后者在准入控制层把关,而AppArmor在运行时内核层面兜底。

Ubuntu系统默认加载了docker-default这个AppArmor策略,它为容器提供基础防护,但这个通用策略比较宽松,无法阻止针对性逃逸。例如docker-default允许挂载proc和sys文件系统,而很多逃逸手法正是利用/proc或/sys下的伪文件来篡改内核参数或注入恶意模块。因此,面向业务关键型容器,必须定制AppArmor配置文件,把攻击面压缩到最小。

从零构建一个限制容器逃逸的AppArmor规则

编写AppArmor规则需要明确容器进程的正常行为边界。假设一个Java微服务容器,它只需要读取特定目录的配置文件、监听固定端口、写日志到标准输出,那么规则就应该禁止一切与此无关的操作。下面是一个实战中可用的规则模板:

#include 

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

  # 允许读取容器内应用目录
  /app/ r,
  /app/config/ r,

  # 允许执行Java运行时和系统基础库
  /usr/lib/jvm/java-11-openjdk-amd64/ rix,
  /lib/x86_64-linux-gnu/ rm,
  /usr/lib/x86_64-linux-gnu/ rm,
  /etc/ld.so.cache r,
  /etc/ld.so.preload r,

  # 允许网络操作
  network inet tcp,
  network inet6 tcp,

  # 允许标准输入输出
  /dev/null rw,
  /dev/zero rw,
  /dev/random r,
  /dev/urandom r,

  # 明确禁止挂载任何文件系统
  deny mount,
  deny remount,

  # 禁止访问宿主机敏感路径
  deny /proc/sysrq-trigger rw,
  deny /proc/sys/ rw,
  deny /sys/kernel/ rw,
  deny /sys/fs/cgroup/ rw,

  # 禁止加载内核模块
  deny /sys/module/ rw,

  # 禁止修改系统时间
  deny capability sys_time,

  # 禁止ptrace其他进程
  deny capability sys_ptrace,

  # 禁止修改capability边界
  deny capability sys_admin,

  # 日志记录违规行为
  audit deny /etc/shadow r,
  audit deny /root/ rwx,
}

这个规则的核心思路是只开放应用必需的路径和权限,其余一律拒绝。其中deny mount和deny remount直接封死了通过挂载宿主机目录逃逸的路径。禁止访问/proc/sys和/sys/kernel则阻止了通过修改内核参数实现提权的攻击。capability sys_admin的禁用尤为关键,因为该能力允许执行大量特权操作,包括挂载、设置主机名、加载内核模块等,是容器逃逸的重灾区。

规则加载与容器关联的完整流程

规则编写完成后,需要将其加载到内核并关联到目标容器。在Ubuntu主机上,首先将配置文件保存为/etc/apparmor.d/my-container-profile,然后执行加载命令:

sudo apparmor_parser -r -W /etc/apparmor.d/my-container-profile

验证规则是否加载成功,可以通过sudo aa-status查看当前活跃的配置文件列表。接下来启动容器时,使用--security-opt参数指定该AppArmor配置:

docker run -d \
  --name secure-java-app \
  --security-opt apparmor=my-container-profile \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --read-only \
  --tmpfs /tmp:noexec,nosuid,size=256M \
  my-java-image:latest

这里配合使用了多项安全加固措施。--cap-drop=ALL移除所有Linux能力后再按需添加,与AppArmor的能力限制形成双重保障。--read-only将根文件系统设为只读,配合tmpfs为/tmp提供临时存储,防止攻击者写入恶意二进制文件。这种多层防御体系让逃逸难度呈指数级上升。

在Kubernetes环境中,AppArmor的启用方式略有不同。需要在Pod的annotations中声明使用的配置文件名称,并且该配置文件必须预先在所有工作节点上加载:

apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
  annotations:
    container.apparmor.security.beta.kubernetes.io/secure-container: localhost/my-container-profile
spec:
  containers:
  - name: secure-container
    image: my-java-image:latest
    securityContext:
      readOnlyRootFilesystem: true
      capabilities:
        drop:
        - ALL
        add:
        - NET_BIND_SERVICE

注意annotations中的localhost前缀表示使用节点本地已加载的AppArmor配置。如果配置未在所有节点上同步加载,Pod会创建失败,这在生产环境中需要结合配置管理工具如Ansible或Puppet来保证一致性。

针对高级逃逸手法的AppArmor阻断策略

近年来出现的容器逃逸技术越来越精巧,需要针对性地编写规则。CVE-2022-0492这类利用cgroup release_agent的逃逸,核心步骤是向/sys/fs/cgroup下的release_agent文件写入恶意脚本路径,然后触发cgroup清理来执行该脚本。AppArmor规则中deny /sys/fs/cgroup/ rw这一条就能直接阻断写入操作,即使容器内拥有CAP_SYS_ADMIN也无法绕过。

另一种利用core_pattern管道注入的逃逸手法,通过向/proc/sys/kernel/core_pattern写入管道符号和恶意程序路径,在内核转储时执行任意命令。规则中的deny /proc/sys/ rw覆盖了这个攻击面。对于利用用户命名空间和overlayfs组合的逃逸,虽然AppArmor不能直接阻止命名空间创建,但可以禁止后续的挂载操作和敏感文件访问,使攻击链断裂。

容器内如果存在特权模式或者挂载了docker.sock,AppArmor的防护效果会大打折扣。因此规则中必须加入对docker.sock的访问限制:

deny /var/run/docker.sock rw,
deny /run/docker.sock rw,

同时,对于可能被滥用来读取宿主机文件系统的/proc/*/root符号链接,虽然AppArmor在路径解析上存在一定局限,但可以通过禁止访问/proc下非本进程的目录来降低风险。结合Pod安全策略中的procMount类型限制,能形成更立体的防护。

生产环境中的AppArmor运维实践

规则上线前必须在测试环境进行充分验证,因为过于严格的限制可能导致应用异常。建议采用complain模式先行运行,该模式下AppArmor只记录违规行为而不实际阻止,通过审计日志分析应用的真实行为模式:

sudo aa-complain /etc/apparmor.d/my-container-profile

查看审计日志可以使用aa-logprof工具,它会解析日志中的拒绝事件,并交互式地帮助管理员更新规则。当确认规则已经覆盖所有合法操作后,再切换回enforce模式:

sudo aa-enforce /etc/apparmor.d/my-container-profile

对于大规模容器集群,手动管理AppArmor配置文件的同步和加载效率低下。可以将配置文件纳入CI/CD流水线,通过DaemonSet或init容器在节点启动时自动加载。监控方面,需要将AppArmor的拒绝日志接入集中式日志平台,设置告警规则,一旦出现大量DENIED事件立即通知安全团队排查。

值得注意的是,AppArmor规则本身也需要版本管理。随着应用迭代,容器内可能新增合法的文件访问或系统调用,规则必须同步更新。建议将AppArmor配置文件与应用代码放在同一仓库,由开发和安全团队共同维护,每次应用发版时评估规则是否需要调整。

AppArmor与其他安全层的协同效应

单独依赖AppArmor是不够的,它应该作为纵深防御体系的一环。在容器启动层面,使用非root用户运行应用,配合User Namespace将容器内的root映射到宿主机的普通用户。在网络层面,通过NetworkPolicy限制容器间的通信,减少横向移动的风险。在镜像层面,使用最小化基础镜像并定期扫描漏洞,减少攻击者可利用的组件。

Seccomp与AppArmor的互补性很强。Seccomp过滤系统调用,能阻止容器执行危险的系统调用如mount、kexec_load等,而AppArmor提供更细粒度的文件路径和网络控制。两者叠加使用时,攻击者即使绕过了Seccomp的系统调用白名单,仍会面临AppArmor的路径访问控制。在Ubuntu系统上,Docker默认同时启用Seccomp和AppArmor,但自定义规则才能真正发挥它们的威力。

对于运行在Ubuntu主机上的关键业务容器,建议的安全基线是:使用定制AppArmor配置、启用Seccomp默认规则并追加自定义系统调用黑名单、删除所有Linux Capability后按需添加、根文件系统只读挂载、禁止特权模式、不挂载宿主机敏感目录。这套组合拳能有效防御绝大多数已知和未知的容器逃逸攻击,为业务提供坚实的安全底座。