为 Pod 安全性应用 Security Context Constraints

本指南面向平台管理员和安全管理员。它介绍如何在现有 Kyverno 部署之上安装一个 SecurityContextConstraints (SCC) 引擎,以及如何将 SCC 配置文件绑定到 ServiceAccounts、Users 和 Groups,从而在准入时自动强制执行 Pod 安全边界。

简介

OpenShift 的 SecurityContextConstraints (SCC) 模型允许集群管理员定义一组 Pod 安全配置文件库,然后授予受试对象(ServiceAccounts、Users、Groups)使用特定配置文件的权限。当一个 Pod 被准入时,平台会选择该受试对象被允许使用的最合适的 SCC,补齐缺失的默认值,并根据该配置文件验证 Pod。工作负载本身无需声明每一个安全字段——SCC 配置文件会替它完成这些工作。

原生 Kubernetes 没有内置等效能力。本指南会安装一个基于 Kyverno 的引擎,在任何已运行 Kyverno 的标准 Kubernetes 集群上复现 SCC 体验。它使用:

  • 一个 SecurityContextConstraints CRD(security.alauda.io/v1alpha1)来存储 SCC 配置文件。
  • 标准 Kubernetes RBAC(use 动词加 resourceNames)将受试对象绑定到配置文件,因此操作流程与 OpenShift 保持一致(oc adm policy add-scc-to-user 模式可一一对应)。
  • 一对 Kyverno 准入策略——一个变更型、一个校验型——用于选择正确的 SCC、补齐默认值,并拒绝任何没有可接受 SCC 的 Pod。
  • 五个 GlobalContextEntry 资源,它们在内存中缓存 SCC 配置文件以及相关 RBAC 对象,因此准入决策不需要额外的 API 调用。

结果是:应用团队可以继续编写简单直接的 Pod 清单,集群会自动将其约束到其 ServiceAccount 被允许使用的安全配置文件中,而从 OpenShift 迁移时也无需更改绑定模型。

SCC 授权是一项安全控制变更。应用团队不应被授予直接创建或修改 SCC RBAC 绑定的权限,因为这样会让他们绕过集群安全边界。应用团队应描述工作负载需求,例如 anyuidhostNetworkhostPath;平台或安全管理员审查请求,并将最小权限 SCC 绑定到相应受试对象。

各角色职责

使用下表来判断本指南中的哪些部分适用于你。

角色你的工作你不应做的事
平台管理员或安全管理员安装 SCC 引擎、审批 SCC 请求、创建 SCC RBAC 绑定、将校验策略从 Warn 切换到 Deny,以及审计例外情况。不要在没有工作负载层面的明确理由和负责人时授予过宽的 SCC,例如 privilegedhostaccessanyuid
应用经理或应用所有者识别工作负载所需能力,例如 root UID、host networking、host ports、host paths、user namespaces 或固定 UID 范围。向平台或安全管理员提供 namespace、ServiceAccount、工作负载名称和原因。审批后,使用分配的 ServiceAccount 部署工作负载。不要创建 SCC RBAC 绑定,也不要向自己的 ServiceAccount 授予 SCC 权限。除非请求的 SCC 已经获批并完成绑定,否则不要使用 alauda.io/required-scc

如果你是平台或安全管理员,请按照第 1 部分和第 2 部分执行。如果你是应用经理,请先使用步骤 2.1 准备 SCC 请求,然后仅在管理员批准并完成绑定后,才使用步骤 2.5 和步骤 2.6。不要自行应用步骤 2.2 到步骤 2.4 中的 RBAC 清单。

标准工作流如下:

  1. 应用经理识别工作负载需求和目标 ServiceAccount。
  2. 平台或安全管理员选择最小权限 SCC 并创建 RBAC 绑定。
  3. 应用经理使用获批的 ServiceAccount 部署工作负载,只有在管理员要求固定到某个特定 SCC 时,才添加 alauda.io/required-scc
  4. 管理员使用 kubectl auth can-i 验证授权,而工作负载所有者验证准入后的 Pod 具有预期的 alauda.io/scc 注解。

适用场景

当出现以下任一情况时,应用本指南:

  • 你正在将工作负载从 OpenShift 迁移过来,并希望保留现有的 oc adm policy add-scc-to-* 绑定模型,以便平台团队和审计工具继续按原样工作。
  • 你已经在使用 Kyverno,并且需要一个集中管理的安全边界,而不要求每个 Pod 清单都声明完整的 securityContext
  • 你运行的是多租户集群,并希望不同 namespace 中的不同 ServiceAccount 具有不同的安全上限——例如,应用 SA 仅限 restricted-v2,日志收集 SA 允许 hostmount-anyuid,入口控制器 SA 允许 NET_BIND_SERVICE
  • 你希望在集群范围内有一个统一位置来表达和审计“谁被允许运行 privileged Pod”,而不是把例外分散到每个 namespace 中。

先决条件

开始前,请确保满足以下所有条件:

  1. Kubernetes 集群运行 1.30 或更高版本(CEL admission 已稳定)。

  2. Kyverno 已经安装并运行,版本为 v4.3.1 或更高,并且可用 MutatingPolicyValidatingPolicyGlobalContextEntry CRD。你可以使用以下命令验证:

    kubectl get crd validatingpolicies.policies.kyverno.io mutatingpolicies.policies.kyverno.io globalcontextentries.kyverno.io
  3. kyverno namespace 中包含以下 ServiceAccounts(默认 Kyverno 安装):

    • kyverno-admission-controller
    • kyverno-background-controller
    • kyverno-reports-controller
  4. 你拥有 cluster-admin(或等效)权限,因为安装引擎需要创建 CRD、ClusterRoles、ClusterRoleBindings、GlobalContextEntries 和 admission policies。

  5. 你已经审查了打算允许非 restricted Pod 的每个 namespace 上的 Pod Security Admission (PSA) enforce 标签。PSA 会在 Kyverno 之前运行;如果某个 namespace 标记为 pod-security.kubernetes.io/enforce: restricted,那么在 Kyverno 介入前,该 namespace 会拒绝任何与宽松 SCC(例如 anyuidhostnetwork-v2)匹配的 Pod。请在适当的 namespace 中将标签调整为 baselineprivileged,或者限制你在这些 namespace 中提供的 SCC 配置文件集合。

Tip

引擎安装是一次性工作,通常由平台管理员执行。第 2 部分同样属于管理员工作流:平台或安全管理员在审查工作负载需求后绑定 SCC 配置文件。应用团队通常只需提供这些需求,然后使用分配的 ServiceAccount。

步骤

工作分为两部分:

  • 第 1 部分 在集群范围内安装 SCC 引擎。每个集群执行一次。
  • 第 2 部分 通过将 SCC 配置文件绑定到 ServiceAccounts、Users 和 Groups 来授权工作负载在需要时固定到特定 SCC。

第 1 部分:安装 SCC 引擎

步骤 1.1 — 安装 SecurityContextConstraints CRD

将以下清单保存为 scc-crd.yaml。它定义了一个集群级别的 SecurityContextConstraints 资源(短名 scc),其字段与 OpenShift SCC 语义一致。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: securitycontextconstraints.security.alauda.io
spec:
  group: security.alauda.io
  names:
    plural: securitycontextconstraints
    singular: securitycontextconstraints
    kind: SecurityContextConstraints
    listKind: SecurityContextConstraintsList
    shortNames:
      - scc
  scope: Cluster
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          description: |
            SecurityContextConstraints governs the ability to make requests that affect
            container security context. This custom CRD mirrors OpenShift SCC semantics
            while keeping fields under spec for Kyverno CEL consumption.
          type: object
          required:
            - spec
          properties:
            apiVersion:
              type: string
            kind:
              type: string
            metadata:
              type: object
            spec:
              type: object
              required:
                - runAsUser
              properties:
                allowHostPorts:
                  description: Determines if the profile allows host ports in containers.
                  type: boolean
                priority:
                  description: Higher priority SCC is evaluated first.
                  type: integer
                  format: int32
                  nullable: true
                restrictiveScore:
                  description: Secondary sort key. Lower score means less restrictive.
                  type: integer
                  format: int32
                  minimum: 0
                requiredDropCapabilities:
                  description: Capabilities that must be dropped.
                  type: array
                  nullable: true
                  items:
                    type: string
                  x-kubernetes-list-type: atomic
                allowPrivilegedContainer:
                  description: Determines if privileged containers are allowed.
                  type: boolean
                runAsUser:
                  description: Strategy controlling runAsUser.
                  type: object
                  nullable: true
                  properties:
                    type:
                      description: Strategy type for runAsUser.
                      type: string
                      enum:
                        - RunAsAny
                        - MustRunAs
                        - MustRunAsRange
                        - MustRunAsNonRoot
                        - MustRunAsNonRootOrSystem
                    uid:
                      description: Required when type=MustRunAs.
                      type: integer
                      format: int64
                      minimum: 0
                    uidRangeMin:
                      description: Minimum uid for MustRunAsRange.
                      type: integer
                      format: int64
                      minimum: 0
                    uidRangeMax:
                      description: Maximum uid for MustRunAsRange.
                      type: integer
                      format: int64
                      minimum: 0
                users:
                  description: Users who can use this SCC.
                  type: array
                  nullable: true
                  items:
                    type: string
                  x-kubernetes-list-type: atomic
                groups:
                  description: Groups who can use this SCC.
                  type: array
                  nullable: true
                  items:
                    type: string
                  x-kubernetes-list-type: atomic
                allowHostDirVolumePlugin:
                  description: Determines if hostPath-like volume plugin usage is allowed.
                  type: boolean
                seccompProfiles:
                  description: Allowed seccomp profiles. '*' allows all.
                  type: array
                  nullable: true
                  items:
                    type: string
                    pattern: "^(\\*|runtime/default|unconfined|localhost/.+)$"
                  x-kubernetes-list-type: atomic
                allowHostIPC:
                  description: Determines if host IPC is allowed.
                  type: boolean
                forbiddenSysctls:
                  description: Explicitly forbidden sysctls.
                  type: array
                  nullable: true
                  items:
                    type: string
                  x-kubernetes-list-type: atomic
                seLinuxContext:
                  description: Strategy controlling SELinux labels.
                  type: object
                  nullable: true
                  properties:
                    type:
                      description: Strategy type for SELinux context.
                      type: string
                    seLinuxOptions:
                      description: Fixed SELinux options required by MustRunAs.
                      type: object
                      properties:
                        user:
                          type: string
                        role:
                          type: string
                        type:
                          type: string
                        level:
                          type: string
                readOnlyRootFilesystem:
                  description: Forces readOnlyRootFilesystem when set to true.
                  type: boolean
                fsGroup:
                  description: Strategy controlling fsGroup.
                  type: object
                  nullable: true
                  properties:
                    type:
                      type: string
                    ranges:
                      type: array
                      items:
                        type: object
                        properties:
                          min:
                            type: integer
                            format: int64
                          max:
                            type: integer
                            format: int64
                      x-kubernetes-list-type: atomic
                supplementalGroups:
                  description: Strategy controlling supplemental groups.
                  type: object
                  nullable: true
                  properties:
                    type:
                      type: string
                    ranges:
                      type: array
                      items:
                        type: object
                        properties:
                          min:
                            type: integer
                            format: int64
                          max:
                            type: integer
                            format: int64
                      x-kubernetes-list-type: atomic
                userNamespaceLevel:
                  description: Controls host user namespace usage.
                  type: string
                  default: AllowHostLevel
                  enum:
                    - AllowHostLevel
                    - RequirePodLevel
                defaultAddCapabilities:
                  description: Capabilities added by default unless explicitly dropped.
                  type: array
                  nullable: true
                  items:
                    type: string
                  x-kubernetes-list-type: atomic
                allowedUnsafeSysctls:
                  description: Explicitly allowed unsafe sysctls.
                  type: array
                  nullable: true
                  items:
                    type: string
                  x-kubernetes-list-type: atomic
                allowedFlexVolumes:
                  description: Allowed flex volume drivers.
                  type: array
                  nullable: true
                  items:
                    type: object
                    required:
                      - driver
                    properties:
                      driver:
                        type: string
                  x-kubernetes-list-type: atomic
                volumes:
                  description: Allowed volume plugin types. '*' allows all.
                  type: array
                  nullable: true
                  items:
                    type: string
                    enum:
                      - '*'
                      - none
                      - hostPath
                      - emptyDir
                      - gcePersistentDisk
                      - awsElasticBlockStore
                      - gitRepo
                      - secret
                      - nfs
                      - iscsi
                      - glusterfs
                      - persistentVolumeClaim
                      - rbd
                      - flexVolume
                      - cinder
                      - cephfs
                      - flocker
                      - downwardAPI
                      - fc
                      - azureFile
                      - configMap
                      - vsphereVolume
                      - quobyte
                      - azureDisk
                      - photonPersistentDisk
                      - projected
                      - portworxVolume
                      - scaleIO
                      - storageos
                      - csi
                      - ephemeral
                      - image
                  x-kubernetes-list-type: atomic
                allowHostPID:
                  description: Determines if host PID is allowed.
                  type: boolean
                allowHostNetwork:
                  description: Determines if hostNetwork is allowed.
                  type: boolean
                allowPrivilegeEscalation:
                  description: Determines if privilege escalation can be requested.
                  type: boolean
                  nullable: true
                defaultAllowPrivilegeEscalation:
                  description: Default for allowPrivilegeEscalation when container omits it.
                  type: boolean
                  nullable: true
                allowedCapabilities:
                  description: Capabilities that may be added.
                  type: array
                  nullable: true
                  items:
                    type: string
                  x-kubernetes-list-type: atomic
              x-kubernetes-validations:
                - rule: "!has(self.runAsUser) || self.runAsUser.type != 'MustRunAs' || has(self.runAsUser.uid)"
                  message: "runAsUser.uid is required when runAsUser.type is MustRunAs."
                - rule: "!has(self.runAsUser) || self.runAsUser.type == 'MustRunAs' || !has(self.runAsUser.uid)"
                  message: "runAsUser.uid is only allowed when runAsUser.type is MustRunAs."
                - rule: "!has(self.runAsUser) || self.runAsUser.type != 'MustRunAsRange' || (has(self.runAsUser.uidRangeMin) && has(self.runAsUser.uidRangeMax))"
                  message: "uidRangeMin and uidRangeMax are required when runAsUser.type is MustRunAsRange."
                - rule: "!has(self.runAsUser) || self.runAsUser.type == 'MustRunAsRange' || (!has(self.runAsUser.uidRangeMin) && !has(self.runAsUser.uidRangeMax))"
                  message: "uidRangeMin and uidRangeMax are only allowed when runAsUser.type is MustRunAsRange."
                - rule: "!has(self.runAsUser) || !has(self.runAsUser.uidRangeMin) || !has(self.runAsUser.uidRangeMax) || self.runAsUser.uidRangeMin <= self.runAsUser.uidRangeMax"
                  message: "uidRangeMin must be less than or equal to uidRangeMax."
      additionalPrinterColumns:
        - name: Priv
          type: string
          description: Determines if privileged containers are allowed
          jsonPath: .spec.allowPrivilegedContainer
        - name: Caps
          type: string
          description: Allowed capabilities
          jsonPath: .spec.allowedCapabilities
        - name: SELinux
          type: string
          description: SELinux strategy
          jsonPath: .spec.seLinuxContext.type
        - name: RunAsUser
          type: string
          description: RunAsUser strategy
          jsonPath: .spec.runAsUser.type
        - name: FSGroup
          type: string
          description: FSGroup strategy
          jsonPath: .spec.fsGroup.type
        - name: SupGroup
          type: string
          description: SupplementalGroups strategy
          jsonPath: .spec.supplementalGroups.type
        - name: Priority
          type: string
          description: SCC sort priority
          jsonPath: .spec.priority
        - name: Score
          type: string
          description: Secondary restrictive score
          jsonPath: .spec.restrictiveScore
        - name: ReadOnlyRootFS
          type: string
          description: Force read-only root filesystem
          jsonPath: .spec.readOnlyRootFilesystem
        - name: Volumes
          type: string
          description: Allowed volume plugins
          jsonPath: .spec.volumes
  conversion:
    strategy: None

