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

非 root 容器与设备

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

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

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

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

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

在Kubernetes中,kubelet构建了可供容器使用的Device资源列表 (基于设备插件的输入),并将该列表包含在发送到CRI容器运行时的CreateContainer CRI消息中。每个Device包含的信息很少:宿主/容器设备路径以及所需的设备cgroups权限。

OCI运行时规范Linux容器配置要求,除了设备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 (int数组, 可选) 指定要添加到进程的容器命名空间中的附加组ID。

然而,当尝试运行既添加了设备又通过runAsUser/runAsGroup设置了非root uid/gid的容器时,生成的config.json会引发问题:即使设备组ID (gid,从宿主机复制) 对非root组具有许可权限,容器用户进程也没有使用该设备的权限。这是因为容器用户不属于该宿主组 (例如,通过additionalGids)。

能够以非root用户身份运行使用设备的应用是正常且预期的,这样才能满足安全原则。因此,我们考虑了几种替代方案来填补PodSec/CRI/OCI目前支持的功能与此之间的差距。

如何解决这个问题?

你可能从问题定义中注意到,至少可以通过手动将设备gid(s)添加到supplementalGroups,或者对于单个设备,将runAsGroup设置为设备的组ID来解决这个问题。然而,这会带来问题,因为设备gid(s) 在集群中不同节点的分发版/版本上可能具有不同的值。例如,对于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的Data Plane Development Kit (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

以及运行DPDK crypto-perf测试工具的Guaranteed QoS Class 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会议或issue)!此标志已在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 (Intel) 提供的所有反馈和富有成效的交流。