Kubernetes v1.33:用户名字空间默认启用!

在 Kubernetes v1.33 中,对用户名字空间的支持已默认启用。这意味着,当满足技术栈要求时,Pod 可以选择性地使用用户名字空间。使用该功能不再需要启用任何 Kubernetes 特性门控!

在这篇博客文章中,我们回答了一些关于用户名字空间的常见问题。但在我们深入探讨之前,让我们先回顾一下什么是用户名字空间以及它们为何重要。

什么是用户名字空间?

注意:Linux 用户名字空间与 Kubernetes 名字空间 是不同的概念。前者是 Linux 内核的一个特性;后者是 Kubernetes 的一个特性。

Linux 提供了不同的名字空间来隔离进程。例如,一个典型的 Kubernetes Pod 在一个网络名字空间中运行以隔离网络身份,在一个 PID 名字空间中运行以隔离进程。

一个被遗忘的 Linux 名字空间是用户名字空间。它将容器的 UID 和 GID 与主机上的 UID 和 GID 隔离开来。容器中的标识符可以映射到主机上的标识符,使得主机和容器的 UID/GID 永远不会重叠。此外,这些标识符可以映射到主机上非特权的、不重叠的 UID 和 GID。这带来了三个关键好处:

  • 防止横向移动:由于不同容器的 UID 和 GID 映射到主机上不同的 UID 和 GID,即使容器逃逸出容器边界,它们也很难互相攻击。例如,假设容器 A 在主机上运行的 UID 和 GID 与容器 B 不同。在这种情况下,它对容器 B 的文件和进程可以执行的操作是有限的:只能读/写文件允许其他人操作的部分,因为它永远不会拥有所有者或组权限(主机上的 UID/GID 保证对不同容器是不同的)。

  • 增强主机隔离:由于 UID 和 GID 映射到主机上的非特权用户,如果一个容器逃逸出容器边界,即使它在容器内以 root 身份运行,它在主机上也没有特权。这极大地保护了它能读/写的主机文件、能发送信号的进程等。此外,授予的能力仅在用户名字空间内有效,而在主机上无效,从而限制了容器逃逸可能造成的影响。

  • 启用新的使用场景:用户名字空间允许容器在自己的用户名字空间内获得某些能力,而不会影响主机。这开启了新的可能性,例如运行需要特权操作的应用程序,而无需在主机上授予完全的 root 访问权限。这对于运行嵌套容器特别有用。

Image showing IDs 0-65535 are reserved to the host, pods use higher IDs

用户名字空间 ID 分配

如果一个没有用户名字空间的 Pod 以 root 用户身份运行并成功逃逸,它将在节点上拥有 root 权限。如果容器被授予了某些能力,这些能力在主机上也有效。当使用用户名字空间时,这些情况都不会发生(当然,不考虑 bug 🙂)。

演示

Rodrigo 创建了一些演示,以帮助理解使用用户名字空间时如何缓解某些 CVE。我们之前在这里展示过(参见这里这里),如果你还没看过,可以看看:

使用用户名字空间缓解 CVE-2024-21626

使用用户名字空间缓解 CVE-2022-0492

关于 Kubernetes 中用户名字空间你想知道的一切

在这里,我们尝试回答一些关于 Kubernetes 中用户名字空间支持的常见问题。

1. 使用它有什么要求?

要求已在这里详细说明。但我们将在接下来的问题中进一步阐述。

请注意,这是一个仅限 Linux 的功能。

2. 我该如何配置一个 Pod 来选择性启用它?

一个完整的步骤指南可以在这里找到。但简而言之,你需要在 Pod 规约中设置 hostUsers: false 字段。例如像这样:

apiVersion: v1
kind: Pod
metadata:
  name: userns
spec:
  hostUsers: false
  containers:
  - name: shell
    command: ["sleep", "infinity"]
    image: debian

是的,就是这么简单。应用程序将正常运行,无需任何其他更改(除非你的应用程序需要特权)。

用户名字空间允许你在容器内以 root 身份运行,但在主机上没有特权。然而,如果你的应用程序需要在主机上拥有特权,例如一个需要加载内核模块的应用,那么你不能使用用户名字空间。