应用它并等待 CRD 变为 Established 后再继续:

kubectl apply -f scc-crd.yaml
kubectl wait --for=condition=Established --timeout=120s \
  crd/securitycontextconstraints.security.alauda.io

步骤 1.2 — 安装 13 个内置 SCC 配置文件

将以下清单保存为 scc-profiles.yaml。它定义了 13 个基于 OpenShift 内置集合建模的 SCC 配置文件,按限制程度从高到低排序(restrictiveScore: 100restrictiveScore: 0)。当同一受试对象被授予多个 SCC 时,自动选择策略会优先选择更高的 restrictiveScore

Tip

你不必安装全部配置文件。可以将此清单裁剪为平台实际提供的子集——但你必须至少为每个受试对象保留一个可用配置文件,否则它们的 Pod 会在准入时被拒绝。

apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: restricted-v2
spec:
  priority: 0
  restrictiveScore: 100
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: false
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsRange
    uidRangeMin: 1
    uidRangeMax: 2147483647
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: MustRunAs
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities:
    - NET_BIND_SERVICE
  requiredDropCapabilities:
    - ALL
  defaultAddCapabilities: []
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  seccompProfiles:
    - runtime/default
  readOnlyRootFilesystem: false
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: restricted-v3
spec:
  priority: 0
  restrictiveScore: 100
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: false
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsRange
    uidRangeMin: 1000
    uidRangeMax: 65534
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: MustRunAs
    ranges:
      - min: 1000
        max: 65534
  supplementalGroups:
    type: MustRunAs
    ranges:
      - min: 1000
        max: 65534
  userNamespaceLevel: RequirePodLevel
  allowedCapabilities:
    - NET_BIND_SERVICE
  requiredDropCapabilities:
    - ALL
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  seccompProfiles:
    - runtime/default
  readOnlyRootFilesystem: false
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: restricted
spec:
  priority: 0
  restrictiveScore: 98
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsRange
    uidRangeMin: 1
    uidRangeMax: 2147483647
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: MustRunAs
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities: []
  requiredDropCapabilities:
    - KILL
    - MKNOD
    - SETUID
    - SETGID
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  readOnlyRootFilesystem: false
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: nonroot-v2
spec:
  priority: 0
  restrictiveScore: 95
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: false
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsNonRoot
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: RunAsAny
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities:
    - NET_BIND_SERVICE
  requiredDropCapabilities:
    - ALL
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  seccompProfiles:
    - runtime/default
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: nonroot
spec:
  priority: 0
  restrictiveScore: 92
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsNonRoot
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: RunAsAny
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities: []
  requiredDropCapabilities:
    - KILL
    - MKNOD
    - SETUID
    - SETGID
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  readOnlyRootFilesystem: false
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: hostnetwork-v2
spec:
  priority: 0
  restrictiveScore: 70
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: false
  allowHostNetwork: true
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: true
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsRange
    uidRangeMin: 1
    uidRangeMax: 2147483647
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: MustRunAs
  supplementalGroups:
    type: MustRunAs
  allowedCapabilities:
    - NET_BIND_SERVICE
  requiredDropCapabilities:
    - ALL
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  seccompProfiles:
    - runtime/default
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: hostnetwork
spec:
  priority: 0
  restrictiveScore: 68
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: true
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: true
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsRange
    uidRangeMin: 1
    uidRangeMax: 2147483647
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: MustRunAs
  supplementalGroups:
    type: MustRunAs
  allowedCapabilities: []
  requiredDropCapabilities:
    - KILL
    - MKNOD
    - SETUID
    - SETGID
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  readOnlyRootFilesystem: false
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: anyuid
spec:
  priority: 10
  restrictiveScore: 60
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: false
  runAsUser:
    type: RunAsAny
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: RunAsAny
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities: []
  requiredDropCapabilities:
    - MKNOD
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: nested-container
spec:
  priority: 0
  restrictiveScore: 58
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: false
  runAsUser:
    type: MustRunAsRange
    uidRangeMin: 0
    uidRangeMax: 65534
  seLinuxContext:
    type: MustRunAs
    seLinuxOptions:
      type: container_engine_t
  fsGroup:
    type: MustRunAs
    ranges:
      - min: 0
        max: 65534
  supplementalGroups:
    type: MustRunAs
    ranges:
      - min: 0
        max: 65534
  userNamespaceLevel: RequirePodLevel
  allowedCapabilities:
    - SETUID
    - SETGID
  requiredDropCapabilities: []
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - image
    - persistentVolumeClaim
    - projected
    - secret
  seccompProfiles:
    - '*'
  readOnlyRootFilesystem: false
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: hostmount-anyuid
spec:
  priority: 0
  restrictiveScore: 55
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: true
  runAsUser:
    type: RunAsAny
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: RunAsAny
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities: []
  requiredDropCapabilities:
    - MKNOD
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - hostPath
    - image
    - nfs
    - persistentVolumeClaim
    - projected
    - secret
  readOnlyRootFilesystem: false
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: hostmount-anyuid-v2
spec:
  priority: 0
  restrictiveScore: 50
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: false
  allowHostPID: false
  allowHostIPC: false
  allowHostPorts: false
  allowHostDirVolumePlugin: true
  runAsUser:
    type: RunAsAny
  seLinuxContext:
    type: RunAsAny
  fsGroup:
    type: RunAsAny
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities: []
  requiredDropCapabilities:
    - MKNOD
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - hostPath
    - image
    - nfs
    - persistentVolumeClaim
    - projected
    - secret
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: hostaccess
spec:
  priority: 0
  restrictiveScore: 40
  allowPrivilegedContainer: false
  allowPrivilegeEscalation: true
  allowHostNetwork: true
  allowHostPID: true
  allowHostIPC: true
  allowHostPorts: true
  allowHostDirVolumePlugin: true
  runAsUser:
    type: MustRunAsRange
    uidRangeMin: 1
    uidRangeMax: 2147483647
  seLinuxContext:
    type: MustRunAs
  fsGroup:
    type: MustRunAs
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities: []
  requiredDropCapabilities:
    - KILL
    - MKNOD
    - SETUID
    - SETGID
  volumes:
    - configMap
    - csi
    - downwardAPI
    - emptyDir
    - ephemeral
    - hostPath
    - image
    - persistentVolumeClaim
    - projected
    - secret
---
apiVersion: security.alauda.io/v1alpha1
kind: SecurityContextConstraints
metadata:
  name: privileged
spec:
  priority: 0
  restrictiveScore: 0
  allowPrivilegedContainer: true
  allowPrivilegeEscalation: true
  allowHostNetwork: true
  allowHostPID: true
  allowHostIPC: true
  allowHostPorts: true
  allowHostDirVolumePlugin: true
  runAsUser:
    type: RunAsAny
  seLinuxContext:
    type: RunAsAny
  fsGroup:
    type: RunAsAny
  supplementalGroups:
    type: RunAsAny
  allowedCapabilities:
    - '*'
  requiredDropCapabilities: []
  volumes:
    - '*'
  seccompProfiles:
    - '*'
  allowedUnsafeSysctls:
    - '*'

