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

非 root 容器和设备

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

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

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

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

此外,集群管理员可以使用 PodSecurityPolicy(已弃用)或 Pod Security Admission(Alpha)等工具来强制执行部署在集群中的 Pod 所需的安全设置。这些设置可能要求容器必须 runAsNonRoot,或者禁止它们以 root 的组 ID 运行在 runAsGroupsupplementalGroups 中。

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

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

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

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

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

同样,运行时会根据 CRI 字段(包括 securityContext 中定义的字段:runAsUser/runAsGroup)准备其他强制性的 config.json 部分,这些字段通过以下方式成为 POSIX 平台用户结构的一部分:

  • uid (int, 必填) 指定容器命名空间中的用户 ID。
  • gid (int, 必填) 指定容器命名空间中的组 ID。
  • additionalGids (int 数组, 可选) 指定容器命名空间中要添加到进程的其他组 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 (布尔值)

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

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

为了演示新行为,我们以使用硬件加速器、Kubernetes CPU 管理器和 HugePages 的数据平面开发工具包 (DPDK) 应用程序为例。集群运行 containerd,配置如下:

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

或 CRI-O 配置如下:

[crio.runtime]
device_ownership_from_security_context = true

并使用以下 YAML 运行 DPDK 的 crypto-perf 测试工具的 Guaranteed QoS Class Pod:

...
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 支持设备直通,并允许将设备提供给虚拟机沙箱中的容器。

此外,支持用户命名空间和设备带来了额外的挑战。这个问题仍然悬而未决,需要更多的集思广益。

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

感谢

我感谢 Mike Brown(IBM,containerd)、Peter Hunt(Redhat,CRI-O)和 Alexander Kanevskiy(Intel)提供的所有反馈和愉快的交流。