3. 什么是 idmap 挂载?为什么使用的文件系统需要支持它?

Idmap 挂载是 Linux 内核的一个特性,它在访问挂载点时使用 UID/GID 的映射。当与用户名字空间结合使用时,它极大地简化了对卷的支持,因为你可以不用关心用户名字空间正在使用的主机 UID/GID。

特别地,感谢 idmap 挂载,我们可以:

  • 让每个 Pod 在主机上以不同的 UID/GID 运行。这对于我们之前提到的防止横向移动至关重要。
  • 与不使用用户名字空间的 Pod 共享卷。
  • 启用/禁用用户名字空间,而无需对 Pod 的卷进行 chown 操作。

内核中对 idmap 挂载的支持是按文件系统进行的,不同的内核版本为不同的文件系统添加了对 idmap 挂载的支持。

要了解哪个内核版本为每个文件系统添加了支持,你可以查看 mount_setattr 的 man 手册页,或其在线版本这里

大多数流行的文件系统都已支持,一个显著的例外是 NFS 尚未支持。

4. 你能具体说明哪些文件系统需要支持 idmap 挂载吗?

需要支持 idmap 挂载的文件系统是 Pod 在 pod.spec.volumes 字段中使用的所有文件系统。

这意味着:对于 PV/PVC 卷,PV 中使用的文件系统需要支持 idmap 挂载;对于 hostPath 卷,hostPath 中使用的文件系统需要支持 idmap 挂载。

这对 secrets/configmaps/projected/downwardAPI 卷意味着什么?对于这些卷,kubelet 会创建一个 tmpfs 文件系统。所以,你需要 6.3 版本的内核才能使用这些卷(请注意,如果你将它们用作环境变量则没有问题)。

那么 emptyDir 卷呢?这些卷默认由 kubelet 在 /var/lib/kubelet/pods/ 中创建。你也可以为此使用自定义目录。但需要支持 idmap 挂载的是该目录中使用的文件系统。

kubelet 还会为容器创建一些其他文件,如 /etc/hostname/etc/resolv.conf/dev/termination-log/etc/hosts 等。这些文件默认也创建在 /var/lib/kubelet/pods/ 中,所以该目录中使用的文件系统支持 idmap 挂载非常重要。

此外,一些容器运行时可能会将这些临时卷放在一个 tmpfs 文件系统中,在这种情况下,你需要 tmpfs 支持 idmap 挂载。

5. 我可以使用低于 6.3 版本的内核吗?

是的,但你需要确保你没有使用 tmpfs 文件系统。如果你避免使用它,你可以轻松地使用 5.19 版本(如果所有你使用的其他文件系统在该内核中支持 idmap 挂载)。

不过,要避免使用 tmpfs 可能有点棘手,正如我们上面所描述的。除了必须避免那些卷类型,你还必须避免挂载服务账号令牌。每个 Pod 默认都会挂载它,并且它使用了一个 projected 卷,正如我们提到的,它使用了一个 tmpfs 文件系统。

你甚至可以使用比 5.19 更低的版本,一直到 5.12。然而,你的容器根文件系统(rootfs)可能使用 overlayfs 文件系统,而对 overlayfs 的支持是在 5.19 版本中添加的。我们不建议使用低于 5.19 版本的内核,因为无法在 rootfs 上使用 idmap 挂载是一个很大的限制。如果你绝对需要这样做,可以查看 Rodrigo 几年前写的这篇博客文章,其中介绍了在 rootfs 不支持 idmap 挂载时使用用户名字空间的技巧。

6. 如果我的技术栈支持用户名字空间,我还需要配置其他东西吗?

不,如果你的技术栈支持它并且你正在使用 Kubernetes v1.33,你*不需要*配置任何东西。你应该能够按照任务文档操作:为 Pod 使用用户名字空间

然而,如果你有特定的需求,你可以配置各种选项。你可以在这里找到更多信息。你还可以启用一个特性门控来放宽 PSS 规则