应用这些配置文件:

kubectl apply -f scc-profiles.yaml
kubectl get scc

你应该会看到全部 13 个配置文件列出,并且其 PriorityScore 列已填充(以及其他 SCC 列,如 PrivRunAsUserVolumes)。

步骤 1.3 — 安装 GlobalContextEntries、Kyverno 读取 RBAC,以及 admission policies

此步骤一次性安装三项内容:

  1. GlobalContextEntries (GCE) —— 五个内存缓存,Kyverno 在准入过程中使用它们查找 SCC 配置文件、ClusterRoles、ClusterRoleBindings、RoleBindings 和 Roles,而无需为每次请求发起 API 调用。
  2. Reader RBAC —— 一个 ClusterRole,授予 Kyverno 的三个服务账户对 SCC CRD、上述四类 RBAC 资源,以及策略所匹配的 Pod / pods/ephemeralcontainers 资源的只读权限。
  3. 两个 admission policies —— 一个 MutatingPolicy,用于用所选 SCC 的默认值填充字段;以及一个 ValidatingPolicy,用于拒绝任何没有可接受 SCC 的 Pod。
Warning

这两个策略包含驱动 SCC 选择与验证的 CEL 逻辑。你不需要阅读或理解 CEL 也能使用该引擎——按原样应用这些清单即可。之所以表达式很长,是因为它们逐字段复现了 OpenShift SCC 的准入算法。

将以下内容保存为 scc-gce.yaml 并应用:

apiVersion: kyverno.io/v2alpha1
kind: GlobalContextEntry
metadata:
  name: scc-profiles
spec:
  kubernetesResource:
    group: security.alauda.io
    version: v1alpha1
    resource: securitycontextconstraints
  projections:
    - name: items
      jmesPath: "@"
---
apiVersion: kyverno.io/v2alpha1
kind: GlobalContextEntry
metadata:
  name: scc-clusterroles
spec:
  kubernetesResource:
    group: rbac.authorization.k8s.io
    version: v1
    resource: clusterroles
  projections:
    - name: items
      jmesPath: "@"
---
apiVersion: kyverno.io/v2alpha1
kind: GlobalContextEntry
metadata:
  name: scc-clusterrolebindings
spec:
  kubernetesResource:
    group: rbac.authorization.k8s.io
    version: v1
    resource: clusterrolebindings
  projections:
    - name: items
      jmesPath: "@"
---
apiVersion: kyverno.io/v2alpha1
kind: GlobalContextEntry
metadata:
  name: scc-rolebindings
spec:
  kubernetesResource:
    group: rbac.authorization.k8s.io
    version: v1
    resource: rolebindings
  projections:
    - name: items
      jmesPath: "@"
---
apiVersion: kyverno.io/v2alpha1
kind: GlobalContextEntry
metadata:
  name: scc-roles
spec:
  kubernetesResource:
    group: rbac.authorization.k8s.io
    version: v1
    resource: roles
  projections:
    - name: items
      jmesPath: "@"

将以下内容保存为 scc-reader-rbac.yaml 并应用。由于 Kyverno 在策略就绪门控(RBACPermissionsGranted)期间会检查每个匹配资源的读取权限,因此必须包含 podspods/ephemeralcontainers 的读取权限;否则变更型策略会保持 NotReady

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno-scc-reader
rules:
  - apiGroups:
      - security.alauda.io
    resources:
      - securitycontextconstraints
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - rbac.authorization.k8s.io
    resources:
      - clusterroles
      - clusterrolebindings
      - rolebindings
      - roles
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - pods
      - pods/ephemeralcontainers
    verbs:
      - get
      - list
      - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kyverno-scc-reader
subjects:
  - kind: ServiceAccount
    name: kyverno-admission-controller
    namespace: kyverno
  - kind: ServiceAccount
    name: kyverno-background-controller
    namespace: kyverno
  - kind: ServiceAccount
    name: kyverno-reports-controller
    namespace: kyverno
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kyverno-scc-reader

将以下内容保存为 scc-auto-pick.yaml。这是一个 ValidatingPolicy,用于拒绝任何没有可接受 SCC 的 Pod。

Warning

下面的示例配置为 validationActions: [Deny]。在现有集群上,第一次应用前请将其改为 validationActions: [Warn],然后在审查警告并创建所需 SCC 绑定后,再改回 Deny。有关滚动发布流程,请参见步骤 1.4。

apiVersion: policies.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
  name: scc-auto-pick
  labels:
    reports.kyverno.io/disabled: "true"
  annotations:
    policies.kyverno.io/title: SCC Auto-Pick (CEL, CRD + RBAC)
    pod-policies.kyverno.io/autogen-controllers: "none"
spec:
  autogen:
    podControllers:
      controllers: []
    validatingAdmissionPolicy:
      enabled: false
  evaluation:
    admission:
      enabled: true
    background:
      enabled: false
  failurePolicy: Fail
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  matchConditions:
    - name: skip-system-ns
      expression: |
        !(request.namespace.startsWith('kube-') ||
          request.namespace.startsWith('cpaas-') ||
          request.namespace.startsWith('alauda-') ||
          request.namespace == 'kyverno' ||
          request.namespace == 'cattle-system' ||
          request.namespace == 'operators' ||
          request.namespace == 'default')

  variables:
    - name: containers
      expression: |
        object.spec.containers + object.spec.?initContainers.orValue([]) +
        object.spec.?ephemeralContainers.orValue([])

    - name: required
      expression: object.metadata.?annotations[?'alauda.io/required-scc'].orValue('')

    - name: profiles
      expression: |
        cel.bind(items, globalContext.Get('scc-profiles', 'items'),
          items == null ? [] : items)

    - name: subjectMatches
      expression: |
        [
          {'kind':'ServiceAccount',
           'name': string(object.spec.?serviceAccountName.orValue('default')),
           'namespace': string(request.namespace)},
          {'kind':'Group', 'name':'system:serviceaccounts'},
          {'kind':'Group', 'name':'system:serviceaccounts:'+request.namespace},
          {'kind':'Group', 'name':'system:authenticated'},
          {'kind':'User',  'name': request.userInfo.username}
        ]
        + request.userInfo.groups.map(g, {'kind':'Group','name': g})

    - name: rolebindings
      expression: |
        cel.bind(rbs, globalContext.Get('scc-rolebindings','items'),
          rbs == null ? [] : rbs)

    - name: matchedClusterRoleRefsFromCRB
      expression: |
        cel.bind(crbs, globalContext.Get('scc-clusterrolebindings','items'),
          crbs == null ? [] : crbs)
        .filter(b, b.?roleRef.?kind.orValue('') == 'ClusterRole'
                && b.?subjects.orValue([]).exists(s,
            variables.subjectMatches.exists(m,
              s.kind == m.kind && s.name == m.name &&
              (s.kind != 'ServiceAccount' ||
               s.?namespace.orValue('') == m.?namespace.orValue('')))))
        .map(b, b.roleRef.name)

    - name: matchedClusterRoleRefsFromRB
      expression: |
        variables.rolebindings
          .filter(b, b.?metadata.?namespace.orValue('') == request.namespace
                  && b.?roleRef.?kind.orValue('') == 'ClusterRole'
                  && b.?subjects.orValue([]).exists(s,
              variables.subjectMatches.exists(m,
                s.kind == m.kind && s.name == m.name &&
                (s.kind != 'ServiceAccount' ||
                 s.?namespace.orValue('') == m.?namespace.orValue('')))))
          .map(b, b.roleRef.name)

    - name: matchedRoleRefsFromRB
      expression: |
        variables.rolebindings
          .filter(b, b.?metadata.?namespace.orValue('') == request.namespace
                  && b.?roleRef.?kind.orValue('') == 'Role'
                  && b.?subjects.orValue([]).exists(s,
              variables.subjectMatches.exists(m,
                s.kind == m.kind && s.name == m.name &&
                (s.kind != 'ServiceAccount' ||
                 s.?namespace.orValue('') == m.?namespace.orValue('')))))
          .map(b, b.roleRef.name)

    - name: matchedClusterRoleRefs
      expression: |
        variables.matchedClusterRoleRefsFromCRB + variables.matchedClusterRoleRefsFromRB

    - name: allSccNames
      expression: |
        variables.profiles.map(p, p.metadata.name)

    - name: assignedFromClusterRoles
      expression: |
        cel.bind(crs, globalContext.Get('scc-clusterroles','items'),
          crs == null ? [] : crs)
          .filter(r, variables.matchedClusterRoleRefs.exists(n, n == r.metadata.name))
          .map(r, r.?rules.orValue([])
            .filter(ru,
              ru.?apiGroups.orValue([]).exists(g, g == 'security.alauda.io' || g == '*') &&
              ru.?resources.orValue([]).exists(x, x == 'securitycontextconstraints' || x == '*') &&
              ru.?verbs.orValue([]).exists(v, v == 'use' || v == '*'))
            .map(ru,
              ru.?resourceNames.orValue([]).size() == 0
                ? variables.allSccNames
                : ru.resourceNames)
          )
          .flatten()
          .flatten()

    - name: assignedFromRoles
      expression: |
        cel.bind(roles, globalContext.Get('scc-roles','items'),
          roles == null ? [] : roles)
          .filter(r,
            r.?metadata.?namespace.orValue('') == request.namespace
            && variables.matchedRoleRefsFromRB.exists(n, n == r.metadata.name))
          .map(r, r.?rules.orValue([])
            .filter(ru,
              ru.?apiGroups.orValue([]).exists(g, g == 'security.alauda.io' || g == '*') &&
              ru.?resources.orValue([]).exists(x, x == 'securitycontextconstraints' || x == '*') &&
              ru.?verbs.orValue([]).exists(v, v == 'use' || v == '*'))
            .map(ru,
              ru.?resourceNames.orValue([]).size() == 0
                ? variables.allSccNames
                : ru.resourceNames)
          )
          .flatten()
          .flatten()

    - name: assigned
      expression: |
        (variables.assignedFromClusterRoles + variables.assignedFromRoles)
          .filter(n, variables.allSccNames.exists(s, s == n))

    - name: safeSysctls
      expression: |
        ['kernel.shm_rmid_forced',
         'net.ipv4.ip_local_port_range',
         'net.ipv4.ip_unprivileged_port_start',
         'net.ipv4.tcp_syncookies',
         'net.ipv4.ping_group_range']

    - name: vtypes
      expression: |
        ['hostPath','emptyDir','gcePersistentDisk','awsElasticBlockStore','gitRepo',
         'secret','nfs','iscsi','glusterfs','persistentVolumeClaim','rbd','flexVolume',
         'cinder','cephfs','flocker','downwardAPI','fc','azureFile','configMap',
         'vsphereVolume','quobyte','azureDisk','photonPersistentDisk','projected',
         'portworxVolume','scaleIO','storageos','csi','ephemeral','image']

    - name: ordered
      expression: |
        variables.assigned.sortBy(n,
          int(variables.profiles.filter(pr, pr.metadata.name == n)[?0].orValue({}).?spec.?priority.orValue(0)) * -100000 +
          -int(variables.profiles.filter(pr, pr.metadata.name == n)[?0].orValue({}).?spec.?restrictiveScore.orValue(100))
        )
    - name: requiredExists
      expression: variables.required == '' || variables.profiles.exists(pr, pr.metadata.name == variables.required)
    - name: requiredBound
      expression: variables.required == '' || variables.assigned.exists(n, n == variables.required)
    - name: candidateNames
      expression: |
        variables.required != ''
          ? [variables.required]
          : variables.ordered

    - name: matched
      expression: |
        variables.candidateNames.exists(n,
          cel.bind(p, variables.profiles.filter(pr, pr.metadata.name == n)[?0].orValue({}).?spec.orValue({}),
             (p.?allowPrivilegedContainer.orValue(false)
               || !variables.containers.exists(c, c.?securityContext.?privileged.orValue(false)))
          && (p.?allowPrivilegeEscalation.orValue(true)
               || !variables.containers.exists(c, c.?securityContext.?allowPrivilegeEscalation.orValue(true)))
          && (p.?allowHostNetwork.orValue(false) || !object.spec.?hostNetwork.orValue(false))
          && (p.?allowHostPID.orValue(false)     || !object.spec.?hostPID.orValue(false))
          && (p.?allowHostIPC.orValue(false)     || !object.spec.?hostIPC.orValue(false))
          && (p.?allowHostDirVolumePlugin.orValue(false)
               || !object.spec.?volumes.orValue([]).exists(v, has(v.hostPath)))
          && (
               p.?runAsUser.?type.orValue('RunAsAny') == 'RunAsAny'
               || (
                    (p.?runAsUser.?type.orValue('RunAsAny') in ['MustRunAsNonRoot','MustRunAsNonRootOrSystem'])
                    && !variables.containers.exists(c, c.?securityContext.?runAsUser.orValue(
                         object.spec.?securityContext.?runAsUser.orValue(1)) == 0)
                  )
               || (
                    p.?runAsUser.?type.orValue('RunAsAny') == 'MustRunAs'
                    && variables.containers.all(c, c.?securityContext.?runAsUser.orValue(
                         object.spec.?securityContext.?runAsUser.orValue(1))
                         == p.?runAsUser.?uid.orValue(-1))
                  )
               || (
                    p.?runAsUser.?type.orValue('RunAsAny') == 'MustRunAsRange'
                    && variables.containers.all(c,
                         c.?securityContext.?runAsUser.orValue(
                           object.spec.?securityContext.?runAsUser.orValue(1))
                           >= p.?runAsUser.?uidRangeMin.orValue(1)
                         && c.?securityContext.?runAsUser.orValue(
                              object.spec.?securityContext.?runAsUser.orValue(1))
                           <= p.?runAsUser.?uidRangeMax.orValue(2147483647))
                  )
             )
          && (p.?allowedCapabilities.orValue([]).exists(t, t == '*')
               || variables.containers.all(c,
                    c.?securityContext.?capabilities.?add.orValue([]).all(cap,
                      p.?allowedCapabilities.orValue([]).exists(a, a == cap))))
          && (p.?requiredDropCapabilities.orValue([]).size() == 0
               || variables.containers.all(c,
                    p.?requiredDropCapabilities.orValue([]).all(req,
                      c.?securityContext.?capabilities.?drop.orValue([]).exists(d, d == req || d == 'ALL'))))
          && (p.?volumes.orValue(['*']).exists(t, t == '*')
               || object.spec.?volumes.orValue([]).all(v,
                    variables.vtypes.filter(t, v[?t].hasValue()).all(t,
                      p.?volumes.orValue([]).exists(a, a == t))))
          && (p.?allowHostPorts.orValue(false)
               || variables.containers.all(c,
                    c.?ports.orValue([]).all(port, port.?hostPort.orValue(0) == 0)))
          && (p.?allowedUnsafeSysctls.orValue([]).exists(t, t == '*')
               || object.spec.?securityContext.?sysctls.orValue([]).all(s,
                    variables.safeSysctls.exists(safe, safe == s.name)
                    || p.?allowedUnsafeSysctls.orValue([]).exists(a, a == s.name)))
          && (!p.?readOnlyRootFilesystem.orValue(false)
               || variables.containers.all(c, c.?securityContext.?readOnlyRootFilesystem.orValue(false) == true))
          && (p.?seccompProfiles.orValue([]).size() == 0
               || p.?seccompProfiles.orValue([]).exists(t, t == '*')
               || variables.containers.all(c,
                    p.?seccompProfiles.orValue([]).exists(a,
                      (c.?securityContext.?seccompProfile.?type.orValue(
                         object.spec.?securityContext.?seccompProfile.?type.orValue('')) == 'RuntimeDefault'
                         && a == 'runtime/default')
                      || (c.?securityContext.?seccompProfile.?type.orValue(
                         object.spec.?securityContext.?seccompProfile.?type.orValue('')) == 'Unconfined'
                         && a == 'unconfined')
                      || (c.?securityContext.?seccompProfile.?type.orValue(
                         object.spec.?securityContext.?seccompProfile.?type.orValue('')) == 'Localhost'
                         && a == 'localhost/' + c.?securityContext.?seccompProfile.?localhostProfile.orValue(
                              object.spec.?securityContext.?seccompProfile.?localhostProfile.orValue(''))))))
          && (p.?allowedFlexVolumes.orValue([]).size() == 0
               || object.spec.?volumes.orValue([]).filter(v, v.?flexVolume.hasValue()).all(v,
                    p.?allowedFlexVolumes.orValue([]).exists(d, d.?driver.orValue('') == v.flexVolume.driver)))
          )
        )

  validations:
    - expression: variables.requiredExists
      message: "required-scc does not exist"
      messageExpression: |
        "required SCC '" + variables.required + "' not found in scc-profiles"
    - expression: variables.requiredBound
      message: "required-scc is not bound to ServiceAccount"
      messageExpression: |
        "required SCC '" + variables.required +
        "' is not bound to ServiceAccount '" +
        object.spec.?serviceAccountName.orValue('default') +
        "' in namespace '" + request.namespace + "'"
    - expression: variables.matched
      message: "Pod violates all SCCs assigned to its ServiceAccount"
      messageExpression: |
        variables.required != ''
        ? ("Pod " + object.metadata.name +
           " does not satisfy required SCC '" + variables.required + "'")
        : ("Pod " + object.metadata.name +
           " does not satisfy any SCC profile assigned to ServiceAccount '" +
           object.spec.?serviceAccountName.orValue('default') +
           "' in namespace '" + request.namespace +
           "' (candidates: " + variables.ordered.join(",") + ")")

