这篇文章已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变为不正确。
修复 Kubernetes 中的子路径卷漏洞
2018 年 3 月 12 日,Kubernetes 产品安全团队披露了 CVE-2017-1002101,该漏洞允许使用 子路径卷挂载的容器访问卷外的文件。这意味着容器可以访问主机上可用的任何文件,包括它不应访问的其他容器的卷。
该漏洞已在最新的 Kubernetes 补丁版本中修复并发布。我们建议所有用户升级以获取修复。有关影响和如何获取修复的更多详细信息,请参阅公告。(注意,在初始修复后发现了一些功能退化,目前正在 issue #61563 中进行跟踪)。
这篇文章深入探讨了该漏洞和解决方案的技术细节。
Kubernetes 背景
要理解该漏洞,必须首先理解 Kubernetes 中卷和子路径挂载的工作方式。
在容器在节点上启动之前,kubelet 卷管理器会在主机系统上为该 Pod 在目录下本地挂载 PodSpec 中指定的所有卷。成功挂载所有卷后,它会构造要传递给容器运行时的卷挂载列表。每个卷挂载都包含容器运行时需要的信息,最相关的是
- 容器中卷的路径
- 主机上卷的路径(
/var/lib/kubelet/pods/<pod uid>/volumes/<volume type>/<volume name>
)
启动容器时,容器运行时会在容器根文件系统中创建路径(如有必要),然后将其绑定挂载到提供的主机路径。
子路径挂载像任何其他卷一样传递给容器运行时。容器运行时不区分基本卷和子路径卷,并以相同的方式处理它们。Kubernetes 不是将主机路径传递到卷的根目录,而是通过将 Pod 指定的子路径(相对路径)附加到基本卷的主机路径来构造主机路径。
例如,这是一个子路径卷挂载的规范
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
<snip>
volumeMounts:
- mountPath: /mnt/data
name: my-volume
subPath: dataset1
volumes:
- name: my-volume
emptyDir: {}
在此示例中,当 Pod 被调度到节点时,系统将
- 在
/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume
上设置一个 EmptyDir 卷 - 构造子路径挂载的主机路径:
/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/ + dataset1
- 将以下挂载信息传递给容器运行时
- 容器路径:
/mnt/data
- 主机路径:
/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/dataset1
- 容器路径:
- 容器运行时将容器根文件系统中的
/mnt/data
绑定挂载到主机上的/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/dataset1
。 - 容器运行时启动容器。
漏洞
Maxim Ivanov 通过一些观察发现了子路径卷的漏洞
- 子路径引用由用户控制而不是由系统控制的文件或目录。
- 卷可以由在 Pod 生命周期中不同时间(包括由不同 Pod)启动的容器共享。
- Kubernetes 将主机路径传递给容器运行时以绑定挂载到容器中。
下面的基本示例演示了该漏洞。它利用了上面概述的观察结果
- 使用 init 容器设置带有符号链接的卷。
- 使用常规容器稍后将该符号链接作为子路径挂载。
- 导致 kubelet 在将其传递到容器运行时之前在主机上评估符号链接。
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
initContainers:
- name: prep-symlink
image: "busybox"
command: ["bin/sh", "-ec", "ln -s / /mnt/data/symlink-door"]
volumeMounts:
- name: my-volume
mountPath: /mnt/data
containers:
- name: my-container
image: "busybox"
command: ["/bin/sh", "-ec", "ls /mnt/data; sleep 999999"]
volumeMounts:
- mountPath: /mnt/data
name: my-volume
subPath: symlink-door
volumes:
- name: my-volume
emptyDir: {}
对于此示例,系统将
- 在
/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume
上设置一个 EmptyDir 卷 - 将以下挂载信息传递给 init 容器的容器运行时
- 容器路径:
/mnt/data
- 主机路径:
/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume
- 容器路径:
- 容器运行时将容器根文件系统中的
/mnt/data
绑定挂载到主机上的/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume
。 - 容器运行时启动 init 容器。
- init 容器在容器内创建符号链接:
/mnt/data/symlink-door
->/
,然后退出。 - Kubelet 开始为普通容器准备卷挂载。
- 它构造子路径卷挂载的主机路径:
/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/ + symlink-door
。 - 并将以下挂载信息传递给容器运行时
- 容器路径:
/mnt/data
- 主机路径:
/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/symlink-door
- 容器路径:
- 容器运行时将容器根文件系统中的
/mnt/data
绑定挂载到/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty~dir/my-volume/symlink-door
- 但是,绑定挂载会解析符号链接,在本例中,它解析到主机上的
/
!现在,容器可以通过其挂载点/mnt/data
查看主机的整个文件系统。
这是 符号链接竞争的一种表现形式,其中恶意用户程序可以通过导致特权程序(在本例中为 kubelet)跟踪用户创建的符号链接来访问敏感数据。
应该注意的是,并非总是需要 init 容器来利用此漏洞,这取决于卷类型。它在 EmptyDir 示例中使用是因为 EmptyDir 卷不能与其他 Pod 共享,并且仅在创建 Pod 时创建,并在销毁 Pod 时销毁。对于持久卷类型,此漏洞也可以在共享同一卷的两个不同 Pod 之间完成。
修复
根本问题在于,子路径的主机路径是不可信任的,并且可以指向系统中的任何位置。修复需要确保此主机路径
- 已解析并验证为指向基本卷内部。
- 在验证时间和容器运行时绑定挂载它之间,用户不可更改。
Kubernetes 产品安全团队在最终达成设计共识之前,经历了多次可能的解决方案迭代。
想法 1
我们的第一个设计相对简单。对于每个容器中的每个子路径挂载
- 解析子路径的所有符号链接。
- 验证解析的路径是否在卷内。
- 将解析的路径传递给容器运行时。
但是,这种设计容易出现经典的检查时间到使用时间(TOCTTOU)问题。在步骤 2)和 3)之间,用户可以将路径改回符号链接。正确的解决方案需要某种方法来“锁定”路径,使其在验证和容器运行时绑定挂载之间不能更改。所有后续想法都使用 kubelet 的中间绑定挂载来实现此“锁定”步骤,然后再将其传递给容器运行时。执行绑定挂载后,挂载源是固定的,不能更改。
想法 2
我们对这个想法有点疯狂
- 在 kubelet 的 pod 目录下创建一个工作目录。我们称之为
dir1
。 - 将基本卷绑定挂载到工作目录下,
dir1/volume
。 - 使用 chroot 进入工作目录
dir1
。 - 在 chroot 内部,将
volume/subpath
绑定挂载到subpath
。这确保了任何符号链接都被解析到 chroot 环境内部。 - 退出 chroot。
- 再次在主机上,将绑定挂载的
dir1/subpath
传递给容器运行时。
虽然这种设计确实确保了符号链接不能指向卷外部,但由于在 Kubernetes 必须支持的所有各种发行版和环境中(包括容器化的 kubelet)实现 4) 中的 chroot 机制存在困难,最终被拒绝。
想法 3
稍微回到现实,我们的下一个想法是
- 将子路径绑定挂载到 kubelet 的 pod 目录下的工作目录。
- 获取绑定挂载的源,并验证它是否在基本卷内。
- 将绑定挂载传递给容器运行时。
从理论上讲,这听起来很简单,但实际上,2)很难正确实现。必须处理许多场景,其中卷(如 EmptyDir)可能在共享文件系统上、在单独的文件系统上、在根文件系统上或不在根文件系统上。NFS 卷最终将所有绑定挂载都作为单独的挂载来处理,而不是作为基本卷的子挂载。对于我们无法测试的树外卷类型,还有一些关于它们将如何表现的不确定性。
解决方案
鉴于之前的设计必须处理的大量场景和极端情况,我们真的想找到一种在所有卷类型中更通用的解决方案。我们最终采用的最终设计是
- 解析子路径中的所有符号链接。
- 从基本卷开始,使用
openat()
系统调用逐个打开每个路径段,并禁止使用符号链接。对于每个路径段,验证当前路径是否在基本卷内。 - 将
/proc/<kubelet pid>/fd/<final fd>
绑定挂载到 kubelet 的 pod 目录下的工作目录。proc 文件是指向打开文件的链接。如果 kubelet 仍然打开该文件时该文件被替换,则该链接仍将指向原始文件。 - 关闭 fd 并将绑定挂载传递给容器运行时。
请注意,对于 Windows 主机,此解决方案是不同的,其中挂载语义与 Linux 不同。在 Windows 中,设计是
- 解析子路径中的所有符号链接。
- 从基本卷开始,使用文件锁逐个打开每个路径段,并禁止使用符号链接。对于每个路径段,验证当前路径是否在基本卷内。
- 将解析的子路径传递给容器运行时,并启动容器。
- 容器启动后,解锁并关闭所有文件。
两种解决方案都能够满足所有要求
- 解析子路径并验证它指向基本卷内的路径。
- 确保在验证和容器运行时绑定挂载之间,子路径主机路径不能被更改。
- 具有足够的通用性,以支持所有卷类型。
鸣谢
特别感谢许多参与处理此漏洞的人员
- Maxim Ivanov,他负责任地向 Kubernetes 产品安全团队披露了此漏洞。
- 来自 Google、Microsoft 和 RedHat 的 Kubernetes 存储和安全工程师,他们开发、测试和审查了修复程序。
- Kubernetes 测试基础设施团队,负责搭建私有构建基础设施
- Kubernetes 补丁发布经理,负责协调和处理所有发布。
- 所有生产发布团队,他们在发布后迅速部署了修复程序。
如果您在 Kubernetes 中发现漏洞,请遵循我们负责任的披露流程并告知我们;我们希望尽最大努力使 Kubernetes 对所有用户都是安全的。