7. 那些演示很不错,但这个功能还能缓解更多的 CVE 吗?

是的,实际上相当多!除了演示中的那些,KEP 中还有更多你可以查看的 CVE。那个列表并不详尽,还有很多其他的。

8. 你能总结一下为什么用户名字空间很重要吗?

想象一下以 root 身份运行一个进程,甚至可能是一个不受信任的进程。你认为这安全吗?如果我们通过添加 seccomp 和 apparmor 来限制它,屏蔽 /proc 中的一些文件(这样它就不会使节点崩溃等),以及一些其他的调整呢?

如果我们从一开始就不给它特权,而不是试图玩“打地鼠”游戏来应对 root 可能逃逸的所有方式,那不是更好吗?

这就是用户名字空间所做的,外加一些其他的好处:

  • 在主机上以非特权用户身份运行,而无需对你的应用程序进行任何更改。Greg 和 Vinayak 在一次精彩的演讲中谈到了在没有用户名字空间的情况下尝试以非特权方式运行时可能遇到的痛苦。关于痛苦的部分从这个时间点开始

  • 所有 Pod 都以不同的 UID/GID 运行,我们显著改善了横向移动的防护。这通过用户名字空间得到了保证(kubelet 会为你选择)。在同一次演讲中,Greg 和 Vinayak 展示了为了在没有用户名字空间的情况下实现同样的目标,他们经历了一个相当复杂的自定义解决方案。这部分从这个时间点开始

  • 授予的能力仅在用户名字空间内有效。这意味着如果一个 Pod 从容器中逃逸出来,这些能力在主机上是无效的。没有用户名字空间我们无法提供这一点。

  • 它以一种*安全*的方式启用了新的使用场景。你可以运行 Docker in Docker、非特权容器构建、Kubernetes in Kubernetes 等,所有这些都**以一种安全的方式**。以前的大多数解决方案都需要特权容器或将节点置于高度的被攻破风险中。

9. 有关于用户名字空间的容器运行时文档吗?

是的,我们有 containerd 的文档。这解释了 containerd 1.7 的不同限制以及如何在没有 Kubernetes Pod 的情况下在 containerd 中使用用户名字空间(使用 ctr)。请注意,如果你使用 containerd,你需要 containerd 2.0 或更高版本才能在 Kubernetes 中使用用户名字空间。

CRI-O 没有专门的用户名字空间文档,它可以直接开箱即用。

10. 其他容器运行时呢?

据我们所知,没有其他容器运行时支持在 Kubernetes 中使用用户名字空间。遗憾的是,这其中也包括 cri-dockerd

11. 我想了解更多,你有什么推荐?

Rodrigo 在 KubeCon 2022 上对用户名字空间做了一个介绍:

此外,前面提到的在 KubeCon 2023 上的这个演讲也可以作为用户名字空间的动机:

请注意,这些演讲是几年前的,从那时起有些事情已经发生了变化。请以 Kubernetes 文档作为事实的来源。

如果你想了解更多关于用户名字空间的底层细节,可以查看 man 7 user_namespacesman 1 unshare。你可以轻松地创建名字空间并实验它们的行为。请注意,unshare 工具有很大的灵活性,因此也有创建不完整设置的选项。

如果你想了解更多关于 idmap 挂载的信息,可以查看其 Linux 内核文档

结论

以 root 身份运行 Pod 并不理想,而在容器中以非 root 身份运行也很困难,因为它可能需要对应用程序进行大量更改。用户名字空间是一个独特的特性,让你能够两全其美:以非 root 身份运行,而无需对你的应用程序进行任何更改。

这篇文章涵盖了:什么是用户名字空间,它们为什么重要,一些由用户名字空间缓解的真实 CVE 示例,以及一些常见问题。希望这篇文章能帮助你消除最后的疑虑,现在你会去尝试用户名字空间(如果你还没有的话!)。

我如何参与?

你可以通过多种方式联系 SIG Node

你也可以直接联系我们:

  • GitHub: @rata @giuseppe @saschagrunert
  • Slack: @rata @giuseppe @sascha