一个困惑的星期二凌晨
周二 02:14,值班工程师收到一条告警:生产集群 payment-ns 命名空间下的所有新 Pod 全部卡在 Pending 状态,ReplicaSet 控制器不断重试,Kubernetes 事件日志显示:
Error creating: pods \"payment-processor-7d4f6c8b9-x2k3l\" is forbidden:
violates PodSecurity \"restricted:latest\": privileged
(container \"processor\" must not set securityContext.privileged=true),
runAsNonRoot != true (pod or container must set
securityContext.runAsNonRoot=true)
一小时前,平台团队刚给这个命名空间打了一个标签:pod-security.kubernetes.io/enforce=restricted。他们以为这是 PSP 的"平替"——就像 PSP 那样,拒绝不合规的 Pod,但不会影响已经在运行的 Pod。他们以为对了前一半,没料到后一半的后果比想象中严重得多:Pod 不是被"拒绝创建"那么简单,而是 Deployment 的滚动更新完全卡死,新 Pod 起不来,旧 Pod 被缩容,最终导致服务部分不可用。
一个常见的误区:PSA 和 PSP 都是"准入控制",所以差不多。 实际上,PSP 和 PSA 的决策模型完全不同,从迁移的第一步开始,就处处是坑。
为什么说 PSA 不是 PSP 的平替
看过官方迁移文档的人都会注意到一行小字:"Pod Security Admission 是一个非变更性质的准入控制器"——这句话翻译成人话就是:PSP 会帮你改 Pod 的字段,PSA 不会。
PSP 是一个 Mutating + Validating Admission Controller。当你创建一个 Pod 时,如果它缺少 securityContext.runAsNonRoot,PSP 会自动注入默认值。它还会自动 drop 你不需要的 Capabilities,自动设置 Seccomp Profile。很多人以为"我的 Pod 能跑,说明它符合安全策略"——其实只是 PSP 帮你擦了屁股。
PSA 是纯 Validating 的。它只做一件事:检查 Pod 的字段是否满足策略要求,不满足就拒绝(或记录警告),但绝不帮你修改任何字段。
这意味着你从 PSP 迁移到 PSA 后,那些之前"能跑"的 Pod 全部会暴露原形。它们的 YAML 里可能根本没有 runAsNonRoot,没有 seccompProfile,capabilities.drop 是空的——这些在 PSP 时代被自动注入的字段,到 PSA 时代需要你自己写在 Pod 模板里。
| 能力 | PSP | PSA |
|---|---|---|
| 拒绝不符合策略的 Pod | ✅ | ✅ |
| 自动注入缺失的安全字段 | ✅ | ❌ |
| 基于 RBAC 绑定策略到用户/SA | ✅ | ❌ |
| 标签/注解驱动策略绑定 | ❌ | ✅ |
| 多档策略级别 | 自定义 | 三档标准 |
| 支持 audit/warn 模式 | ❌ | ✅ |
| 支持豁免规则 | Partial | ✅ |
另一个关键差异:PSP 的策略绑定依赖 RBAC。用户必须通过 Role/ClusterRole 获得 use 某个 PSP 的权限,然后 PSP 才能生效。这种设计导致了两个严重问题:一是策略定义和权限控制耦合,二是当某个用户没有 use 任何 PSP 时,他创建的 Pod 会完全绕过安全策略。PSA 的策略绑定只依赖命名空间标签,策略的作用范围和命名空间的生命周期绑定,不再需要维护 RBAC 的关联关系。
陷阱一:直接上 enforce=restricted,不经过 audit 过渡
错误做法:在现有命名空间上直接执行 kubectl label ns production pod-security.kubernetes.io/enforce=restricted,以为就像打开一个开关。
这个命令本身没错,但它的效果是立即生效。所有存量 Pod 虽然不会被驱逐(PSA 只影响新创建和更新的 Pod),但 Deployment 的滚动更新、HPA 自动扩缩、CronJob 新建 Pod——所有这些操作都会被 PSA 拦截。如果该命名空间下的 Pod 模板不满足 restricted 的要求,滚动更新会失败,旧 Pod 被缩容后新 Pod 起不来,服务实际降级。
正确做法:使用 audit 模式先观察。
# 第一步:先开 audit 和 warn,观察一周
kubectl label --overwrite ns production \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
# 第二步:查询审计日志中记录的违规事件
kubectl get events -n production --field-selector reason=FailedCreateAudit 模式会把违规记录到 API Server 审计日志,但不拒绝 Pod;Warn 模式会在 kubectl apply 返回时输出警告信息。建议先让 audit 模式运行至少 3-7 天,覆盖完整的发布周期,确认所有工作负载都合规后再切换到 enforce 模式。
陷阱二:Helm Chart 模板中的安全字段是"隐形"的
错误做法:跑 kubectl get pods -n staging -o yaml 检查所有 Pod,发现 securityContext 都正确,就认为 PSA 不会出问题。
静态检查正在运行的 Pod 是一个误导性极强的做法。原因有两点:
-
你看到的 Pod 字段可能是 PSP 注入的,不是 Pod 模板里的。PSP 被移除后,新 Pod 不会继承这些字段。
-
大量工作负载的
securityContext是通过 Helm Chart 模板条件注入的。例如:
# values.yaml
securityContext:
enabled: false # 默认关闭!
# templates/deployment.yaml
{{- if .Values.securityContext.enabled }}
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
{{- end }}当 enabled: false 时,securityContext 字段在最终渲染的 YAML 中完全不存在。PSA 不会因为字段不存在就放行——restricted 级别要求 runAsNonRoot: true 必须显式存在,缺失就是违规。
验证方法:用 helm template 渲染出完整 YAML 再检查,而不是查正在运行的 Pod。
helm template my-release ./charts/my-app \
--namespace staging \
| kubectl label --dry-run=server -f - \
pod-security.kubernetes.io/enforce=restricted--dry-run=server 会在 API Server 端执行真正的 PSA 校验,但不实际创建资源。这是最可靠的预检方式。
陷阱三:restricted 不只要求"不以 root 运行"
错误做法:以为 restricted 就是加一个 runAsNonRoot: true 就完事。
很多人在 kubectl create 遇到 violates PodSecurity \"restricted:latest\" 错误时,第一反应是"我把 runAsNonRoot 加上就行了"。但 PSA 的 restricted 级别是一组组合约束,漏掉任何一条都会被拒绝。
| 约束字段 | 要求 | 常见遗漏场景 |
|---|---|---|
runAsNonRoot: true 或 runAsUser > 0 | 非 root 运行 | 基础镜像默认 root |
allowPrivilegeEscalation: false | 禁止提权 | 忘记显式设置 |
capabilities.drop: [\"ALL\"] | 丢弃所有 Capabilities | 只写了 drop: 但没写 [\"ALL\"] |
seccompProfile.type: RuntimeDefault | 启用 Seccomp | 默认值是 Unconfined |
hostNetwork: false | 禁用宿主机网络 | 默认值即为 false,restricted 要求不得设为 true |
hostPID: false | 禁用宿主机 PID | 默认值即为 false,restricted 要求不得设为 true |
hostIPC: false | 禁用宿主机 IPC | 默认值即为 false,restricted 要求不得设为 true |
一个典型的合规 Pod 安全上下文:
securityContext:
runAsNonRoot: true
runAsUser: 1001
seccompProfile:
type: RuntimeDefault
capabilities:
drop: ["ALL"]
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true注意:readOnlyRootFilesystem 不是 restricted 的硬性要求,但它是生产加固的推荐设置。capabilities.drop 允许额外 add 某些能力(如 NET_BIND_SERVICE),但 drop: [\"ALL\"] 是必须的。
陷阱四:Helm Operator 和 Istio 注入造成的"隐形违规"
错误做法:自己写的 Deployment YAML 看起来合规,就认为最终运行的 Pod 也合规。
这是 PSA 迁移中最隐蔽的坑。Pod 的最终 spec 不是由 Deployment YAML 单独决定的。以下情况都会导致最终 Pod 的 spec 和你写的 YAML 不一致:
-
Istio sidecar 注入:Istio 的 sidecar injector 会将 Proxy 容器注入到 Pod 中,这个注入发生在 MutatingAdmissionWebhook 阶段。如果 Proxy 容器的
securityContext有privileged或capabilities.add等不被 restricted 允许的字段,PSA 会拒绝整个 Pod。 -
Helm Operator 动态注入:某些 Operator(如 Strimzi、Redis Operator)会在 Reconciling 过程中修改 Pod 模板,添加 hostPath volume 或 privileged 容器。
-
PodSecurityPolicy 的 Mutating 残留:如果你在迁移过程中开启了 PSP 和 PSA 双轨运行,PSP 的 mutate 已经修改了 Pod 的字段,但 PSA 是在 PSP mutate 之后执行的。这意味着你最终看到的 Pod 可能同时受两者影响,难以排查。
排查方法:使用 kubectl describe 查看 Pod 被拒绝的实际原因,而不是只看 Deployment 的状态。
# 查看被拒绝的 Pod 创建事件
kubectl get events -n my-namespace --field-selector reason=FailedCreate
# 查看 API Server 的审计日志中的违规详情
# 审计日志中 pod-security.kubernetes.io/audit-violations 注解
# 包含了完整的违规字段列表陷阱五:忘记给系统命名空间设置豁免
错误做法:把所有命名空间统一打上 enforce=restricted 标签,包括 kube-system、istio-system、velero-system 等。
Kubernetes 系统组件(如 CoreDNS、kube-proxy、CNI 插件)通常需要 hostNetwork、privileged 或 hostPath 等能力,它们无法在 restricted 级别下运行。随意给系统命名空间设置 restricted 策略会导致集群组件故障。
# 错误:给所有命名空间强制执行 restricted
kubectl label --overwrite ns --all \
pod-security.kubernetes.io/enforce=restrictedPSA 的豁免机制有两种:
- 命名空间级别的豁免:在
AdmissionConfiguration中配置豁免列表。
# /etc/kubernetes/admission-control-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: "restricted"
enforce-version: "latest"
exemptions:
namespaces:
- kube-system
- istio-system
- velero-system
- cert-manager
- nvidia-network-operator # 见 NVIDIA 官方文档要求- RuntimeClass 豁免:某些需要特权的运行时(如 GPU Operator)可以通过 RuntimeClass 获得豁免。
注意:AKS 默认对这些命名空间进行了豁免,但自建集群需要手动配置。EKS 的 eks.privileged PSP 自动迁移到 PSA 后,系统命名空间也是豁免的,但业务命名空间需要自行策略。AWS EKS 迁移文档 明确说明:"除了 eks.privileged 之外,升级到 1.25 时,不会对集群中的其他 PSP 进行任何更改。"
陷阱六:依赖 kube-score 之类的静态工具做最终验证
错误做法:CI 流水线里跑一遍 kube-score 或者 kubectl validate,通过了就认为 PSA 不会拒绝。
静态工具只能检查 YAML 的结构性错误,但 PSA 的校验发生在 API Server 侧,和静态检查有本质区别:
- 静态工具不知道你的集群版本(
enforce-version影响校验规则) - 静态工具不知道你的命名空间标签(PSA 的级别取决于命名空间标签,不是 YAML 里的字段)
- 静态工具无法模拟 MutatingAdmissionWebhook(如 Istio、OPA)之后的 Pod spec
- 静态工具无法验证 Helm Chart 的模板条件分支
可靠的做法:在 CI 流水线中增加 kubectl label --dry-run=server。
# CI 流水线片段
- name: PSA 合规性预检
run: |
kubectl label --dry-run=server --overwrite \
ns ${NAMESPACE} \
pod-security.kubernetes.io/enforce=${PSA_LEVEL:-restricted} \
pod-security.kubernetes.io/enforce-version=${PSA_VERSION:-latest}
helm template my-release ./charts/my-app \
--namespace ${NAMESPACE} \
| kubectl apply --dry-run=server -f - 2>&1这个命令不仅会检查 YAML 合法性,还会触发 API Server 侧的 PSA 校验,并返回所有违规细节。只有通过这个检查,才允许合并 PR。
PSA 三档策略的威胁模型对比
| 维度 | privilege | baseline | restricted |
|---|---|---|---|
| 威胁假设 | 信任容器内所有操作 | 攻击者已获得容器内 shell,但未突破运行时隔离 | 攻击者可能已利用内核漏洞或配置缺陷 |
| privileged | 允许 | 禁止 | 禁止 |
| hostNetwork / hostPID / hostIPC | 允许 | 禁止 | 禁止 |
| runAsNonRoot | 不要求 | 不要求(但建议) | 强制 |
| allowPrivilegeEscalation | 不限制 | 强制 false | 强制 false |
| capabilities.drop | 不要求 | 不要求 | 至少包含 ALL |
| seccompProfile | 不限制 | 不限制 | 必须是 RuntimeDefault 或 Localhost |
| hostPorts | 允许 | 允许 | 禁止 |
| hostPath volumes | 允许 | 禁止 | 禁止 |
| AppArmor | 不限制 | 允许 RuntimeDefault/Localhost | 允许 RuntimeDefault/Localhost |
| 适用场景 | 系统组件、Infra Pod | 常规业务服务 | 高安全要求的业务服务、多租户 |
为什么 PSA 不是终点
PSA 取代 PSP 的根本原因不是"功能升级",而是 PSP 的 RBAC 耦合设计导致了权限爆炸和审计盲区。PSA 用命名空间标签替代了 RBAC 绑定,简化了策略的绑定逻辑,代价是丧失了 PSP 对单个 Pod 做差异化控制的能力。如果你的场景需要同一命名空间内的不同 Pod 使用不同安全级别,PSA 不足以满足需求——你需要引入 Kyverno 或 OPA 等策略引擎做补充。
一个经常被忽略的细节:enforce-version 标签的作用。不加版本标签时,PSA 默认使用 latest,即当前集群版本对应的策略规则。如果你在 Kubernetes 1.28 集群上设置 enforce=restricted,然后升级到 1.29,latest 指向的规则集可能发生变化,导致某些之前合规的 Pod 突然被拒绝。显式指定 enforce-version 可以避免版本漂移带来的意外。
迁移前先把 kube-system 等系统命名空间加入豁免列表,设置 enforce-version 到目标版本,用 audit 模式观察至少一个完整发布周期——这是代价最低的陷阱绕行方案。应用层同时把 Pod 模板补齐安全字段,因为 Helm Chart 的 values.yaml 不会自动继承 PSP 的 Mutating 逻辑。
参考资料
- Pod Security Admission - Kubernetes 官方文档
- Pod 安全标准 - Kubernetes 官方文档
- 从 PodSecurityPolicy 迁移到内置的 PodSecurity 准入控制器
- 通过配置内置准入控制器实施 Pod 安全标准
- Mapping PodSecurityPolicies to Pod Security Standards
- EKS PSP 迁移 FAQ - AWS
- TKG PSA 配置指南 - Broadcom
- Use Pod Security Admission in AKS - Azure
- NVIDIA Network Operator PSA 配置要求
- Enforce Pod Security Standards with Namespace Labels"}]