Kubernetes 1.37 存储加固:用 noexec 与 emptyDir 权限堵住可写卷风险

2026-09-17 17 预计阅读时间: 1 分钟
来源: kubernetes.io AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

Kubernetes v1.37 为 Linux 工作负载补上了两块长期缺失的存储安全能力:在容器的卷绑定挂载上设置 noexecnosuidnodev,以及直接指定 emptyDir 的创建权限。过去需要 init container 执行 chmod、修改镜像入口或依赖存储驱动的场景,现在可以直接在 Pod API 中声明。

这两项能力目前都处于 Alpha 阶段,但它们解决的问题很实际:只读根文件系统并不代表容器没有可执行的落脚点,而默认模式为 0777 的共享临时目录,也无法阻止一个容器删除另一个容器创建的文件。

只读根文件系统为何还不够

常见的加固配置是:

securityContext:
  readOnlyRootFilesystem: true

它能阻止进程修改容器根文件系统,却不会自动限制挂载进来的卷。攻击者一旦控制应用进程,仍可能把文件写入 emptyDir、PersistentVolume 或其他可写卷,然后添加执行权限并尝试运行。

Kubernetes v1.37 新增的 bindMountOptions 控制的是容器运行时创建的绑定挂载,而不是底层存储文件系统:

  • noexec:禁止从该挂载点直接执行二进制文件或脚本。
  • nosuid:让 setuid、setgid 位不产生提权效果。
  • nodev:不把挂载中的字符设备或块设备文件解释为设备。

这与 PersistentVolume 原有的 mountOptions 不是一回事。PV 的 mountOptions 通常由 CSI 驱动作用于节点上的存储文件系统;bindMountOptions 则由容器运行时应用在容器看到的绑定挂载上。两者位于不同层次,可以同时存在。

下面这个 Pod 把 /tmp 设置为可写,但禁止从中直接执行文件,同时启用只读根文件系统:

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
        allowPrivilegeEscalation: false
      volumeMounts:
        - name: temp-storage
          mountPath: /tmp
          bindMountOptions:
            - noexec
            - nosuid
            - nodev
  volumes:
    - name: temp-storage
      emptyDir: {}

应用前需要在 API Server 和 kubelet 上启用 Alpha 特性门:

--feature-gates=VolumeBindMountOptions=true,EmptyDirVolumeMode=true

具体放置位置取决于集群安装方式。例如,自建静态 Pod 控制面的 API Server 参数通常位于 /etc/kubernetes/manifests/kube-apiserver.yaml,kubelet 参数则可能来自 systemd、kubelet 配置文件或集群管理工具。修改前应确认升级与回滚方案,不要直接在生产集群批量开启 Alpha 特性。

部署并验证 noexec

kubectl apply -f hardened-bindmount-pod.yaml
kubectl wait --for=condition=Ready pod/hardened-bindmount-pod --timeout=120s

kubectl exec hardened-bindmount-pod -- sh -c '
  cp /bin/busybox /tmp/test-binary
  chmod +x /tmp/test-binary
  /tmp/test-binary echo "should not run"
'

预期结果包含 Permission denied。文件仍然可以写入并设置执行位,但 Linux 内核会拒绝从该挂载点直接执行它。

需要注意,noexec 不是完整的应用沙箱。它主要阻止直接执行,例如 ./payload;如果容器里已有解释器,攻击者仍可能让解释器读取脚本,如 sh /tmp/payload.sh。因此还应结合最小化镜像、移除不必要的解释器和工具、非 root 用户、seccomp、能力集裁剪以及网络出口控制。

用 01777 保护多容器共享目录

emptyDir 过去使用固定的 0777 权限创建。对共享临时空间而言,“所有进程都能写”不一定是问题,“所有进程都能删除别人的文件”才是。

Linux 目录的 sticky bit 正是为此设计的。模式 01777 中的前导 1 表示 sticky bit:目录仍允许多个用户写入,但文件通常只能由文件所有者、目录所有者或 root 删除和重命名。这也是传统 /tmp 常见的权限模型。

下面的 Pod 用两个不同 UID 的容器共享同一个 emptyDir

apiVersion: v1
kind: Pod
metadata:
  name: sticky-emptydir-demo
  namespace: default
spec:
  os:
    name: linux
  containers:
    - name: writer-a
      image: alpine:latest
      command: ["sleep", "3600"]
      securityContext:
        runAsUser: 1001
        runAsGroup: 1001
        runAsNonRoot: true
      volumeMounts:
        - name: shared-tmp
          mountPath: /shared
    - name: writer-b
      image: alpine:latest
      command: ["sleep", "3600"]
      securityContext:
        runAsUser: 1002
        runAsGroup: 1002
        runAsNonRoot: true
      volumeMounts:
        - name: shared-tmp
          mountPath: /shared
  volumes:
    - name: shared-tmp
      emptyDir:
        mode: 01777