将以下内容保存为 scc-fill-defaults.yaml 并应用。这是一个 MutatingPolicy,它会在 Pod 上记录所选 SCC(alauda.io/scc 注解),并填充从该 SCC 继承而来的 runAsUserseccompProfileallowPrivilegeEscalation 默认值。

apiVersion: policies.kyverno.io/v1alpha1
kind: MutatingPolicy
metadata:
  name: scc-fill-defaults
  labels:
    reports.kyverno.io/disabled: "true"
  annotations:
    policies.kyverno.io/title: SCC default value filler (CRD + RBAC, explicit-wins)
    pod-policies.kyverno.io/autogen-controllers: "none"
spec:
  autogen:
    podControllers:
      controllers: []
  evaluation:
    admission:
      enabled: true
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["UPDATE"]
        resources: ["pods/ephemeralcontainers"]
  matchConditions:
    - name: skip-system-ns
      expression: |
        !(request.namespace.startsWith('kube-') ||
          request.namespace.startsWith('cpaas-') ||
          request.namespace.startsWith('alauda-') ||
          request.namespace == 'kyverno' ||
          request.namespace == 'cattle-system' ||
          request.namespace == 'operators' ||
          request.namespace == 'default')

  variables:
    - name: containers
      expression: |
        object.spec.containers + object.spec.?initContainers.orValue([]) +
        object.spec.?ephemeralContainers.orValue([])
    - name: required
      expression: object.metadata.?annotations[?'alauda.io/required-scc'].orValue('')

    - name: profiles
      expression: |
        cel.bind(items, globalContext.Get('scc-profiles', 'items'),
          items == null ? [] : items)

    - name: subjectMatches
      expression: |
        [
          {'kind':'ServiceAccount',
           'name': string(object.spec.?serviceAccountName.orValue('default')),
           'namespace': string(object.metadata.namespace)},
          {'kind':'Group', 'name':'system:serviceaccounts'},
          {'kind':'Group', 'name':'system:serviceaccounts:'+object.metadata.namespace},
          {'kind':'Group', 'name':'system:authenticated'},
          {'kind':'User',  'name': request.userInfo.username}
        ]
        + request.userInfo.groups.map(g, {'kind':'Group','name': g})
    - name: rolebindings
      expression: |
        cel.bind(rbs, globalContext.Get('scc-rolebindings','items'),
          rbs == null ? [] : rbs)
    - name: matchedClusterRoleRefsFromCRB
      expression: |
        cel.bind(crbs, globalContext.Get('scc-clusterrolebindings','items'),
          crbs == null ? [] : crbs)
        .filter(b, b.?roleRef.?kind.orValue('') == 'ClusterRole'
                && b.?subjects.orValue([]).exists(s,
            variables.subjectMatches.exists(m,
              s.kind == m.kind && s.name == m.name &&
              (s.kind != 'ServiceAccount' ||
               s.?namespace.orValue('') == m.?namespace.orValue('')))))
        .map(b, b.roleRef.name)
    - name: matchedClusterRoleRefsFromRB
      expression: |
        variables.rolebindings
          .filter(b, b.?metadata.?namespace.orValue('') == object.metadata.namespace
                  && b.?roleRef.?kind.orValue('') == 'ClusterRole'
                  && b.?subjects.orValue([]).exists(s,
              variables.subjectMatches.exists(m,
                s.kind == m.kind && s.name == m.name &&
                (s.kind != 'ServiceAccount' ||
                 s.?namespace.orValue('') == m.?namespace.orValue('')))))
          .map(b, b.roleRef.name)
    - name: matchedRoleRefsFromRB
      expression: |
        variables.rolebindings
          .filter(b, b.?metadata.?namespace.orValue('') == object.metadata.namespace
                  && b.?roleRef.?kind.orValue('') == 'Role'
                  && b.?subjects.orValue([]).exists(s,
              variables.subjectMatches.exists(m,
                s.kind == m.kind && s.name == m.name &&
                (s.kind != 'ServiceAccount' ||
                 s.?namespace.orValue('') == m.?namespace.orValue('')))))
          .map(b, b.roleRef.name)
    - name: matchedClusterRoleRefs
      expression: |
        variables.matchedClusterRoleRefsFromCRB + variables.matchedClusterRoleRefsFromRB
    - name: allSccNames
      expression: |
        variables.profiles.map(p, p.metadata.name)
    - name: assignedFromClusterRoles
      expression: |
        cel.bind(crs, globalContext.Get('scc-clusterroles','items'),
          crs == null ? [] : crs)
          .filter(r, variables.matchedClusterRoleRefs.exists(n, n == r.metadata.name))
          .map(r, r.?rules.orValue([])
            .filter(ru,
              ru.?apiGroups.orValue([]).exists(g, g == 'security.alauda.io' || g == '*') &&
              ru.?resources.orValue([]).exists(x, x == 'securitycontextconstraints' || x == '*') &&
              ru.?verbs.orValue([]).exists(v, v == 'use' || v == '*'))
            .map(ru,
              ru.?resourceNames.orValue([]).size() == 0
                ? variables.allSccNames
                : ru.resourceNames)
          )
          .flatten()
          .flatten()

    - name: assignedFromRoles
      expression: |
        cel.bind(roles, globalContext.Get('scc-roles','items'),
          roles == null ? [] : roles)
          .filter(r,
            r.?metadata.?namespace.orValue('') == object.metadata.namespace
            && variables.matchedRoleRefsFromRB.exists(n, n == r.metadata.name))
          .map(r, r.?rules.orValue([])
            .filter(ru,
              ru.?apiGroups.orValue([]).exists(g, g == 'security.alauda.io' || g == '*') &&
              ru.?resources.orValue([]).exists(x, x == 'securitycontextconstraints' || x == '*') &&
              ru.?verbs.orValue([]).exists(v, v == 'use' || v == '*'))
            .map(ru,
              ru.?resourceNames.orValue([]).size() == 0
                ? variables.allSccNames
                : ru.resourceNames)
          )
          .flatten()
          .flatten()

    - name: assigned
      expression: |
        (variables.assignedFromClusterRoles + variables.assignedFromRoles)
          .filter(n, variables.allSccNames.exists(s, s == n))

    - name: safeSysctls
      expression: |
        ['kernel.shm_rmid_forced',
         'net.ipv4.ip_local_port_range',
         'net.ipv4.ip_unprivileged_port_start',
         'net.ipv4.tcp_syncookies',
         'net.ipv4.ping_group_range']
    - name: vtypes
      expression: |
        ['hostPath','emptyDir','gcePersistentDisk','awsElasticBlockStore','gitRepo',
         'secret','nfs','iscsi','glusterfs','persistentVolumeClaim','rbd','flexVolume',
         'cinder','cephfs','flocker','downwardAPI','fc','azureFile','configMap',
         'vsphereVolume','quobyte','azureDisk','photonPersistentDisk','projected',
         'portworxVolume','scaleIO','storageos','csi','ephemeral','image']

    - name: ordered
      expression: |
        variables.assigned.sortBy(n,
          int(variables.profiles.filter(pr, pr.metadata.name == n)[?0].orValue({}).?spec.?priority.orValue(0)) * -100000 +
          -int(variables.profiles.filter(pr, pr.metadata.name == n)[?0].orValue({}).?spec.?restrictiveScore.orValue(100))
        )
    - name: requiredExists
      expression: variables.required == '' || variables.profiles.exists(pr, pr.metadata.name == variables.required)
    - name: requiredBound
      expression: variables.required == '' || variables.assigned.exists(n, n == variables.required)
    - name: candidateNames
      expression: |
        variables.required != ''
          ? ((variables.requiredExists && variables.requiredBound) ? [variables.required] : [])
          : variables.ordered
    - name: isEphemeralSubresource
      expression: request.operation == 'UPDATE'
    - name: annotatedSelectedName
      expression: object.metadata.?annotations[?'alauda.io/scc'].orValue('')

    - name: matchedNames
      expression: |
        variables.candidateNames.filter(n,
          cel.bind(p, variables.profiles.filter(pr, pr.metadata.name == n)[?0].orValue({}).?spec.orValue({}),
            cel.bind(defaultPE, p.?defaultAllowPrivilegeEscalation.orValue(
                  p.?allowPrivilegeEscalation.orValue(true)),
              cel.bind(podRunAsUserForFill,
                    object.spec.?securityContext.?runAsUser.orValue(
                      (p.?runAsUser.?type.orValue('') == 'MustRunAs' && p.?runAsUser.?uid.hasValue())
                        ? p.?runAsUser.?uid.orValue(1)
                        : 1),
                cel.bind(seccompFirstForFill,
                      p.?seccompProfiles.orValue([]).filter(s, s != '' && s != '*')[?0].orValue(''),
                  cel.bind(fillSeccompType,
                        seccompFirstForFill == 'runtime/default' ? 'RuntimeDefault' :
                        seccompFirstForFill.startsWith('localhost/') ? 'Localhost' : '',
                    cel.bind(fillSeccompLocalhost,
                          fillSeccompType == 'Localhost'
                            ? seccompFirstForFill.substring('localhost/'.size()) : '',
                      cel.bind(needPodSeccompFillForMatch,
                            !object.spec.?securityContext.?seccompProfile.hasValue() &&
                            object.spec.containers.all(c, !c.?securityContext.?seccompProfile.hasValue()) &&
                            object.spec.?initContainers.orValue([]).all(c, !c.?securityContext.?seccompProfile.hasValue()),
                        (p.?allowPrivilegedContainer.orValue(false)
                          || !variables.containers.exists(c, c.?securityContext.?privileged.orValue(false)))
                        && (p.?allowPrivilegeEscalation.orValue(true)
                            || !variables.containers.exists(c, c.?securityContext.?allowPrivilegeEscalation.orValue(defaultPE)))
                        && (p.?allowHostNetwork.orValue(false) || !object.spec.?hostNetwork.orValue(false))
                        && (p.?allowHostPID.orValue(false)     || !object.spec.?hostPID.orValue(false))
                        && (p.?allowHostIPC.orValue(false)     || !object.spec.?hostIPC.orValue(false))
                        && (p.?allowHostDirVolumePlugin.orValue(false)
                            || !object.spec.?volumes.orValue([]).exists(v, has(v.hostPath)))
                        && (
                            p.?runAsUser.?type.orValue('RunAsAny') == 'RunAsAny'
                            || (
                                (p.?runAsUser.?type.orValue('RunAsAny') in ['MustRunAsNonRoot','MustRunAsNonRootOrSystem'])
                                && !variables.containers.exists(c, c.?securityContext.?runAsUser.orValue(
                                      podRunAsUserForFill) == 0)
                              )
                            || (
                                p.?runAsUser.?type.orValue('RunAsAny') == 'MustRunAs'
                                && variables.containers.all(c, c.?securityContext.?runAsUser.orValue(
                                      podRunAsUserForFill) == p.?runAsUser.?uid.orValue(-1))
                              )
                            || (
                                p.?runAsUser.?type.orValue('RunAsAny') == 'MustRunAsRange'
                                && variables.containers.all(c,
                                    c.?securityContext.?runAsUser.orValue(podRunAsUserForFill)
                                      >= p.?runAsUser.?uidRangeMin.orValue(1)
                                    && c.?securityContext.?runAsUser.orValue(podRunAsUserForFill)
                                      <= p.?runAsUser.?uidRangeMax.orValue(2147483647))
                              )
                          )
                        && (p.?allowedCapabilities.orValue([]).exists(t, t == '*')
                            || variables.containers.all(c,
                                c.?securityContext.?capabilities.?add.orValue([]).all(cap,
                                  p.?allowedCapabilities.orValue([]).exists(a, a == cap))))
                        && (p.?requiredDropCapabilities.orValue([]).size() == 0
                            || variables.containers.all(c,
                                p.?requiredDropCapabilities.orValue([]).all(req,
                                  c.?securityContext.?capabilities.?drop.orValue([]).exists(d, d == req || d == 'ALL'))))
                        && (p.?volumes.orValue(['*']).exists(t, t == '*')
                            || object.spec.?volumes.orValue([]).all(v,
                                variables.vtypes.filter(t, v[?t].hasValue()).all(t,
                                  p.?volumes.orValue([]).exists(a, a == t))))
                        && (p.?allowHostPorts.orValue(false)
                            || variables.containers.all(c,
                                c.?ports.orValue([]).all(port, port.?hostPort.orValue(0) == 0)))
                        && (p.?allowedUnsafeSysctls.orValue([]).exists(t, t == '*')
                            || object.spec.?securityContext.?sysctls.orValue([]).all(s,
                                variables.safeSysctls.exists(safe, safe == s.name)
                                || p.?allowedUnsafeSysctls.orValue([]).exists(a, a == s.name)))
                        && (!p.?readOnlyRootFilesystem.orValue(false)
                            || variables.containers.all(c, c.?securityContext.?readOnlyRootFilesystem.orValue(false) == true))
                        && (p.?seccompProfiles.orValue([]).size() == 0
                            || p.?seccompProfiles.orValue([]).exists(t, t == '*')
                            || variables.containers.all(c,
                                p.?seccompProfiles.orValue([]).exists(a,
                                  (c.?securityContext.?seccompProfile.?type.orValue(
                                      object.spec.?securityContext.?seccompProfile.?type.orValue(
                                        (needPodSeccompFillForMatch && fillSeccompType != '') ? fillSeccompType : '')) == 'RuntimeDefault'
                                    && a == 'runtime/default')
                                  || (c.?securityContext.?seccompProfile.?type.orValue(
                                      object.spec.?securityContext.?seccompProfile.?type.orValue(
                                        (needPodSeccompFillForMatch && fillSeccompType != '') ? fillSeccompType : '')) == 'Unconfined'
                                    && a == 'unconfined')
                                  || (c.?securityContext.?seccompProfile.?type.orValue(
                                      object.spec.?securityContext.?seccompProfile.?type.orValue(
                                        (needPodSeccompFillForMatch && fillSeccompType != '') ? fillSeccompType : '')) == 'Localhost'
                                    && a == 'localhost/' + c.?securityContext.?seccompProfile.?localhostProfile.orValue(
                                        object.spec.?securityContext.?seccompProfile.?localhostProfile.orValue(
                                          (needPodSeccompFillForMatch && fillSeccompType == 'Localhost')
                                            ? fillSeccompLocalhost : ''))))))
                        && (p.?allowedFlexVolumes.orValue([]).size() == 0
                            || object.spec.?volumes.orValue([]).filter(v, v.?flexVolume.hasValue()).all(v,
                                p.?allowedFlexVolumes.orValue([]).exists(d, d.?driver.orValue('') == v.flexVolume.driver)))
                      )
                    )
                  )
                )
              )
            )
          )
        )

    - name: selectedName
      expression: |
        variables.isEphemeralSubresource
          && variables.annotatedSelectedName != ''
          && variables.candidateNames.exists(n, n == variables.annotatedSelectedName)
          ? variables.annotatedSelectedName
          : variables.matchedNames[?0].orValue('')

    - name: selectedSpec
      expression: |
        variables.profiles.filter(pr, pr.metadata.name == variables.selectedName)[?0].orValue({}).?spec.orValue({})

    - name: defaultPE
      expression: |
        variables.selectedSpec.?defaultAllowPrivilegeEscalation.orValue(
          variables.selectedSpec.?allowPrivilegeEscalation.orValue(true))

    - name: seccompFirst
      expression: |
        variables.selectedSpec.?seccompProfiles.orValue([])
          .filter(s, s != '' && s != '*')[?0].orValue('')
    - name: defaultSeccompType
      expression: |
        variables.seccompFirst == 'runtime/default' ? 'RuntimeDefault' :
        variables.seccompFirst.startsWith('localhost/') ? 'Localhost' : ''
    - name: defaultSeccompLocalhostProfile
      expression: |
        variables.defaultSeccompType == 'Localhost'
          ? variables.seccompFirst.substring('localhost/'.size()) : ''
    - name: needPodSeccomp
      expression: |
        variables.selectedName != '' && variables.defaultSeccompType != '' &&
        !object.spec.?securityContext.?seccompProfile.hasValue() &&
        object.spec.containers.all(c, !c.?securityContext.?seccompProfile.hasValue()) &&
        object.spec.?initContainers.orValue([]).all(c, !c.?securityContext.?seccompProfile.hasValue())

    - name: hasLiteralUid
      expression: |
        variables.selectedName != '' &&
        variables.selectedSpec.?runAsUser.?type.orValue('') == 'MustRunAs' &&
        variables.selectedSpec.?runAsUser.?uid.hasValue()
    - name: literalUid
      expression: |
        variables.hasLiteralUid ? variables.selectedSpec.?runAsUser.?uid.orValue(-1) : -1
    - name: needPodRunAsUser
      expression: |
        variables.hasLiteralUid &&
        !object.spec.?securityContext.?runAsUser.hasValue()

  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: |
          (variables.isEphemeralSubresource || variables.selectedName == '') ? Object{} :
          Object{
            metadata: Object.metadata{
              annotations: {
                "alauda.io/scc": string(variables.selectedName)
              }
            }
          }

    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: |
          (variables.isEphemeralSubresource || !variables.needPodRunAsUser) ? Object{} :
          Object{
            spec: Object.spec{
              securityContext: Object.spec.securityContext{
                runAsUser: variables.literalUid
              }
            }
          }

    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: |
          (variables.isEphemeralSubresource || !variables.needPodSeccomp) ? Object{} :
          (variables.defaultSeccompType == 'Localhost') ?
          Object{
            spec: Object.spec{
              securityContext: Object.spec.securityContext{
                seccompProfile: Object.spec.securityContext.seccompProfile{
                  type: 'Localhost',
                  localhostProfile: variables.defaultSeccompLocalhostProfile
                }
              }
            }
          } :
          Object{
            spec: Object.spec{
              securityContext: Object.spec.securityContext{
                seccompProfile: Object.spec.securityContext.seccompProfile{
                  type: variables.defaultSeccompType
                }
              }
            }
          }

    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: |
          (variables.isEphemeralSubresource || variables.selectedName == '') ? Object{} :
          Object{
            spec: Object.spec{
              containers: object.spec.containers.map(c, Object.spec.containers{
                name: c.name,
                securityContext: Object.spec.containers.securityContext{
                  allowPrivilegeEscalation:
                    c.?securityContext.?allowPrivilegeEscalation.hasValue()
                      ? c.securityContext.allowPrivilegeEscalation
                      : variables.defaultPE
                }
              })
            }
          }

    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: |
          (variables.isEphemeralSubresource || variables.selectedName == '' || !object.spec.?initContainers.hasValue()) ? Object{} :
          Object{
            spec: Object.spec{
              initContainers: object.spec.initContainers.map(c, Object.spec.initContainers{
                name: c.name,
                securityContext: Object.spec.initContainers.securityContext{
                  allowPrivilegeEscalation:
                    c.?securityContext.?allowPrivilegeEscalation.hasValue()
                      ? c.securityContext.allowPrivilegeEscalation
                      : variables.defaultPE
                }
              })
            }
          }

    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: |
          (!variables.isEphemeralSubresource || variables.selectedName == '' || !object.spec.?ephemeralContainers.hasValue()) ? Object{} :
          Object{
            spec: Object.spec{
              ephemeralContainers: object.spec.ephemeralContainers.map(c, Object.spec.ephemeralContainers{
                name: c.name,
                securityContext: Object.spec.ephemeralContainers.securityContext{
                  allowPrivilegeEscalation:
                    c.?securityContext.?allowPrivilegeEscalation.hasValue()
                      ? c.securityContext.allowPrivilegeEscalation
                      : variables.defaultPE
                }
              })
            }
          }

