2018年3月12日,Kubernetes产品安全团队透露CVE-2017-1002101,允许使用子路径卷装载的容器访问卷外的文件。这意味着容器可以访问主机上可用的任何文件,包括它不应该访问的其他容器的卷。
该漏洞已在最新的Kubernetes补丁版本中得到修复和发布。我们建议所有用户升级以获得修复。有关影响以及如何获得修复的更多详细信息,请参阅公告。(请注意,一些功能回归是在初始修复后发现的,并且正在#61563的问题中进行跟踪)。
这篇文章对这个漏洞和解决方案进行了深入的技术介绍。
Kubernetes背景
要理解这个漏洞,首先必须了解Kubernetes中的卷和子路径安装是如何工作的。
在节点上启动容器之前,kubelet卷管理器将在主机系统上的该Pod中的一个目录下本地装入PodSpec中指定的所有卷。一旦所有卷成功安装,它就构建了要传递给容器运行时的卷装入列表。每个卷装载包含容器运行时需要的信息,最相关的是:
1.容器中容积的路径
2.主机上卷的路径(/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被调度到节点时,系统将:
1.设置一个EmptyDir卷 /var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume
2.构建子路径安装的主机路径: /var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/ + dataset1
3.将下列装载信息传递给容器运行时:
集装箱路径: /mnt/data
主机路径: /var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/dataset1
4.容器运行时绑定/mnt/data在容器根文件系统中/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/dataset1安装到主机上。
5.容器运行时启动容器。
漏洞
Maxim Ivanov发现了子路径卷的漏洞,并发表了一些观察结论:
1.子路径引用由用户控制的文件或目录,而不是系统。
2.卷可以由Pod生命周期中不同时间的容器共享,包括不同的Pod。
3.Kubernetes将主机路径传递给容器运行时以将装载绑定到容器中。
下面的基本示例演示了此漏洞。它利用上面概述的观察结果:
1.使用init容器通过符号链接设置卷。
2.使用常规容器稍后将该符号链接安装为子路径。
3.导致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: {}
在这个例子中,系统将会:
1.在设置EmptyDir卷 /var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume
2.将init容器的以下安装信息传递给容器运行时:
集装箱路径: /mnt/data
主机路径: /var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume
3.容器运行时绑定/mnt/data在容器根文件系统中/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume安装到主机上。
4.容器运行时启动init容器。
5.init容器在容器内创建一个符号链接:/mnt/data/symlink-door- > /,然后退出。
6.Kubelet开始为普通容器准备卷装。
7.它构建子路径卷挂载的主机路径:/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/ + symlink-door。
8.并将下列装载信息传递给容器运行时:
集装箱路径: /mnt/data
主机路径: /var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty-dir/my-volume/symlink-door
9.容器运行时绑定安装/mnt/data在容器根文件系统中/var/lib/kubelet/pods/1234/volumes/kubernetes.io~empty~dir/my-volume/symlink-door
10.但是,bind mount可以解析符号链接,在这种情况下,它会解析到/主机上!现在容器可以通过它的挂载点看到主机的所有文件系统/mnt/data。
这是符号链接竞赛的一种表现形式,恶意用户程序可以通过使特权程序(在本例中为kubelet)遵循用户创建的符号链接来访问敏感数据。
应该注意的是,根据卷的类型,此容器并不总是需要init容器。它用于EmptyDir示例中,因为EmptyDir卷不能与其他Pod共享,并且只能在创建Pod时创建,并在Pod被销毁时销毁。对于持续卷类型,此漏洞也可以跨两个共享相同卷的不同Pod进行。
修正
根本问题是子路径的主机路径是不可信的,并且可以指向系统中的任何位置。修复程序需要确保此主机路径是:
1.解决并验证指向基本卷内部。
2.用户在验证时与容器运行时绑定装入时不可更改。
Kubernetes产品安全团队在最终达成设计协议之前,经历了许多可能的解决方案迭代。
想法1
我们的第一个设计相对简单。对于每个容器中的每个子路径安装:
1.解决子路径的所有符号链接。
2.验证解析的路径是否在卷内。
3.将解析后的路径传递给容器运行时。
但是,这种设计很容易出现经典的检查到使用时间(TOCTTOU)问题。在步骤2)和3)之间,用户可以将路径更改回符号链接。正确的解决方案需要通过某种方式来“锁定”路径,以便在容器运行时在验证和绑定挂载之间不能更改路径。所有后续的想法都通过kubelet使用中间绑定装载来实现此“锁定”步骤,然后将其交给容器运行时。绑定安装完成后,安装源是固定的,不能更改。
想法2
我们对这个想法有些狂热:
1.在kubelet的pod目录下创建一个工作目录。我们称之为dir1。
2.将基础卷绑定到工作目录下,dir1/volume。
3.Chroot到工作目录dir1。
4.在chroot里面,绑定volume/subpath到subpath。这确保了任何符号链接都可以解析到chroot环境中。
退出chroot。
5.再次在主机上,传递装载dir1/subpath到容器运行时的绑定。
尽管这种设计确保符号链接不能指向卷之外,但由于在所有各种Kubernetes必须支持的发行版和环境(包括集装箱化kubelets)中实施chroot机制的困难,最终被拒绝。
想法3
我们的下一个想法是:
1.将子路径绑定到kubelet的pod目录下的工作目录。
2.获取绑定装入的来源,并验证它位于基本卷内。
3.将绑定挂载传递给容器运行时。
理论上,这听起来很简单,但实际上,2)很难正确实施。卷(如EmptyDir)可能位于共享文件系统上,独立文件系统上,根文件系统上或不在根文件系统上时,必须处理许多场景。NFS卷最终将所有绑定挂载作为独立挂载处理,而不是作为基本卷的子处理。对于树外卷类型(我们无法测试)将如何表现还存在额外的不确定性。
解决方案
鉴于必须使用先前设计处理的场景和角落情况的数量,我们确实希望找到一种在所有卷类型中更通用的解决方案。我们最终的最终设计是:
1.解决子路径中的所有符号链接。
2.从基本卷开始,使用openat()系统调用逐个打开每个路径段,并禁用符号链接。使用每个路径段,验证当前路径是否位于基本卷内。
3.将挂载绑定/proc/<kubelet pid>/fd/<final fd>到kubelet的pod目录下的工作目录。proc文件是到打开文件的链接。如果该文件在kubelet仍然打开的情况下被替换,那么链接仍然会指向原始文件。
4.关闭fd并将绑定挂载传递给容器运行时。
请注意,对于Windows主机,这种解决方案是不同的,其中安装语义与Linux不同。在Windows中,设计是:
1.解决子路径中的所有符号链接。
2.从基本卷开始,使用文件锁逐个打开每个路径段,并禁止符号链接。使用每个路径段,验证当前路径在基本卷内。
3.将已解析的子路径传递给容器运行时,然后启动容器。
4.容器启动后,解锁并关闭所有文件。
这两种解决方案都能够满足以下所有要求:
1.解析子路径并验证它是否指向基本卷内的路径。
2.确保子路径主机路径无法在验证时间与容器运行时绑定装入时间之间进行更改。
3.足够通用以支持所有卷类型。
致谢
特别感谢许多参与处理此漏洞的人士:
1.Maxim Ivanov,负责任地向Kubernetes产品安全团队披露了漏洞。
2.来自Google,Microsoft和RedHat的Kubernetes存储和安全工程师开发,测试并审查了这些修补程序。
3.Kubernetes测试团队,用于建立私人构建基础架构
4.Kubernetes补丁发布经理,协调和处理所有版本。
5.所有生产发布团队在发布后迅速部署修补程序
如果您在Kubernetes中发现漏洞,请遵循我们负责任的披露流程并告诉我们 ; 我们希望尽最大努力让Kubernetes为所有用户提供安全保障。
- 谷歌软件工程师Michelle Au; 和红帽软件工程师JanŠafránek