Kubernetes v1.37:用 bind mount 选项与 emptyDir 权限加固容器存储
Kubernetes v1.37 加入两个存储安全特性:卷挂载上的 bind mount 选项(noexec / nosuid / nodev)与 emptyDir 的创建权限模式(含 sticky bit 01777)。前者补上了一个从 1.24 安全审计就被点名的缺口,两个都还在 Alpha 门控之后。
中文
复制
Kubernetes v1.37 带来了重要的存储安全特性:emptyDir 权限模式和 bind mount 选项。它们帮助应用开发者和安全从业者实施严格的安全策略,比如禁止跨容器删除文件、禁止从可写卷里执行任意二进制,而且可以直接在 Kubernetes 里做到,不需要任何复杂的绕行办法。
Linux 存储与权限基础
在深入这些新的 Kubernetes 特性之前,先简单回顾一下让它们成为可能的那些底层 Linux 安全机制。
Bind mount 标志
当 Linux 挂载或重新挂载一个目录时,虚拟文件系统(VFS)标志决定这个文件系统上允许哪些操作:
noexec:不允许在这个挂载的文件系统上直接执行任何二进制。nosuid:不允许 set-user-identifier 或 set-group-identifier 位生效。nodev:不解析这个文件系统上的字符设备或块设备。
目录权限与 sticky bit
标准的 Unix 权限在三个范围上管控访问:属主、属组、其他人(例如 0755 或 0777)。
除了标准的读、写、执行位之外,Linux 还支持 sticky bit(例如模式 01777)。作用在目录上时,sticky bit 保证目录里的文件只能被文件的属主或 root 删除或改名。对于 /tmp 这类共享的可写目录,这是必需的。
这些改进的动因
Kubernetes 为什么需要 bind mount 选项和 emptyDir 权限?
这些特性的首要目标是提高 Kubernetes 工作负载的安全性,办法是允许在卷挂载上使用与安全相关的 bind mount 选项。默认情况下,卷由容器运行时和 kubelet 以 bind mount 方式挂进容器,不带 noexec、nosuid 或 nodev 标志。这个默认值会削弱安全性。举例来说,缺少 noexec 时,一个被攻陷的进程可以利用任何可写卷(emptyDir、PersistentVolume 等等)下载任意二进制、chmod +x 再执行它,即使容器的根文件系统是只读的(readOnlyRootFilesystem: true)。支持 noexec、nodev 和 nosuid,让用户有了原生的办法来加固卷挂载,以符合安全基准和策略。
这个缺口在 emptyDir 卷上最明显,它是最常见的可写卷类型,也是多项安全发现的对象:
- Issue #48912:被认定的安全缺口——无法在
emptyDir上设置挂载选项这一点在审计中被指出,但一直悬而未决,直到现在。 - Issue #119627:Kubernetes 1.24 安全审计(发现编号 NCC-E003660-7HM)——外部审计人员特别指出,无法以
noexec挂载emptyDir构成一次安全失效。
不过,同样的缺口适用于所有卷类型。PersistentVolume 有一个 mountOptions 字段,但那些选项是 CSI 驱动在节点上应用的文件系统级标志,因此并不能可靠地转成容器内部的 bind mount 标志。此前没有任何机制可以为容器运行时为任何卷类型创建的 bind mount 设置 noexec、nosuid 或 nodev。
此外,emptyDir 卷类型默认以硬编码的 0777 模式创建目录。这在此前意味着,任何能发现这个卷的进程都能读、写、删除卷里的任何东西,不管是谁创建的。
你可以——现在也仍然可以——用一个初始容器去设置不同的访问模式,但这更复杂,而且难以通过合规核验。
这会造成真实的问题:
- 共享一个
emptyDir的多容器 Pod,无法阻止一个容器删掉另一个容器的文件。sticky bit(01777)能解决这个问题,但此前没有原生的办法去设置它。 - 一些应用和安全框架期望
/tmp目录带 sticky bit(模式01777)。在没有原生支持设置emptyDir模式的情况下,用户不得不借助 init 容器或其他卷类型来满足这个要求。 - 想要更严格权限的平台工程师(比如只给属主和属组的
0750),只能使用跑chmod的 init 容器,这平添了不必要的复杂度。
emptyDir 卷类型是一个明显的缺口。作为 Kubernetes 里最常见的可写卷类型之一,它却没有任何办法控制自己创建时的权限。
现实中的用例
应用开发者与安全工程师紧密合作,负责维护应用的安全态势,并确保工作负载不会给更广泛的基础设施带来风险。这些特性让开发团队能够有信心地应对关键的安全场景:
防止可写挂载上的权限提升: 配置临时工作区卷(比如 emptyDir 或 /tmp 挂载)的应用开发者,可以确保它们以 nosuid 和 noexec 挂载。这保证了即使应用被攻陷、恶意载荷被下载下来,工作负载也无法执行这个载荷,更无法用它去提升节点上的权限。
保护多容器 Pod 里的共享临时空间: 配置 CI/CD 流水线 Pod 的开发者经常需要多个容器(比如一个构建容器和一个 sidecar 日志容器)共享一个工作区。通过在 emptyDir 上设置 mode: 01777,开发者让这个共享工作区表现得像传统的 Unix /tmp 目录。每个容器都能独立写文件,但一个容器里被攻陷的进程无法删掉另一个容器产出的构建产物。
为应用数据落实最小权限原则: 部署数据库 Pod 的应用开发者可以锁死对数据库临时存储的访问。通过在 emptyDir 上设置 mode: 0750,开发者确保只有特定的数据库用户和组能读写这个卷,明确拒绝同一个 Pod 里任何其他进程或 sidecar 的访问。
注意: 这两个特性在 Kubernetes v1.37 里都处于 Alpha 特性门控之后。要使用它们,需要在 API server 和 kubelet 上启用 VolumeBindMountOptions 和 EmptyDirVolumeMode。
示例 1:落实 bind mount 选项
这份完整的 Pod manifest 把一个 emptyDir 卷挂载到 /tmp,并带上 bindMountOptions: [noexec, nosuid]。
apiVersion: v1
kind: Pod
metadata:
name: hardened-bindmount-pod
namespace: default
spec:
os:
name: linux
containers:
- name: hardened-app
image: alpine:latest
command: ["sleep", "3600"]
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: temp-storage
mountPath: /tmp
bindMountOptions:
- noexec
- nosuid
volumes:
- name: temp-storage
emptyDir: {}
示例 2:带 sticky bit 的 emptyDir 卷权限模式
这份完整的 Pod manifest 用 mode: 01777 创建一个 emptyDir 卷,以在容器之间落实标准 Unix /tmp 的 sticky bit 保护。
apiVersion: v1
kind: Pod
metadata:
name: hardened-emptydir-pod
namespace: default
spec:
os:
name: linux
containers:
- name: app-container
image: alpine:latest
command: ["sleep", "3600"]
volumeMounts:
- name: shared-tmp
mountPath: /tmp
volumes:
- name: shared-tmp
emptyDir:
mode: 01777
在 Linux 上验证这些特性
要验证这些特性确实在强制执行限制,你可以用 kubectl exec 进入容器。下面的示例模拟了一些尝试执行、但会被这些特性成功拦下的动作。
验证 noexec
尝试在一个以 noexec 挂载的卷上写入并运行一个脚本:
# 1. Exec into the pod
kubectl exec -it hardened-bindmount-pod -- sh
# 2. Create an executable script on the mounted volume
cd /tmp
echo '#!/bin/sh' > test.sh
echo 'echo "Executing untrusted code..."' >> test.sh
chmod +x test.sh
# 3. Attempt to run the script
./test.sh
预期结果:
sh: ./test.sh: Permission denied
即使可执行文件被创建出来了,Linux 内核也会拒绝执行它,因为 MS_NOEXEC 在 bind mount 这一层被强制执行。
验证 sticky bit
尝试在一个权限模式为 01777 的 emptyDir 里删掉另一个用户的文件:
# 1. Exec into the pod
kubectl exec -it hardened-emptydir-pod -- sh
# 2. Verify directory permissions on /tmp
ls -ld /tmp
# Output: drwxrwxrwt 2 root root ... /tmp (Notice the 't' indicating sticky bit)
# 3. Create a file as the guest user
su -s /bin/sh -c "touch /tmp/guest_file" guest
# 4. Attempt to delete that file as nobody
su -s /bin/sh -c "rm /tmp/guest_file" nobody
预期结果:
rm: can't remove '/tmp/guest_file': Operation not permitted
内核拦下了这次删除,因为 sticky bit(01777)把文件删除严格限制在文件属主身上。
需要知道的事
开始使用这些特性时,请记住下面这些关键细节。完整细节可以在 bind mount 选项、emptyDir 卷模式,以及 emptyDir 卷的官方文档里找到。
- 默认行为不变: 如果你不写
bindMountOptions,或者不给emptyDir设置mode,你会得到和以前完全一样的标准默认行为(比如0777权限)。 - 卷类型支持很广:
bindMountOptions可用于emptyDir、PersistentVolume、CSI 卷、projected 卷、ConfigMap、Secret 等等。唯一的例外是 image 卷,它被明确不支持。mode字段适用于所有emptyDir介质类型:默认(磁盘支撑)、Memory(tmpfs)和HugePages。 - 运行时能力有关系(针对
bindMountOptions): 容器运行时必须支持 CRI 的mount_options字段,并通过runtimeFeatures声明它。调度器使用_节点声明的特性_来避免把 Pod 放到不兼容的节点上。如果一个 Pod 还是到了这样的节点上,kubelet 会拒绝它。不存在静默降级。不过,给emptyDir使用mode不需要运行时支持。 - 和 PV 的 mountOptions 不是一回事: PersistentVolume 的
mountOptions通过 CSI 驱动作用在存储层。新的bindMountOptions控制的是由运行时应用在容器内部的 bind mount 标志。它们作用在不同层次上,彼此不冲突。 - 与 fsGroup 的交互: 如果 Pod 的 security context 里设置了
fsGroup,那么由fsGroup应用的组权限会覆盖为emptyDir卷指定的mode。这与 Secret 和 ConfigMap 卷上defaultMode的既有行为一致。 - 仅限 Linux:
noexec、nosuid、nodev这类标志和 Unix 权限模式都是 Linux 的概念。bindMountOptions在 Windows 节点上没有效果。在 Windows 上,emptyDir卷的mode字段也会被跳过,因为 Windows 不支持 Unix 风格的文件权限。 - 版本偏斜是安全的: 这两个特性都是纯增量的。对于
emptyDir的mode:如果 API server 启用了门控而 kubelet 没有,字段会被接受但忽略,kubelet 回落到0777。对于bindMountOptions:调度器用「节点声明的特性」来避免把 Pod 放到没有运行时支持的节点上;如果 Pod 到了这样的节点,kubelet 会拒绝它,而不是静默忽略这些选项。 - 特性门控: 这两项能力在 Kubernetes v1.37 里都以 Alpha 特性提供:
VolumeBindMountOptions:控制卷挂载上的 bind mount 标志。 EmptyDirVolumeMode:控制emptyDir卷创建时的权限模式。
我该怎么参与?
这些新特性由 SIG Node 和 SIG Storage 推动。你可以在这些增强的 KEP 里找到更多细节:KEP-5855(bind mount 选项)和 KEP-5502(emptyDir 权限模式)。
联系 SIG Node:
- Slack: #sig-node
- Mailing list
联系 SIG Storage:
- Slack: #sig-storage
- Mailing list