在两个策略上都保留 reports.kyverno.io/disabled: "true"。SCC 选择依赖准入请求中的 User、Groups 和 ServiceAccount,而后台报告扫描没有等效的请求身份。变更型策略也只会在准入期间填充默认值。将这些仅限准入的策略排除在后台报告之外,可以避免产生误导性的 PolicyReports,也避免对每个现有 Pod 进行不必要的重新编译。

两个策略默认都会跳过以下 namespace:以 kube-cpaas-alauda- 开头的 namespace,以及 kyvernocattle-systemoperatorsdefault。如果你的平台使用不同的系统 namespace,请相应调整两个策略中的 skip-system-ns 表达式。

步骤 1.4 — 以 Warn → Deny 的方式安全上线

校验策略通过 failurePolicy: FailvalidationActions: [Deny] 交付,这意味着它会立即拒绝不合规 Pod。在现有集群上,如果在未做准备的情况下直接启用,可能会破坏那些 ServiceAccount 还未绑定任何 SCC 的工作负载。

请采用三阶段滚动发布:

  1. 在首次应用前先使用 Warn。在现有集群上应用 scc-auto-pick.yaml 之前,将 validationActions 改为:

    validationActions:
      - Warn

    然后应用该文件。此时,策略会在每个原本会被拒绝的准入响应中附加警告,但仍然允许 Pod 通过。观察 Kyverno admission controller 日志以收集受影响的工作负载:

    kubectl logs -n kyverno -l app.kubernetes.io/component=admission-controller \
      --tail=500 | grep -i 'scc-auto-pick'
  2. 修复。针对每个被警告的工作负载,添加或修正 RBAC 绑定,使其 ServiceAccount 可以 use 一个合适的 SCC(参见第 2 部分)。使用以下命令确认:

    kubectl auth can-i use \
      securitycontextconstraints.security.alauda.io/<scc-name> \
      --as="system:serviceaccount:<namespace>:<sa-name>" -n <namespace>
  3. Deny。当合法工作负载不再产生警告后,将其切回 Deny 并重新应用:

    validationActions:
      - Deny
