PVE9与k3s那点事:深夜填坑记,搞定AppArmor

适用于部署了 PVE9 的 k3s 节点。由于本文属于回忆文,可能存在一些出入,总体流程没问题。

背景

我目前已经将大部分 k3s 节点系统升级到最新的 Debian13 了,最近手头又多了一台杜甫,首先装了 Debian13,部署了 k3s 计算节点,然后观察到节点资源还很富裕,可以跑个 PVE 集群节点。

PVE 部署

主要参考 oneclickvirt/pve,直接在 Debian13 上安装 PVE9。中间踩了一个坑,忘记安装前配置 hosts

# 确定能ping,不然后面/etc/pve/local/pve-ssl.key创建失败导致pve没法正常启动
ping $(uname -n)

现象

未部署 PVE 前,k3s 正常工作了很长时间。安装完 PVE 后,部分 POD 开始报错了,本文以 kruise-daemon 为例,一直启动不了直至 CrashLoopBackOff。

查看相关日志后,发现 pod 所在节点审计日志:

[Thu Aug 21 11:52:09 2025] audit: type=1400 audit(1755748328.817:29): apparmor="DENIED" operation="create" class="net" info="failed protocol match" error=-13 profile="cri-containerd.apparmor.d" pid=20287 comm="kruise-daemon" family="unix" sock_type="stream" protocol=0 requested="create" denied="create" addr=none

发现 AppArmor 这位严格的安全管家把你的应用操作给拦下来了。分析一下这个日志,这是一条典型的 AppArmor 安全策略拒绝 (DENIED) 日志。简单来说,系统上的一个名为 kruise-daemon 的程序(运行在容器内)尝试执行一个操作,但被 AppArmor 安全模块阻止了

AI 分析

节选自 AI 分析

日志逐段分析

[Thu Aug 21 11:09:38 2025] audit: type=1400: 这是日志的时间戳和审计事件类型。
apparmor="DENIED": 核心信息。AppArmor 拒绝了一个操作。AppArmor 是 Linux 内核的一个强制访问控制(MAC)安全模块,它可以将程序限制在一组有限的资源中。
operation="create": 被拒绝的操作是“创建”。
class="net": 操作的类别是网络(net)相关的。
info="failed protocol match": 拒绝的详细原因。程序尝试创建的套接字(socket)的协议类型与 AppArmor 策略文件中允许的协议不匹配。
error=-13: 对应系统错误码 EACCES (Permission denied),进一步证实是权限问题。
profile="cri-containerd.apparmor.d": 关键信息。触发这个拒绝事件的安全策略文件是 cri-containerd.apparmor.d。这通常是 containerd 容器运行时的默认 AppArmor 配置文件,用于限制所有由 containerd 管理的容器的行为。
pid=48236 comm="kruise-daemon": 执行这个操作的进程是 kruise-daemon,进程 ID 是 48236。kruise-daemon 是 OpenKruise 的一个组件,通常以 DaemonSet 的形式运行在 Kubernetes 的每个节点上,负责一些节点级的增强功能。
family="unix": 网络套接字的类型是 Unix Domain Socket (UDS),这是一种用于同一台主机上进程间通信的机制,而非跨主机的 TCP/IP 网络。
sock_type="stream": 套接字的类型是流式套接字(stream),类似于 TCP。
protocol=0: 使用的协议号是 0(默认协议)。

问题核心概括

一句话总结: 运行在 containerd 容器中的 kruise-daemon 进程试图创建一个 Unix 域流式套接字(Unix Domain Stream Socket),但是 Kubernetes 节点上为容器配置的默认 AppArmor 安全策略 (cri-containerd.apparmor.d) 不允许这个操作。
这通常意味着 kruise-daemon 需要通过 Unix Socket 与节点上的其他服务(例如 Docker daemon、CNI 插件或其他 agent)进行通信,但默认的安全策略为了保证容器的隔离性,限制了这类“高级”或“特殊”的网络操作。

处理过程

确认 AppArmor 服务状态和已加载的 Profile

需要查看 AppArmor 当前在内核中加载了哪些 Profile

apparmor_status 或者aa-status
# 结果(这个是我调整后的,默认 cri-containerd.apparmor.d也是强制模式)
apparmor module is loaded.
8 profiles are loaded.
7 profiles are in enforce mode.
   /usr/bin/lxc-copy
   /usr/bin/lxc-start
   docker-default
   lxc-container-default
   lxc-container-default-cgns
   lxc-container-default-with-mounting
   lxc-container-default-with-nesting
1 profiles are in complain mode.
   cri-containerd.apparmor.d
