为 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 绑定的权限,因为这样会让他们绕过集群安全边界。应用团队应描述工作负载需求,例如 anyuid、hostNetwork 或 hostPath;平台或安全管理员审查请求,并将最小权限 SCC 绑定到相应受试对象。
各角色职责
使用下表来判断本指南中的哪些部分适用于你。
如果你是平台或安全管理员,请按照第 1 部分和第 2 部分执行。如果你是应用经理,请先使用步骤 2.1 准备 SCC 请求,然后仅在管理员批准并完成绑定后,才使用步骤 2.5 和步骤 2.6。不要自行应用步骤 2.2 到步骤 2.4 中的 RBAC 清单。
标准工作流如下:
- 应用经理识别工作负载需求和目标 ServiceAccount。
- 平台或安全管理员选择最小权限 SCC 并创建 RBAC 绑定。
- 应用经理使用获批的 ServiceAccount 部署工作负载,只有在管理员要求固定到某个特定 SCC 时,才添加
alauda.io/required-scc。
- 管理员使用
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 中。
先决条件
开始前,请确保满足以下所有条件:
-
Kubernetes 集群运行 1.30 或更高版本(CEL admission 已稳定)。
-
Kyverno 已经安装并运行,版本为 v4.3.1 或更高,并且可用 MutatingPolicy、ValidatingPolicy 和 GlobalContextEntry CRD。你可以使用以下命令验证:
kubectl get crd validatingpolicies.policies.kyverno.io mutatingpolicies.policies.kyverno.io globalcontextentries.kyverno.io
-
kyverno namespace 中包含以下 ServiceAccounts(默认 Kyverno 安装):
kyverno-admission-controller
kyverno-background-controller
kyverno-reports-controller
-
你拥有 cluster-admin(或等效)权限,因为安装引擎需要创建 CRD、ClusterRoles、ClusterRoleBindings、GlobalContextEntries 和 admission policies。
-
你已经审查了打算允许非 restricted Pod 的每个 namespace 上的 Pod Security Admission (PSA) enforce 标签。PSA 会在 Kyverno 之前运行;如果某个 namespace 标记为 pod-security.kubernetes.io/enforce: restricted,那么在 Kyverno 介入前,该 namespace 会拒绝任何与宽松 SCC(例如 anyuid 或 hostnetwork-v2)匹配的 Pod。请在适当的 namespace 中将标签调整为 baseline 或 privileged,或者限制你在这些 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: 100 到 restrictiveScore: 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 个配置文件列出,并且其 Priority 和 Score 列已填充(以及其他 SCC 列,如 Priv、RunAsUser 和 Volumes)。
步骤 1.3 — 安装 GlobalContextEntries、Kyverno 读取 RBAC,以及 admission policies
此步骤一次性安装三项内容:
- GlobalContextEntries (GCE) —— 五个内存缓存,Kyverno 在准入过程中使用它们查找 SCC 配置文件、ClusterRoles、ClusterRoleBindings、RoleBindings 和 Roles,而无需为每次请求发起 API 调用。
- Reader RBAC —— 一个 ClusterRole,授予 Kyverno 的三个服务账户对 SCC CRD、上述四类 RBAC 资源,以及策略所匹配的 Pod /
pods/ephemeralcontainers 资源的只读权限。
- 两个 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)期间会检查每个匹配资源的读取权限,因此必须包含 pods 和 pods/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 继承而来的 runAsUser、seccompProfile 和 allowPrivilegeEscalation 默认值。
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,以及 kyverno、cattle-system、operators 和 default。如果你的平台使用不同的系统 namespace,请相应调整两个策略中的 skip-system-ns 表达式。
步骤 1.4 — 以 Warn → Deny 的方式安全上线
校验策略通过 failurePolicy: Fail 和 validationActions: [Deny] 交付,这意味着它会立即拒绝不合规 Pod。在现有集群上,如果在未做准备的情况下直接启用,可能会破坏那些 ServiceAccount 还未绑定任何 SCC 的工作负载。
请采用三阶段滚动发布:
-
在首次应用前先使用 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'
-
修复。针对每个被警告的工作负载,添加或修正 RBAC 绑定,使其 ServiceAccount 可以 use 一个合适的 SCC(参见第 2 部分)。使用以下命令确认:
kubectl auth can-i use \
securitycontextconstraints.security.alauda.io/<scc-name> \
--as="system:serviceaccount:<namespace>:<sa-name>" -n <namespace>
-
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。
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,否则 anyuid 的 priority 高于 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 debug 或 kubectl 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 中的 apiGroups、resources、resourceNames 和 verbs。
应用所有者验证 — 在管理员确认绑定后,使用获批的 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-pick 和 kubectl 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 run 或 kubectl debug,User 和 Group 的 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),并限制写入权限;为每个例外添加 owner 和 expire-at 标签,并按周期进行审计。
引擎如何选择 SCC
当多个 SCC 被授予给某个受试对象,且 Pod 同时满足其中多个时,校验策略按以下顺序对候选项排序:
- 先看更高的
priority。
- 再看更高的
restrictiveScore。
Pod 完整满足的第一个候选项就是最终选择的 SCC。变更型策略在填充默认值时使用相同的排序方式。这与 OpenShift 的意图一致,即“在可接受的 SCC 中,限制最严格者胜出”,同时允许运维人员通过每个配置文件的 priority 覆盖顺序。
OpenShift 命令映射
如果你来自 OpenShift,下面这些 oc 命令可以直接映射为对 SCC 引擎的 kubectl apply。这些操作会授予 SCC 的 use 权限,并且只应由有权更改集群 Pod 安全边界的管理员执行。
最佳实践:在现有 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:authenticated、system:serviceaccounts 和 system: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。只有当工作负载确实需要完全特权时才授予它。如果工作负载只需要更小的例外,例如仅 anyuid、hostNetwork 或 hostPath,请改为绑定更窄的内置配置文件。
除非工作负载必须调用 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-privileged 为 yes,default 为 no。该专用 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 强制执行,请按以下步骤操作:
-
停止或替换特权工作负载,使其变为符合 restricted 的规格。
-
移除授予专用 ServiceAccount privileged 的 RoleBinding:
kubectl -n payments delete rolebinding payment-agent-privileged-scc
-
确认该 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。
-
对剩余工作负载执行 server-side dry-run 检查。
-
最后恢复 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 绑定数量增长时检测准入延迟变化。