Tip

如果你需要临时豁免整个 namespace,可以将其添加到两个策略中的 skip-system-ns 表达式,或者创建一个 PolicyException 资源。有关 PolicyException 模式,请参见下方 了解更多

步骤 1.5 — 验证引擎已就绪

运行以下检查。所有资源都应该存在,并且两个策略都应该是 READY=true

# 1. CRD is established and 13 profiles are loaded
kubectl get crd securitycontextconstraints.security.alauda.io
kubectl get scc

# 2. Five GCE caches exist
kubectl get globalcontextentry scc-profiles scc-clusterroles \
  scc-clusterrolebindings scc-rolebindings scc-roles

# 3. Two admission policies are ready
kubectl get validatingpolicy scc-auto-pick
kubectl get mutatingpolicy scc-fill-defaults

# 4. Reader RBAC is in place
kubectl get clusterrole kyverno-scc-reader
kubectl get clusterrolebinding kyverno-scc-reader

如果 scc-fill-defaults 显示 READY=false,最常见的原因是缺少对 pods/ephemeralcontainers 的读取权限——请确保完整应用了步骤 1.3 中的 kyverno-scc-reader ClusterRole。

第 2 部分:授权工作负载使用 SCC

安装引擎后,默认情况下没有任何 Pod 被授予 SCC。除非管理员为某个 ServiceAccount(或 User,或 Group)创建了 RBAC 绑定,否则在非系统 namespace 中以该受试对象运行的 Pod 会被拒绝,并提示 Pod violates all SCCs assigned to its ServiceAccount

请将每个 SCC 绑定视为一个安全授权决策。只有平台管理员或安全管理员才应拥有授予 SCC 绑定权限;普通应用用户和 namespace 所有者不应能够为自己提升 Pod 权限。

步骤 2.1 — 选择合适的 SCC 配置文件

将工作负载的安全需求与下表进行匹配。默认情况下,引擎会先按 priority、再按 restrictiveScore 对已授予的 SCC 进行排序。选择工作负载所需的最小权限配置文件集合,并在必须强制使用某个特定配置文件时使用 alauda.io/required-scc

