本文发布已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。

非 root 容器和设备

当用户想在 Linux 上部署使用加速器设备(通过 Kubernetes 设备插件)的容器时,Pod 的 securityContext 中与用户/组 ID 相关的安全设置会触发一个问题。在这篇博客文章中,我将讨论这个问题,并描述到目前为止为解决这个问题所做的工作。这并不是一个关于修复 k/k 问题 的长篇故事。

相反,这篇文章旨在提高人们对这个问题的认识,并强调重要的设备用例。这是必要的,因为 Kubernetes 正在开发新的相关功能,例如对用户命名空间的支持。

为什么非 root 容器不能使用设备以及为什么这很重要

在 Kubernetes 中运行容器的关键安全原则之一是最小权限原则。Pod/容器 securityContext 指定要设置的配置选项,例如,Linux 功能、MAC 策略以及用户/组 ID 值来实现这一点。

此外,集群管理员还获得 PodSecurityPolicy(已弃用)或 Pod 安全准入(alpha)等工具的支持,以强制执行集群中部署的 Pod 所需的安全设置。例如,这些设置可能要求容器必须是 runAsNonRoot,或者禁止它们以 root 的组 ID 在 runAsGroupsupplementalGroups 中运行。

在 Kubernetes 中,kubelet 构建一个 Device 资源列表,这些资源将提供给容器(基于来自设备插件的输入),并且该列表包含在发送给 CRI 容器运行时的 CreateContainer CRI 消息中。每个 Device 包含少量信息:主机/容器设备路径和所需的设备 cgroups 权限。

Linux 容器配置的 OCI 运行时规范期望除了设备 cgroup 字段之外,还必须提供关于设备的更详细信息

{
        "type": "<string>",
        "path": "<string>",
        "major": <int64>,
        "minor": <int64>,
        "fileMode": <uint32>,
        "uid": <uint32>,
        "gid": <uint32>
},

CRI 容器运行时(containerd、CRI-O)负责从主机获取每个 Device 的此信息。默认情况下,运行时会复制主机设备的用户和组 ID

  • uid (uint32, 可选) - 容器命名空间中设备所有者的 ID。
  • gid (uint32, 可选) - 容器命名空间中设备组的 ID。

类似地,运行时根据 CRI 字段准备其他强制性的 config.json 部分,包括在 securityContext 中定义的那些:runAsUser/runAsGroup,它们通过以下方式成为 POSIX 平台用户结构的一部分

  • uid (int, 必需) 指定容器命名空间中的用户 ID。
  • gid (int, 必需) 指定容器命名空间中的组 ID。
  • additionalGids (整数数组,可选) 指定要添加到进程的容器命名空间中的其他组 ID。

但是,当尝试运行添加了设备且通过 runAsUser/runAsGroup 设置了非 root uid/gid 的容器时,生成的 config.json 会触发一个问题:容器用户进程没有权限使用该设备,即使其组 id(gid,从主机复制)允许非 root 组。这是因为容器用户不属于该主机组(例如,通过 additionalGids)。

能够以非 root 用户身份运行使用设备的应用程序是正常且预期的,以便满足安全原则。因此,考虑了几种替代方案来填补 PodSec/CRI/OCI 今天所支持的功能之间的差距。

为解决问题做了什么?

你可能已经从问题定义中注意到,至少可以通过手动将设备 gid 添加到 supplementalGroups 来解决该问题,或者在只有一个设备的情况下,将 runAsGroup 设置为设备的组 id。但是,这存在问题,因为设备 gid 在集群中可能根据节点的发行版/版本而具有不同的值。例如,使用 GPU 时,以下不同发行版和版本的命令返回不同的 gid

Fedora 33

$ ls -l /dev/dri/
total 0
drwxr-xr-x. 2 root root         80 19.10. 10:21 by-path
crw-rw----+ 1 root video  226,   0 19.10. 10:42 card0
crw-rw-rw-. 1 root render 226, 128 19.10. 10:21 renderD128
$ grep -e video -e render /etc/group
video:x:39:
render:x:997:

Ubuntu 20.04

$ ls -l /dev/dri/
total 0
drwxr-xr-x 2 root root         80 19.10. 17:36 by-path
crw-rw---- 1 root video  226,   0 19.10. 17:36 card0
crw-rw---- 1 root render 226, 128 19.10. 17:36 renderD128
$ grep -e video -e render /etc/group
video:x:44:
render:x:133:

securityContext 中选择哪个数字?此外,如果 runAsGroup/runAsUser 值不能硬编码,因为它们是在 pod 准入期间通过外部安全策略自动分配的,该怎么办?

与具有 fsGroup 的卷不同,设备没有 deviceGroup/deviceUser 的官方概念,CRI 运行时(或 kubelet)可以使用该概念。我们考虑使用设备插件设置的容器注释(例如,io.kubernetes.cri.hostDeviceSupplementalGroup/)来获取自定义 OCI config.json uid/gid 值。这将需要更改所有现有的设备插件,这并不理想。

