Kubernetes 1.31: 细粒度 SupplementalGroups 控制

本文讨论了 Kubernetes 1.31 中一个新功能,该功能旨在改进 Pod 中容器内补充组(supplementary groups)的处理方式。

动机:容器镜像中 /etc/group 文件里定义的隐式组成员关系

尽管许多 Kubernetes 集群用户/管理员可能不喜欢这种行为,但 Kubernetes 默认会**合并**来自 Pod 的组信息与容器镜像中 /etc/group 文件里定义的信息。

让我们看一个示例,下面的 Pod 在其 security context 中指定了 runAsUser=1000runAsGroup=3000supplementalGroups=4000

apiVersion: v1
kind: Pod
metadata:
  name: implicit-groups
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    supplementalGroups: [4000]
  containers:
  - name: ctr
    image: registry.k8s.io/e2e-test-images/agnhost:2.45
    command: [ "sh", "-c", "sleep 1h" ]
    securityContext:
      allowPrivilegeEscalation: false

ctr 容器中执行 id 命令的结果是什么?

# Create the Pod:
$ kubectl apply -f https://k8s.io/blog/2024-08-22-Fine-grained-SupplementalGroups-control/implicit-groups.yaml

# Verify that the Pod's Container is running:
$ kubectl get pod implicit-groups

# Check the id command
$ kubectl exec implicit-groups -- id

那么,输出应该类似于这样

uid=1000 gid=3000 groups=3000,4000,50000

补充组(groups 字段)中的组 ID 50000 是从哪里来的,尽管 Pod 的 manifest 中根本没有定义 50000?答案是容器镜像中的 /etc/group 文件。

检查容器镜像中 /etc/group 文件的内容应该显示如下:

$ kubectl exec implicit-groups -- cat /etc/group
...
user-defined-in-image:x:1000:
group-defined-in-image:x:50000:user-defined-in-image

啊哈!容器的主要用户 1000 在最后一条记录中属于组 50000

因此,容器镜像中 /etc/group 文件为容器主要用户定义的组成员关系被**隐式地**合并到了来自 Pod 的信息中。请注意,这是当前 CRI 实现从 Docker 继承的一个设计决策,社区直到现在才真正重新考虑它。

这有什么问题?

容器镜像中 /etc/group 文件**隐式**合并的组信息可能会引起一些问题,特别是在访问卷(volumes)时(详见 kubernetes/kubernetes#112879),因为 Linux 中的文件权限是由 uid/gid 控制的。更糟糕的是,由于 manifest 中没有任何关于隐式组信息的线索,任何策略引擎都无法检测/验证来自 /etc/group 的隐式 gid。这也可能是一个 Kubernetes 安全问题。

Pod 中细粒度的 SupplementalGroups 控制:SupplementaryGroupsPolicy

为了解决上述问题,Kubernetes 1.31 在 Pod 的 .spec.securityContext 中引入了新的 supplementalGroupsPolicy 字段。

该字段提供了一种控制如何为 Pod 中容器进程计算补充组的方法。可用的策略如下:

  • Merge:容器主要用户在 /etc/group 中定义的组成员关系将被合并。如果未指定,将应用此策略(即为了向后兼容保持原有行为)。

  • Strict:它只将 fsGroupsupplementalGroupsrunAsGroup 字段中指定的组 ID 作为容器进程的补充组。这意味着容器主要用户在 /etc/group 中定义的任何组成员关系都不会被合并。

让我们看看 Strict 策略是如何工作的。

apiVersion: v1
kind: Pod
metadata:
  name: strict-supplementalgroups-policy
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    supplementalGroups: [4000]
    supplementalGroupsPolicy: Strict
  containers:
  - name: ctr
    image: registry.k8s.io/e2e-test-images/agnhost:2.45
    command: [ "sh", "-c", "sleep 1h" ]
    securityContext:
      allowPrivilegeEscalation: false
# Create the Pod:
$ kubectl apply -f https://k8s.io/blog/2024-08-22-Fine-grained-SupplementalGroups-control/strict-supplementalgroups-policy.yaml

# Verify that the Pod's Container is running:
$ kubectl get pod strict-supplementalgroups-policy

# Check the process identity:
kubectl exec -it strict-supplementalgroups-policy -- id

输出应该类似于这样

uid=1000 gid=3000 groups=3000,4000

你可以看到 Strict 策略可以将组 50000groups 中排除!

因此,确保 supplementalGroupsPolicy: Strict(由某些策略机制强制执行)有助于防止 Pod 中出现隐式补充组。

Pod status 中的附加进程身份

此功能还通过 .status.containerStatuses[].user.linux 字段暴露了附加到容器第一个容器进程的进程身份。这有助于查看是否附加了隐式组 ID。

...
status:
  containerStatuses:
  - name: ctr
    user:
      linux:
        gid: 3000
        supplementalGroups:
        - 3000
        - 4000
        uid: 1000
...

功能可用性

要启用 supplementalGroupsPolicy 字段,必须使用以下组件:

  • Kubernetes:v1.31 或更高版本,并启用 SupplementalGroupsPolicy feature gate。截至 v1.31,该门控被标记为 alpha。
  • CRI 运行时
    • containerd:v2.0 或更高版本
    • CRI-O:v1.31 或更高版本

你可以在 Node 的 .status.features.supplementalGroupsPolicy 字段中查看该功能是否受支持。

apiVersion: v1
kind: Node
...
status:
  features:
    supplementalGroupsPolicy: true

下一步?

Kubernetes SIG Node 希望(并期望)该功能在 Kubernetes 的未来版本中晋升为 Beta 并最终进入正式可用(GA),以便用户不再需要手动启用 feature gate。

当未指定 supplementalGroupsPolicy 时,将应用 Merge 策略,以实现向后兼容。

如何了解更多?

如何参与?

此功能由 SIG Node 社区推动。请加入我们,与社区联系,分享你关于上述功能及其他方面的想法和反馈。我们期待你的参与!