工作负载特征推荐 SCC
无状态服务、非 root、丢弃所有 capabilities、无 host 访问restricted-v2
与上相同,但需要绑定 1024 以下端口restricted-v2(该配置文件已允许 NET_BIND_SERVICE
与上相同,但使用固定 UID 范围,例如 1000–65534,并使用 user namespacesrestricted-v3
以非 root 用户运行,但不能丢弃所有 capabilities 的服务nonroot-v2(丢弃 ALL)或 nonroot(较早的丢弃集合)
需要以 root 运行的镜像(USER rootanyuid
需要 hostNetwork 和 host ports 的 Ingress controller 或其他 Podhostnetwork-v2(丢弃 ALL)或 hostnetwork(较早的丢弃集合)
挂载 hostPath 用于日志 / 指标收集、以非 root 运行的服务hostmount-anyuid
与上相同,但不需要 SELinux 重新标记hostmount-anyuid-v2
需要 hostNetworkhostPIDhostIPC 和 host paths 的诊断 Podhostaccess
使用 user namespaces 的 container-in-container 构建沙箱nested-container
完全特权工作负载(CNI、存储驱动、调试 Pod)privileged
Warning

始终只授予所需的最小权限。绑定到 privileged 的 ServiceAccount 可以运行任何 Pod,包括那些能够突破容器边界的 Pod。privileged 应仅保留给基础设施 DaemonSets,不要授予用户工作负载。

当应用经理请求 SCC 访问时,请提供:

  • Namespace 和 ServiceAccount,例如 databases/postgres-sa
  • 工作负载名称和控制器类型,例如 StatefulSet/postgres
  • 请求的 SCC 或所需能力,例如因为镜像以 UID 0 运行而需要 anyuid
  • 为什么更严格的 SCC(如 restricted-v2)不足以满足需求。
  • 工作负载是否必须使用 alauda.io/required-scc 固定到某个特定 SCC。

步骤 2.2 — 将 SCC 绑定到 ServiceAccount

管理员最常见的操作是将某个 SCC 绑定到工作负载的 ServiceAccount。假设你有一个在 databases/postgres-sa 下运行的应用,并且镜像以 root(UID 0)运行。你希望允许该 ServiceAccount 使用 anyuid,同时仍保留 restricted-v2 供更严格的工作负载使用。在这个 root UID 示例中,restricted-v2 不匹配(runAsUser.uidRangeMin: 1),因此准入会选择 anyuid。更一般地说,当一个 Pod 同时满足两个配置文件时,本指南中的默认配置会优先选择 anyuid,因为除非你调整优先级或固定 alauda.io/required-scc,否则 anyuidpriority 高于 restricted-v2

将以下内容保存为 bind-postgres-sa.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: scc-use-anyuid-restricted
  labels:
    rbac.alauda.io/scc-use: "true"
rules:
  - apiGroups: ["security.alauda.io"]
    resources: ["securitycontextconstraints"]
    resourceNames: ["anyuid", "restricted-v2"]
    verbs: ["use"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: postgres-sa-scc
  namespace: databases
  labels:
    rbac.alauda.io/scc-use: "true"
subjects:
  - kind: ServiceAccount
    name: postgres-sa
    namespace: databases
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: scc-use-anyuid-restricted

应用:

kubectl apply -f bind-postgres-sa.yaml

rbac.alauda.io/scc-use=true 标签是可选的。它不会影响 SCC 选择,但可让你通过 kubectl get clusterrole,rolebinding -l rbac.alauda.io/scc-use=true -A 列出所有与 SCC 相关的 RBAC 对象。

Note

你同样可以使用 ClusterRoleBinding 为这个有命名空间的 ServiceAccount 授予集群级 use 权限。通常当你希望授权只在某一个 namespace 内生效时,使用有命名空间的 RoleBinding 更清晰。

步骤 2.3 — 将 SCC 绑定到 User

当一名受信任的人类操作员(以 Kubernetes User 身份认证,例如通过 OIDC 或证书)需要直接启动 Pod 时——例如运行 kubectl debugkubectl run 的 SRE——你可以将 SCC 授予该 User 主体。

将其保存为 bind-user-sre.yaml,并将 [email protected] 替换为你的 User 名称:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: scc-use-hostaccess
  labels:
    rbac.alauda.io/scc-use: "true"
rules:
  - apiGroups: ["security.alauda.io"]
    resources: ["securitycontextconstraints"]
    resourceNames: ["hostaccess"]
    verbs: ["use"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sre-alice-hostaccess
  labels:
    rbac.alauda.io/scc-use: "true"
subjects:
  - kind: User
    name: [email protected]
    apiGroup: rbac.authorization.k8s.io
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: scc-use-hostaccess

应用:

kubectl apply -f bind-user-sre.yaml

[email protected] 直接运行 kubectl run(而不是通过某个控制器的 ServiceAccount)时,他们创建的 Pod 会以其 User 身份被准入,并获得 hostaccess

步骤 2.4 — 将 SCC 绑定到 Group

Group 绑定适用于管理员管理的整体策略,例如“每个已认证用户都可以运行 restricted-v2 Pod”。两个合成 Group 尤其相关:

  • system:authenticated —— 每个已认证主体。
  • system:serviceaccounts:<namespace> —— 某个特定 namespace 中的每个 ServiceAccount。

将其保存为 bind-group-authenticated.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: scc-use-restricted-v2
  labels:
    rbac.alauda.io/scc-use: "true"
rules:
  - apiGroups: ["security.alauda.io"]
    resources: ["securitycontextconstraints"]
    resourceNames: ["restricted-v2"]
    verbs: ["use"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: scc-use-restricted-v2-authenticated
  labels:
    rbac.alauda.io/scc-use: "true"
subjects:
  - kind: Group
    name: system:authenticated
    apiGroup: rbac.authorization.k8s.io
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: scc-use-restricted-v2
Warning

system:authenticated Group 绑定是一个兜底机制,用于捕获那些 ServiceAccount 没有显式 SCC 绑定的工作负载。它可作为步骤 1.4 中 Warn 阶段上线时的迁移安全网。一旦每个工作负载都有了显式绑定,就应移除此兜底。如果永久保留它,未来一旦新增带有宽松默认值的 SCC 配置文件,你的影响范围会被扩大。

若要将绑定限制为单个 namespace 的 ServiceAccounts,请将 subjects 改为:

subjects:
  - kind: Group
    name: system:serviceaccounts:my-namespace
    apiGroup: rbac.authorization.k8s.io

步骤 2.5 — 使用 alauda.io/required-scc 固定到特定 SCC

默认情况下,引擎会选择受试对象被允许使用的、且 Pod 实际满足条件的最严格 SCC。如果你有一个必须始终以某个特定配置文件准入的工作负载——例如,一个审计敏感的部署,必须使用 restricted-v3,即使其 ServiceAccount 也被允许 anyuid——就在 Pod 上设置 alauda.io/required-scc 注解:

apiVersion: v1
kind: Pod
metadata:
  name: audited-app
  namespace: payments
  annotations:
    alauda.io/required-scc: restricted-v3
spec:
  serviceAccountName: payments-sa
  securityContext:
    runAsNonRoot: true
    runAsUser: 1500
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: registry.example.com/payments/audited-app:1.2.3
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]

alauda.io/required-scc 注解只会从该受试对象已被授权使用的 SCC 中进行选择,并不会授予 SCC 访问权限。该注解要生效,必须同时满足以下条件:

  • 集群中存在名为 restricted-v3 的 SecurityContextConstraints。
  • payments/payments-sa 已通过一个为该资源名授予 use 权限的 ClusterRole 或 Role 绑定到 restricted-v3

如果任一条件不满足,Pod 将被拒绝。校验策略会针对每种情况输出具体消息(参见 故障排查)。

当使用 PodTemplate 风格控制器(Deployment、StatefulSet、Job)时,请将注解放在 Pod template 的 metadata 中,而不是放在控制器上:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: audited-app
  namespace: payments
spec:
  selector:
    matchLabels:
      app: audited-app
  template:
    metadata:
      labels:
        app: audited-app
      annotations:
        alauda.io/required-scc: restricted-v3
    spec:
      serviceAccountName: payments-sa
      # ...

步骤 2.6 — 验证绑定是否生效

应用任意绑定后,请运行以下检查。

管理员验证 — 确认该主体可以 use 该 SCC:

kubectl auth can-i use \
  securitycontextconstraints.security.alauda.io/anyuid \
  --as="system:serviceaccount:databases:postgres-sa" -n databases

期望输出为 yes。如果得到 no,请重新检查 ClusterRole 中的 apiGroupsresourcesresourceNamesverbs

应用所有者验证 — 在管理员确认绑定后,使用获批的 ServiceAccount 创建或重新部署工作负载,然后检查准入后的 Pod 注解。可快速探测如下:

kubectl -n databases run probe \
  --image=registry.example.com/library/pause:3.10 \
  --serviceaccount=postgres-sa \
  --overrides='{"spec":{"securityContext":{"runAsUser":999}}}' \
  --command -- /pause

kubectl -n databases get pod probe \
  -o jsonpath='{.metadata.annotations.alauda\.io/scc}{"\n"}'

输出应为引擎所选择的 SCC 名称(在本例中为 anyuid)。如果应用所有者无法创建探测 Pod,管理员可以执行此检查,或直接检查真实工作负载中的某个 Pod。

Note

GlobalContextEntries 通过 list/watch 刷新,通常会在几秒内将新绑定传播到准入缓存,在高负载情况下可能最长需要一分钟。如果你在应用新绑定后立即遭到拒绝,请稍等片刻后重试,再判断是否为绑定错误。

结果

完成第 1 部分并至少完成一个第 2 部分绑定后,你应该能够验证以下所有内容:

  • kubectl get crd securitycontextconstraints.security.alauda.io 显示 CRD 状态为 Established=True
  • kubectl get scc 列出你安装的每个 SCC 配置文件。
  • kubectl get globalcontextentry 返回全部五个 scc-* 条目。
  • kubectl get validatingpolicy scc-auto-pickkubectl get mutatingpolicy scc-fill-defaults 都显示 READY=true
  • 在非系统 namespace 中,以已绑定 ServiceAccount 创建的 Pod 会收到 alauda.io/scc=<name> 注解,其中 <name> 是引擎选择的 SCC。
  • 在非系统 namespace 中,以未绑定 ServiceAccount 创建的 Pod 会在准入时被拒绝,并提示 Pod violates all SCCs assigned to its ServiceAccount

故障排查

使用下表将症状映射到原因和解决步骤。

对于由 Deployment、StatefulSet、Job 和 DaemonSet 等控制器创建的 Pod,实际生效的工作负载身份通常是 Pod 的 ServiceAccount。对于由受信任的人类操作员直接创建的 Pod,例如 kubectl runkubectl debug,User 和 Group 的 SCC 绑定也可能匹配准入请求。

症状可能原因检查内容
Pod violates all SCCs assigned to its ServiceAccount (candidates: ...)Pod 的 ServiceAccount 至少绑定了一个 SCC,但 Pod 的 spec 违反了这些 SCC 中的每一个。消息末尾的候选列表会列出被考虑的 SCC。对每个候选项,将 Pod 与该 SCC 的字段进行比较。常见不匹配:容器 runAsUser 超出允许范围;当 requiredDropCapabilities: [ALL] 时未设置 drop: [ALL];当 SCC 要求 runtime/default 时缺少 seccompProfile.type
Pod violates all SCCs assigned to its ServiceAccount (candidates: )(候选列表为空)没有任何 SCC 绑定到该 Pod 的 ServiceAccount。kubectl get scc 返回的每个 SCC 名称,运行 kubectl auth can-i use securitycontextconstraints.security.alauda.io/<name> --as=system:serviceaccount:<ns>:<sa> -n <ns>。如果全部返回 no,请按照步骤 2.2 添加绑定。
required SCC '<name>' not found in scc-profilesalauda.io/required-scc 注解引用了一个不存在的 SCC。运行 kubectl get scc <name>。修正注解,或安装缺失的配置文件。
required SCC '<name>' is not bound to ServiceAccount '<sa>' in namespace '<ns>'注解引用了该 ServiceAccount 没有 use 权限的 SCC。添加一个 RoleBinding,将 securitycontextconstraints/<name> 上的 use 权限授予该 SA,然后重试。
刚添加绑定,但 Pod 仍然被拒绝Kyverno GlobalContextEntry 缓存会异步刷新 RBAC 对象;新绑定需要几秒钟才能传播。等待 10–30 秒后重试。检查 kubectl get globalcontextentry scc-rolebindings -o jsonpath='{.status.lastRefreshTime}{"\n"}',确认已最近刷新。
Pod 已准入,但 runAsUser 意外地被设置 / 未设置变更型策略从所选 SCC 中填充了默认值,或者因为 Pod 已经声明了值而没有填充。检查 Pod 上的 alauda.io/scc 注解以了解选择了哪个 SCC,然后查看该 SCC 的 runAsUser.typerunAsUser.uid。Pod 自己声明的 runAsUser 永远不会被覆盖。
scc-fill-defaultsscc-auto-pick 上显示 READY=falseKyverno 缺少策略所匹配某个资源的读取权限(最常见的是 pods/ephemeralcontainers)。重新完整应用步骤 1.3 中的 kyverno-scc-reader ClusterRole。
位于 pod-security.kubernetes.io/enforce: restricted namespace 中的 Pod 在 Kyverno 看到之前就被拒绝Kubernetes Pod Security Admission 插件先于 Kyverno 运行,并独立强制执行 namespace 标签。根据该 namespace 中的工作负载,将 namespace 标签放宽为 baselineprivileged,或者限制你在那里提供的 SCC。

了解更多

使用 PolicyException 临时绕过策略

当你需要在短时间内允许某个 ServiceAccount 超出其当前 SCC(例如紧急调试会话),且修改 RBAC 绑定并不合适时,可以使用 PolicyException 资源。这要求 Kyverno 的 admission controller 启动时启用了 --enablePolicyException=true

apiVersion: policies.kyverno.io/v1alpha1
kind: PolicyException
metadata:
  name: postgres-debug-bypass
  namespace: policy-exceptions
spec:
  policyRefs:
    - name: scc-auto-pick
      kind: ValidatingPolicy
  matchConditions:
    - name: target-sa
      expression: |
        object.metadata.namespace == 'databases' &&
        object.spec.?serviceAccountName.orValue('') == 'postgres-sa'
    - name: must-be-debug-window
      expression: |
        object.metadata.?labels[?'debug-window'].orValue('') == 'open'

最佳实践:将 PolicyException 资源放在专用 namespace 中(例如 policy-exceptions),并限制写入权限;为每个例外添加 ownerexpire-at 标签,并按周期进行审计。

引擎如何选择 SCC

当多个 SCC 被授予给某个受试对象,且 Pod 同时满足其中多个时,校验策略按以下顺序对候选项排序:

  1. 先看更高的 priority
  2. 再看更高的 restrictiveScore

Pod 完整满足的第一个候选项就是最终选择的 SCC。变更型策略在填充默认值时使用相同的排序方式。这与 OpenShift 的意图一致,即“在可接受的 SCC 中,限制最严格者胜出”,同时允许运维人员通过每个配置文件的 priority 覆盖顺序。

OpenShift 命令映射

如果你来自 OpenShift,下面这些 oc 命令可以直接映射为对 SCC 引擎的 kubectl apply。这些操作会授予 SCC 的 use 权限,并且只应由有权更改集群 Pod 安全边界的管理员执行。

OpenShift 命令在本引擎中的等效操作
oc adm policy add-scc-to-user <scc> <user>创建一个在 securitycontextconstraints/<scc> 上授予 use 的 ClusterRole,然后创建一个 subjects: [{kind: User, name: <user>}] 的 ClusterRoleBinding。
oc adm policy add-scc-to-user <scc> -z <sa> -n <ns>与上面相同的 ClusterRole,再加上一个位于 namespace <ns>RoleBinding,其中 subjects: [{kind: ServiceAccount, name: <sa>, namespace: <ns>}]
oc adm policy add-scc-to-group <scc> <group>相同的 ClusterRole,再加上一个 subjects: [{kind: Group, name: <group>}] 的 ClusterRoleBinding。
oc get scckubectl get scc(该 CRD 的 shortNames: [scc] 保持了命令一致性)。

最佳实践:在现有 namespace 中授予特权例外

当一个现有 namespace 通常遵循 PSA restricted 标准,但同一 namespace 中的某个工作负载必须运行 privileged 容器时,请使用此操作步骤。namespace 保持不变;平台管理员将移除该 namespace 上的 PSA restricted 强制执行,并让 Kyverno SCC 引擎成为强制执行的准入边界。

隔离模型如下:

  • namespace 中的每个 ServiceAccount 都可以使用 restricted-v2 作为默认安全上限。
  • 例外工作负载使用专用 ServiceAccount。
  • 只有该 ServiceAccount 可以使用内置的 privileged SCC。
  • 工作负载通过 alauda.io/required-scc 固定到 privileged
  • 其他 ServiceAccounts 不会被授予 privileged,如果请求特权设置,仍会被拒绝。
Warning

PSA 和 Kyverno SCC 引擎是彼此独立的 admission controls。SCC 授权无法覆盖 pod-security.kubernetes.io/enforce: restricted。在移除 PSA 强制执行之前,请先完成 restricted SCC 基线和例外工作负载绑定,这样 namespace 就不会出现未受保护的过渡窗口。

第 1 步 - 审计现有 SCC 授权

在更改 PSA 之前,请检查普通 ServiceAccounts 是否已经通过 User、Group、RoleBinding 或 ClusterRoleBinding 获得了宽松 SCC。特别注意 system:authenticatedsystem:serviceaccountssystem:serviceaccounts:payments 的绑定。

例如,默认 ServiceAccount 不应能够使用内置的 privileged SCC:

kubectl auth can-i use \
  securitycontextconstraints.security.alauda.io/privileged \
  --as=system:serviceaccount:payments:default \
  -n payments

期望输出为 no。在继续之前,先移除任何非预期的宽泛授权。前面提到的可选标签 rbac.alauda.io/scc-use=true 可以帮助定位与 SCC 相关的 RBAC 对象,但不要只依赖标签,因为未打标签的 Role 仍然可以授予 use 权限。

第 2 步 - 将 restricted 基线绑定到所有 namespace ServiceAccounts

创建一个从合成的 system:serviceaccounts:payments Group 到步骤 2.4 中 scc-use-restricted-v2 ClusterRole 的 namespaced RoleBinding。这会覆盖该 namespace 中现有和未来的所有 ServiceAccounts。

保存为 payments-restricted-baseline.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: all-serviceaccounts-restricted-v2
  namespace: payments
  labels:
    rbac.alauda.io/scc-use: "true"
subjects:
  - kind: Group
    name: system:serviceaccounts:payments
    apiGroup: rbac.authorization.k8s.io
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: scc-use-restricted-v2

应用该绑定,并验证普通 ServiceAccount 只能使用 restricted 基线:

kubectl apply -f payments-restricted-baseline.yaml

kubectl auth can-i use \
  securitycontextconstraints.security.alauda.io/restricted-v2 \
  --as=system:serviceaccount:payments:default \
  -n payments

期望输出为 yes

第 3 步 - 创建专用 ServiceAccount

SCC 授权是基于主体的;它不会授予某个 Pod 名称权限。使用一个不与其他无关工作负载共享的专用 ServiceAccount,来建模单一工作负载例外。

保存为 payment-agent-serviceaccount.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-agent-privileged
  namespace: payments
automountServiceAccountToken: false

应用该 ServiceAccount:

kubectl apply -f payment-agent-serviceaccount.yaml
Warning

内置的 privileged SCC 故意不加限制。除了特权容器之外,它还允许 host networking、host namespaces、host ports、host paths、所有 Linux capabilities、所有 volume 类型、所有 seccomp profiles 和 unsafe sysctls。只有当工作负载确实需要完全特权时才授予它。如果工作负载只需要更小的例外,例如仅 anyuidhostNetworkhostPath,请改为绑定更窄的内置配置文件。

除非工作负载必须调用 Kubernetes API,否则请保持 automountServiceAccountToken: false。启用 token 自动挂载会将 API 凭据添加到一个已经拥有较高运行时权限的容器中。

第 4 步 - 仅向专用 ServiceAccount 授予 privileged

创建一个 resourceNames 仅包含内置 privileged SCC 的 ClusterRole,然后通过位于工作负载 namespace 中的 RoleBinding 将其绑定到专用 ServiceAccount。

保存为 payment-agent-scc-binding.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: scc-use-privileged
  labels:
    rbac.alauda.io/scc-use: "true"
rules:
  - apiGroups: ["security.alauda.io"]
    resources: ["securitycontextconstraints"]
    resourceNames: ["privileged"]
    verbs: ["use"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payment-agent-privileged-scc
  namespace: payments
  labels:
    rbac.alauda.io/scc-use: "true"
subjects:
  - kind: ServiceAccount
    name: payment-agent-privileged
    namespace: payments
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: scc-use-privileged

应用并验证该绑定:

kubectl apply -f payment-agent-scc-binding.yaml

kubectl auth can-i use \
  securitycontextconstraints.security.alauda.io/privileged \
  --as=system:serviceaccount:payments:payment-agent-privileged \
  -n payments

kubectl auth can-i use \
  securitycontextconstraints.security.alauda.io/privileged \
  --as=system:serviceaccount:payments:default \
  -n payments

期望输出为:payment-agent-privilegedyesdefaultno。该专用 ServiceAccount 还会从 namespace Group 绑定继承 restricted-v2

第 5 步 - 防止其他工作负载复用该 ServiceAccount

此操作会将 privileged 授予某个 ServiceAccount,而不是某个 Pod 或控制器名称。Kubernetes RBAC 不提供针对 spec.serviceAccountName 的字段级授权。如果某个用户可以在 payments 中创建任意 Pod、Deployment、StatefulSet、DaemonSet、Job 或 CronJob,那么他就可能从其他工作负载引用 payment-agent-privileged,并获得相同的 SCC 访问权限。

在继续之前,请至少实施以下一种控制:

  • 仅允许专用的 GitOps 或部署身份创建或更新引用 payment-agent-privileged 的工作负载,并阻止普通 namespace 用户修改这些工作负载。
  • 添加一个 admission guard,拒绝对 payment-agent-privileged 的引用,除非请求已获授权。将该 guard 同时应用于直接 Pod 和该 namespace 中启用的每一种 Pod-template 控制器。

不要仅依赖工作负载标签或注解来授权使用专用 ServiceAccount;能够创建工作负载的用户通常也可以复制这些值。如果既无法限制部署所有权,也无法限制准入时的 ServiceAccount 使用,则此模式只能提供 ServiceAccount 级别的例外,不得将其表示为单 Pod 例外。

在强制实施其中一种控制之前,不要继续执行 PSA 迁移。

第 6 步 - 将强制执行从 PSA 转移到 Kyverno SCC

只移除 PSA restricted 强制执行标签。保留 restricted 的 audit 和 warning 标签,以便 API server 继续报告那些会违反 restricted 标准的工作负载。

kubectl label namespace payments \
  pod-security.kubernetes.io/enforce- \
  pod-security.kubernetes.io/enforce-version- \
  --overwrite

kubectl label namespace payments \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=latest \
  --overwrite

移除 namespace 标签不会覆盖通过 API server admission configuration 配置的集群级 PSA 默认值。如果集群默认值仍然强制执行 restricted,请显式将该 namespace 从强制执行中排除,同时保留 audit 和 warning 可见性:

kubectl label namespace payments \
  pod-security.kubernetes.io/enforce=privileged \
  pod-security.kubernetes.io/enforce-version=latest \
  --overwrite

这里 enforce=privileged 的意思是 PSA 不再施加额外限制。它并不会向 Pod 授予 privileged 访问;Kyverno SCC 绑定仍然是强制执行的授权边界。

第 7 步 - 在工作负载中固定获批的 SCC

alauda.io/required-scc 放入 Pod template 的 metadata 中,并使用专用 ServiceAccount。不要只把注解放在 Deployment metadata 上,因为控制器的 metadata 不会自动复制到其 Pod。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-agent
  namespace: payments
spec:
  replicas: 1
  selector:
    matchLabels:
      app: payment-agent
  template:
    metadata:
      labels:
        app: payment-agent
      annotations:
        alauda.io/required-scc: privileged
    spec:
      serviceAccountName: payment-agent-privileged
      securityContext:
        seccompProfile:
          type: Unconfined
      containers:
        - name: agent
          image: registry.example.com/payments/agent:1.0.0
          securityContext:
            privileged: true
            allowPrivilegeEscalation: true
            runAsUser: 0

该注解不会授予 SCC 权限。如果 SCC 不存在,或者 ServiceAccount 没有 use 权限,校验策略会拒绝该 Pod。

第 8 步 - 验证例外是否隔离

请使用真实的应用部署身份运行 admission 探测,而不是使用 cluster-admin。对于直接创建的 Pod,SCC 选择会考虑请求中的 User 和 Groups,因此拥有额外宽泛 SCC 授权的管理员,可能会得到与生产中工作负载身份不同的结果。

验证该例外工作负载是否以预期 SCC 被准入:

kubectl -n payments get pod -l app=payment-agent \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.alauda\.io/scc}{"\n"}{end}'

SCC 列应为 privileged

接下来,确认普通 restricted Pod 仍然可以以 restricted-v2 被准入:

cat <<'EOF' | kubectl create --dry-run=server -f - \
  -o jsonpath='{.metadata.annotations.alauda\.io/scc}{"\n"}'
apiVersion: v1
kind: Pod
metadata:
  name: restricted-probe
  namespace: payments
spec:
  serviceAccountName: default
  securityContext:
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: probe
      image: registry.example.com/library/pause:3.10
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]
EOF

期望输出为 restricted-v2

最后,确认普通 ServiceAccount 不能请求 privileged

cat <<'EOF' | kubectl create --dry-run=server -f -
apiVersion: v1
kind: Pod
metadata:
  name: unauthorized-privileged-probe
  namespace: payments
  annotations:
    alauda.io/required-scc: privileged
spec:
  serviceAccountName: default
  containers:
    - name: probe
      image: registry.example.com/library/pause:3.10
      securityContext:
        privileged: true
EOF

当由正常的应用部署身份提交时,请求必须被拒绝,并提示所需 SCC 未绑定到 ServiceAccount default。如果该 Pod 被准入,请停止并检查宽泛的 SCC RoleBindings、ClusterRoleBindings,以及 User 或 Group 授权,然后再允许生产工作负载继续运行。

回滚该例外

要安全恢复 PSA restricted 强制执行,请按以下步骤操作:

  1. 停止或替换特权工作负载,使其变为符合 restricted 的规格。

  2. 移除授予专用 ServiceAccount privileged 的 RoleBinding:

    kubectl -n payments delete rolebinding payment-agent-privileged-scc
  3. 确认该 ServiceAccount 不再能够使用 privileged

    kubectl auth can-i use \
      securitycontextconstraints.security.alauda.io/privileged \
      --as=system:serviceaccount:payments:payment-agent-privileged \
      -n payments

    授权检查必须返回 no。不要删除内置的 privileged SCC。只有在没有其他 RoleBinding 或 ClusterRoleBinding 引用它时,才删除可复用的 scc-use-privileged ClusterRole。

  4. 对剩余工作负载执行 server-side dry-run 检查。

  5. 最后恢复 PSA restricted 强制执行:

    kubectl label namespace payments \
      pod-security.kubernetes.io/enforce=restricted \
      pod-security.kubernetes.io/enforce-version=latest \
      --overwrite

后续步骤

  • 在审查工作负载需求后,确定每个现有 namespace 和 ServiceAccount 应绑定到哪个 SCC,记录该映射,并通过 GitOps 工作流应用这些绑定,以便实现可审计、可复现。
  • 计划对 PolicyException 资源进行定期审查——它们的用途是短期窗口,而不是永久例外。
  • 如果你的规模较大,请监控 Kyverno admission controller 的 kyverno_admission_review_duration_seconds 指标,以便在 SCC 配置文件或 RBAC 绑定数量增长时检测准入延迟变化。