在 Kubernetes 集群里给容器挂载 AppArmor 配置文件,核心难点并不在于写出一条条访问控制规则,而在于如何让分散的多个工作节点都具备相同的策略上下文,并且让调度器把 Pod 放到已经加载好对应 profile 的机器上。AppArmor 本身是 Linux 内核的强制访问控制模块,Kubernetes 只是通过注解机制把容器运行时和节点上的 AppArmor 策略关联起来,真正生效的仍然是宿主机内核。因此实战的第一步永远是把配置文件落到节点并加载进内核,而不是急着写 YAML。

节点侧 AppArmor 配置文件的准备与加载
在 Ubuntu 等主流发行版上,AppArmor 工具链通常已经预装,核心命令是 apparmor_parser。我们需要先把写好的 profile 文件放到节点的 /etc/apparmor.d/ 目录中,这个路径是系统默认扫描和加载的位置。一个典型的容器专用 profile 会以 docker-default 或自定义名称为头,内部通过 file、network、capability 等指令限制容器能碰的资源。注意,Kubernetes 调用的 profile 名称必须和节点上加载的名称严格一致,否则 Pod 会处于错误状态。
假设我们写一个名为 k8s-apparmor-custom 的策略,禁止容器写宿主机的 /etc 目录,同时只允许基础网络能力,文件内容如下。写好之后用 apparmor_parser -q 静默加载,或者用 -r 替换已有规则。如果节点启用了 AppArmor 服务,重启后也会自动加载该目录下的文件,但在实战中我们往往希望立即生效,所以手动执行解析命令更可控。
# /etc/apparmor.d/k8s-apparmor-custom
#include <tunables/global>
profile k8s-apparmor-custom flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet stream,
network inet dgram,
deny /etc/** w,
/bin/** ix,
/lib/** ix,
/usr/** ix,
file,
}
加载命令非常简单,但必须确认执行结果。可以用 aa-status 查看当前内核里处于 enforce 或 complain 模式的 profile 列表。如果 k8s-apparmor-custom 没有出现,说明解析失败,多半是语法问题或路径权限不对。在集群规模较大时,手动登每台机器显然不现实,通常借助 DaemonSet 配合特权容器去分发和加载,或者直接使用节点初始化脚本如 cloud-init 在装机阶段完成。无论哪种方式,都要保证所有可能被调度到的节点策略一致,否则会出现同个 Pod 在 A 节点能起、B 节点报错的现象。
Pod 注解绑定与调度约束实战
当节点已经加载好 profile,下一步就是在 Pod 里通过注解告诉 kubelet 用哪一个。Kubernetes 使用的注解键是 container.apparmor.security.beta.kubernetes.io/<容器名>,值是 runtime/default、localhost/<profile名> 或者 unconfined。其中 localhost/ 前缀代表使用节点本地已加载的 AppArmor 策略,这也是最常用的自定义方式。如果写成 localhost/k8s-apparmor-custom,kubelet 就会在启动容器时把该 profile 传给容器运行时。
下面给出一个完整的 Pod 示例,其中名为 demo 的容器绑定了我们前面加载的自定义策略。需要特别注意的是,注解里的容器名必须和 spec.containers 中的 name 字段完全一致,否则注解会被忽略,容器以默认规则运行,带来安全隐患却难以察觉。另外,AppArmor 注解是 beta 特性,在较新版本中虽然仍可用,但部分发行版可能转向 seccomp 等机制,因此上线前应在测试集群验证注解是否被识别。
apiVersion: v1
kind: Pod
metadata:
name: apparmor-demo
annotations:
container.apparmor.security.beta.kubernetes.io/demo: localhost/k8s-apparmor-custom
spec:
containers:
- name: demo
image: busybox:1.36
command: ["sleep", "3600"]
调度层面也不能忽视。如果只有部分节点加载了 k8s-apparmor-custom,而 Pod 没有节点亲和性或污点容忍限制,调度器可能把它放到没加载的节点上,此时 kubelet 会报 AppArmor 无法应用的错误,Pod 一直 CrashLoopBackOff。实战中建议配合 nodeSelector 或 taints/tolerations 明确圈定策略节点池,或者在加载 profile 的 DaemonSet 中打标签,让业务 Pod 通过拓扑约束只调度过去。这样既能享受 AppArmor 的隔离能力,又不会因为集群扩缩容导致随机失败。
故障排查与 enforce、complain 模式对比
AppArmor 有两种常见模式:enforce 和 complain。enforce 下违规操作直接被拒绝,容器可能启动失败或系统调用返回权限错误;complain 模式只记录不拦截,适合上线前观察容器实际行为。在 Kubernetes 实战里,如果 Pod 起不来且事件里出现 AppArmor 相关字段,第一步应确认节点是否真的加载了该 profile,用 aa-status | grep k8s-apparmor-custom 快速核对。若节点有但 Pod 报错,多半是注解名称拼错或容器名不匹配。
我们对比一下两种模式下的表现。假设容器内部试图执行 echo test > /etc/secret,在 enforce 模式中该写入会被内核拦截,命令失败并返回权限拒绝,业务日志可能只看到 IO 错误,不太直观;而在 complain 模式,写入成功,但 /var/log/syslog 或 dmesg 中会留下 AppArmor 拒绝记录,便于我们反过来完善 profile。因此推荐流程是:先在测试 Pod 用 complain 跑一两天,收集拒绝日志,把必要权限补进 profile,再切到 enforce 推生产。
# 切换为 complain 模式加载 apparmor_parser -R k8s-apparmor-custom 2>/dev/null apparmor_parser -C /etc/apparmor.d/k8s-apparmor-custom # 查看拒绝日志 dmesg | grep apparmor | grep k8s-apparmor-custom
还有一个容易踩的坑是容器运行时本身的限制。Docker 和 containerd 都支持 AppArmor,但如果你用的是较底层的运行时或特定 CRI 插件,可能没把注解翻译为实际的 --security-opt 参数。此时即便节点和 Pod 配置都对,容器依然不受控。排查时可以登到节点用 ps aux | grep <容器id> 看运行时进程参数里有没有 apparmor 字段。没有的话需要升级运行时或调整 CRI 配置,而不是反复改 Kubernetes 侧的 YAML。把节点加载、注解绑定、调度约束、模式选择这四件事串起来,AppArmor 配置文件在 Kubernetes 里才算真正落地可用。
KubernetesAppArmor容器安全修改时间:2026-08-14 20:42:34