相反,首选一种对最终用户来说是无缝的解决方案,而无需设备插件供应商的参与。选择的方法是在 config.json 中为设备重用 runAsUserrunAsGroup

{
        "type": "c",
        "path": "/dev/foo",
        "major": 123,
        "minor": 4,
        "fileMode": 438,
        "uid": <runAsUser>,
        "gid": <runAsGroup>
},

对于 runc OCI 运行时(在非 rootless 模式下),设备是在容器命名空间中创建的 (mknod(2)),并且使用 chmod(2) 将所有权更改为 runAsUser/runAsGroup

在容器命名空间中更新所有权是合理的,因为用户进程是唯一访问该设备的用户。仅考虑 runAsUser/runAsGroup,并且,例如,目前忽略容器中的 USER 设置。

虽然很可能不存在“错误的”部署(即,非 root securityContext + 设备),但为了确保没有部署中断,在 containerd 和 CRI-O 中都添加了一个选择加入配置条目来启用新行为。以下

device_ownership_from_security_context (bool)

默认为 false,必须启用才能使用该功能。

在修复后查看使用设备的非 root 容器

为了演示新行为,让我们使用数据平面开发套件 (DPDK) 应用程序,该应用程序使用硬件加速器、Kubernetes CPU 管理器和 HugePages 作为示例。集群运行 containerd,其中

[plugins]
  [plugins."io.containerd.grpc.v1.cri"]
    device_ownership_from_security_context = true

或运行 CRI-O,其中

[crio.runtime]
device_ownership_from_security_context = true

以及运行 DPDK 加密性能测试实用程序的 Guaranteed QoS 类 Pod,其 YAML 如下

...
metadata:
  name: qat-dpdk
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 2000
    fsGroup: 3000
  containers:
  - name: crypto-perf
    image: intel/crypto-perf:devel
    ...
    resources:
      requests:
        cpu: "3"
        memory: "128Mi"
        qat.intel.com/generic: '4'
        hugepages-2Mi: "128Mi"
      limits:
        cpu: "3"
        memory: "128Mi"
        qat.intel.com/generic: '4'
        hugepages-2Mi: "128Mi"
  ...

要验证结果,请检查容器运行的用户和组 ID

$ kubectl exec -it qat-dpdk -c crypto-perf -- id

它们按预期设置为非零值

uid=1000 gid=2000 groups=2000,3000

接下来,检查设备节点权限(qat.intel.com/generic 公开 /dev/vfio/ 设备)是否可供 runAsUser/runAsGroup 访问

$ kubectl exec -it qat-dpdk -c crypto-perf -- ls -la /dev/vfio
total 0
drwxr-xr-x 2 root root      140 Sep  7 10:55 .
drwxr-xr-x 7 root root      380 Sep  7 10:55 ..
crw------- 1 1000 2000 241,   0 Sep  7 10:55 58
crw------- 1 1000 2000 241,   2 Sep  7 10:55 60
crw------- 1 1000 2000 241,  10 Sep  7 10:55 68
crw------- 1 1000 2000 241,  11 Sep  7 10:55 69
crw-rw-rw- 1 1000 2000  10, 196 Sep  7 10:55 vfio

最后,检查是否也允许非 root 容器创建 HugePages

$ kubectl exec -it qat-dpdk -c crypto-perf -- ls -la /dev/hugepages/

fsGrouprunAsUser 提供可写的 HugePages emptyDir 挂载点

total 0
drwxrwsr-x 2 root 3000   0 Sep  7 10:55 .
drwxr-xr-x 7 root root 380 Sep  7 10:55 ..

帮助我们测试它并提供反馈!

此处描述的功能有望帮助提高集群安全性和设备权限的可配置性。为了允许非 root 容器使用设备,需要集群管理员通过设置 device_ownership_from_security_context = true 来选择启用该功能。为了使其成为默认设置,请对其进行测试并提供反馈(通过 SIG-Node 会议或问题)!该标志在 CRI-O v1.22 版本中可用,并已排队等待 containerd v1.6。

需要做更多的工作才能正确支持它。已知它可以与 runc 一起工作,但也需要使其与适用的其他 OCI 运行时一起工作。例如,Kata Containers 支持设备直通,并允许它在 VM 沙箱中为容器提供设备。

此外,额外的挑战来自对用户名和设备的支持。这个问题仍然 开放,需要更多的集思广益。

最后,需要理解 runAsUser/runAsGroup 是否足够,或者是否需要在 PodSpec/CRI v2 中使用类似于 fsGroups 的设备特定设置。

感谢

我感谢 Mike Brown(IBM,containerd)、Peter Hunt(Redhat,CRI-O)和 Alexander Kanevskiy(英特尔)提供了所有的反馈和良好的对话。