Sooua
登录
返回文章列表
云原生安全··11 分钟阅读

PSA 不是 PSP 的平替:Kubernetes Pod 安全准入迁移中的六个工程陷阱

从 PSP 到 PSA 迁移中,90% 的团队踩过至少一个工程陷阱。本文通过六个真实场景,拆解 PSA 的设计哲学、三档策略的威胁模型、enforce/audit/warn 的行为差异,以及 Helm Chart 模板静态检查的盲区。

一个困惑的星期二凌晨

周二 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,没有 seccompProfilecapabilities.drop 是空的——这些在 PSP 时代被自动注入的字段,到 PSA 时代需要你自己写在 Pod 模板里。

能力PSPPSA
拒绝不符合策略的 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=FailedCreate

Audit 模式会把违规记录到 API Server 审计日志,但不拒绝 Pod;Warn 模式会在 kubectl apply 返回时输出警告信息。建议先让 audit 模式运行至少 3-7 天,覆盖完整的发布周期,确认所有工作负载都合规后再切换到 enforce 模式。

陷阱二:Helm Chart 模板中的安全字段是"隐形"的

错误做法:跑 kubectl get pods -n staging -o yaml 检查所有 Pod,发现 securityContext 都正确,就认为 PSA 不会出问题。

静态检查正在运行的 Pod 是一个误导性极强的做法。原因有两点:

  1. 你看到的 Pod 字段可能是 PSP 注入的,不是 Pod 模板里的。PSP 被移除后,新 Pod 不会继承这些字段。

  2. 大量工作负载的 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: truerunAsUser > 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 不一致:

  1. Istio sidecar 注入:Istio 的 sidecar injector 会将 Proxy 容器注入到 Pod 中,这个注入发生在 MutatingAdmissionWebhook 阶段。如果 Proxy 容器的 securityContextprivilegedcapabilities.add 等不被 restricted 允许的字段,PSA 会拒绝整个 Pod。

  2. Helm Operator 动态注入:某些 Operator(如 Strimzi、Redis Operator)会在 Reconciling 过程中修改 Pod 模板,添加 hostPath volume 或 privileged 容器。

  3. 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-systemistio-systemvelero-system 等。

Kubernetes 系统组件(如 CoreDNS、kube-proxy、CNI 插件)通常需要 hostNetworkprivilegedhostPath 等能力,它们无法在 restricted 级别下运行。随意给系统命名空间设置 restricted 策略会导致集群组件故障。

# 错误:给所有命名空间强制执行 restricted
kubectl label --overwrite ns --all \
  pod-security.kubernetes.io/enforce=restricted

PSA 的豁免机制有两种:

  1. 命名空间级别的豁免:在 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 官方文档要求
  1. 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 三档策略的威胁模型对比

维度privilegebaselinerestricted
威胁假设信任容器内所有操作攻击者已获得容器内 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 逻辑。

参考资料

  1. Pod Security Admission - Kubernetes 官方文档
  2. Pod 安全标准 - Kubernetes 官方文档
  3. 从 PodSecurityPolicy 迁移到内置的 PodSecurity 准入控制器
  4. 通过配置内置准入控制器实施 Pod 安全标准
  5. Mapping PodSecurityPolicies to Pod Security Standards
  6. EKS PSP 迁移 FAQ - AWS
  7. TKG PSA 配置指南 - Broadcom
  8. Use Pod Security Admission in AKS - Azure
  9. NVIDIA Network Operator PSA 配置要求
  10. Enforce Pod Security Standards with Namespace Labels"}]
分享

评论

登录 后参与讨论。

加载中…

相关文章