保存为 sticky-emptydir-demo.yaml 后,可以完整验证:

kubectl apply -f sticky-emptydir-demo.yaml
kubectl wait --for=condition=Ready pod/sticky-emptydir-demo --timeout=120s

kubectl exec sticky-emptydir-demo -c writer-a -- sh -c '
  id
  touch /shared/owned-by-a
  ls -ln /shared/owned-by-a
'

kubectl exec sticky-emptydir-demo -c writer-b -- sh -c '
  id
  ls -ld /shared
  rm /shared/owned-by-a
'

第二个容器执行删除时,预期得到 Operation not permitted。通过 ls -ld /shared 应能看到类似下面的权限,其中末尾的 t 表示 sticky bit:

drwxrwxrwt ... /shared

如果目标不是共享 /tmp,而是数据库临时目录或仅限特定用户组访问的工作区,也可以采用更严格的模式:

volumes:
  - name: database-scratch
    emptyDir:
      mode: 0750

但权限是否符合预期还取决于容器 UID、GID 和 Pod 的安全上下文。特别是设置了 fsGroup 时,Kubernetes 对组权限的处理会覆盖 emptyDir.mode 指定的相关权限,因此必须在真实 Pod 配置上检查最终结果。

调度、运行时与版本偏差

bindMountOptions 可用于 emptyDir、PersistentVolume、CSI 卷、projected 卷、ConfigMap 和 Secret 等多种卷,image 卷明确不支持。emptyDir.mode 则适用于磁盘、Memory(tmpfs)和 HugePages 等 emptyDir 介质。

上线前要特别检查以下边界:

  1. 仅适用于 Linux语义noexecnosuidnodev 和 Unix 权限模式不适用于 Windows 节点;Windows 会跳过 emptyDir 的模式设置。
  2. 容器运行时必须支持挂载选项:使用 bindMountOptions 时,运行时必须实现 CRI mount_options,并通过运行时特性进行声明。调度器会利用节点声明的能力避开不兼容节点;如果 Pod 仍到达不支持的节点,kubelet 会拒绝它,而不是悄悄忽略安全选项。
  3. emptyDir.mode 的版本偏差更危险:如果 API Server 启用了特性门而 kubelet 没有启用,该字段可能被接受但被 kubelet 忽略,目录回退为 0777。升级期间应主动检查每个节点的 kubelet 配置。
  4. 默认行为不变:不设置新字段时,现有工作负载继续沿用原行为。这降低了升级破坏性,但也意味着升级本身不会自动加固旧 Pod。
  5. 权限不能替代身份隔离:同一 UID 的两个容器仍可能操作彼此的文件;特权容器或 root 进程也可能绕过预期边界。需要同时审查 runAsUserrunAsGrouprunAsNonRoot 和特权设置。

可以用下面的命令快速检查节点与 Pod 的实际状态:

kubectl get nodes -o wide
kubectl describe pod hardened-bindmount-pod
kubectl get pod hardened-bindmount-pod -o yaml
kubectl exec hardened-bindmount-pod -- grep ' /tmp ' /proc/mounts

不要只以“Pod 成功创建”作为验收标准。应在目标运行时和节点内核上执行负向测试,确认恶意操作确实失败。

采用建议:先从临时目录开始

这两项 Alpha 能力适合从风险较低、行为清晰的临时卷开始试点:

  • 对只存放缓存、上传中间文件和临时构建产物的卷,优先评估 noexec,nosuid,nodev
  • 对多 UID 容器共享的临时目录,使用 01777 并验证跨用户删除失败。
  • 对数据库或单应用工作区,评估 07500700 等更小权限面。
  • 检查是否设置 fsGroup,避免最终组权限与声明值不同。
  • 盘点节点上的 Kubernetes、kubelet 和容器运行时版本,专门测试滚动升级期间的版本偏差。
  • 用策略引擎检查关键工作负载是否声明了需要的挂载选项,但在 Alpha 阶段保留例外和回滚机制。

Kubernetes v1.37 的变化并不会自动让卷变安全,但它把过去依赖 init container 和运维约定的控制,变成了可声明、可审计的 Pod 配置。对于已经启用只读根文件系统、非 root 运行和最小权限容器的团队,这正好补上了可写卷这一层防线。


相关推荐