0 profiles are in prompt mode.
0 profiles are in kill mode.
0 profiles are in unconfined mode.
1 processes have profiles defined.
0 processes are in enforce mode.
1 processes are in complain mode.
   /kruise-daemon (31408) cri-containerd.apparmor.d
0 processes are in prompt mode.
0 processes are in kill mode.
0 processes are unconfined but have a profile defined.
0 processes are in mixed mode.

默认 /etc/apparmor.d/ 目录下是没有这个的,是 containerd 启动时加载到内核的。针对这种情况下,最好的办法是覆盖它。 我们可以在标准路径 /etc/apparmor.d/ 下创建一个同名的文件。当 AppArmor 服务重载配置时,它会优先使用磁盘上的文件来覆盖内存中已加载的同名 Profile

这里简单记录一下,没那么简单。

在 /etc/apparmor.d/ 目录下创建一个新文件,名字就叫 cri-containerd.apparmor.d

cat > /etc/apparmor.d/cri-containerd.apparmor.d <<EOF
# AppArmor Profile for cri-containerd.apparmor.d
# SYNTAX: AppArmor 4.1 (for Debian 13+)

#include <tunables/global>

# 声明我们使用的是新的 v4 语法,这非常重要!
abi <abi/4.0>,

profile cri-containerd.apparmor.d flags=(attach_disconnected, complain, mediate_deleted) {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # 授予容器运行时所需的大部分 POSIX capabilities
  capability,

  # 拒绝直接写内核和内存等危险操作
  deny /dev/mem w,
  deny /dev/kmem w,

  # 允许通用的网络操作 (TCP/IP 等)
  # 在新语法中,这个规则不包含 unix socket
  network,

  # === AppArmor 4.x 关键修改 ===
  # 使用新的、独立的 unix socket 规则族
  # 直接允许创建、连接、监听、发送、接收 stream 类型的 unix socket
  unix (create, connect, listen, accept, send, receive) type=stream,

  # 允许挂载相关的操作
  mount options=(ro, nosuid, nodev, noexec, remount, bind),
  remount,

  # 允许容器运行时需要的一些基本文件访问
  /dev/urandom r,
  /sys/devices/system/cpu/online r,
  /sys/fs/cgroup/ r,
  /sys/fs/cgroup/** r,

  # 拒绝修改 AppArmor 自身,增强安全性
  deny /sys/kernel/security/apparmor/** w,

}
EOF

重新加载 AppArmor 配置,让我们的新文件生效

# -r 表示 replace,会替换掉内存中已有的同名 profile
apparmor_parser -r /etc/apparmor.d/cri-containerd.apparmor.d

再次运行 aa_status,确保 Profile 仍然在加载状态。然后尝试重建一下出问题的那个 kruise-daemon Pod,发现还是会 Deny,换了一个新错

了[Thu Aug 21 12:07:43 2025] audit: type=1400 audit(1755749262.720:42): apparmor="DENIED" operation="open" class="file" profile="cri-containerd.apparmor.d" name="/run/secrets/kubernetes.io/serviceaccount/..2025_08_21_04_07_36.2733297639/token" pid=28709 comm="kruise-daemon" requested_mask="r" denied_mask="r" fsuid=0 ouid=0

同上流程,是不是改一下配置文件就可以,是的, 截取了新增的地方如下

/sys/fs/cgroup/** r,
  # --- 本次新增规则 ---
  # 允许 Pod 读取其 Service Account Token,以便与 K8s API Server 通信
  "/run/secrets/kubernetes.io/serviceaccount/**" r,
  # 拒绝修改 AppArmor 自身,增强安全性
  deny /sys/kernel/security/apparmor/** w,

}

作为暴躁小伙,这样多麻烦,反正自己的环境应该也没啥大问题, 一梭子解决,观察者模式, 只需记录,不会拦截

aa-complain /etc/apparmor.d/cri-containerd.apparmor.d

其实到这里,问题基本解决了,但是没彻底解决。可以根据审计日志来完善,收集全部权限需求,一次性构建完整的 Profile,然后切换回 Enforce

官方这里确实好像有 BUG,事后没搜到了。

参考文档

希望这次的分享能帮到大家!觉得有用的话,别忘了点赞、在看、分享三连哦!

Sponsor

Like this article? $1 reward

Comments

晨阳 ·v1 北京朝阳区 Reply

哇 pve 都出9了吗,对比来说区别大吗

ysicing 👨‍💻 ·v1 Reply

@晨阳 对于我来说,区别不大,没体验出来哈哈哈。不着急的还是8稳定些,9还可以再观望观望。