Kubernetes内置的Pod安全准入控制,按命名空间强制安全基线
Pod Security Admission(PSA) 是 Kubernetes 内置的准入控制器,只需给命名空间打一个标签,就能把 Pod Security Standards(PSS) 施加到该空间内新建的 Pod 上。它从 v1.23 起默认启用(beta),v1.25 起 GA。
PSA 是 PodSecurityPolicy(PSP) 的官方替代品。PSP 在 v1.21 被弃用、v1.25 被彻底移除,新集群上 kubectl get psp 只会报 the server doesn't have a resource type "psp"。如果升级前没有迁移,PSP 的约束会静默消失 —— Pod 照常运行,但保护伞没了。
它的设计取舍很明确:不做自定义策略。PSA 只提供三档固定基线,靠标签声明,不引入新的 API 对象,也不需要 RBAC 绑定(这正是 PSP 最大的坑:权限模型绕、配错反而静默失效)。需要镜像仓库白名单、必填资源限制这类细粒度规则,应交给 Kyverno、OPA Gatekeeper 等策略引擎,PSA 负责兜底。
三档级别(level),限制逐级叠加:
| 级别 | 说明 |
|---|---|
privileged |
完全不受限,等同于不启用 |
baseline |
阻止已知提权手段,兼容绝大多数常规工作负载 |
restricted |
当前 Pod 加固最佳实践,显著收紧 |
baseline 的核心是「不要设置危险字段」:hostNetwork/hostPID/hostIPC 必须为空或 false,privileged 必须为空或 false,禁止 hostPath 卷,capabilities.add 只允许一份固定白名单(不含 SYS_ADMIN、NET_ADMIN 等),seccompProfile.type 不得显式为 Unconfined。
restricted 在 baseline 之上追加四条硬性要求,且对 所有 containers、initContainers、ephemeralContainers 逐一检查:
allowPrivilegeEscalation 必须为 false
runAsNonRoot 必须为 true
capabilities.drop 必须包含 ALL(只能再加回 NET_BIND_SERVICE)
seccompProfile.type 必须为 RuntimeDefault 或 Localhost(不能留空)
三种模式(mode),彼此完全独立,可以给同一个命名空间配不同的级别:
| 模式 | 行为 |
|---|---|
enforce |
违规直接拒绝创建请求 |
audit |
放行,但在审计日志里给事件打上 pod-security.kubernetes.io/audit-violations 注解 |
warn |
放行,但向发起者返回 Warning 提示 |
kubectl label --overwrite ns <命名空间> pod-security.kubernetes.io/enforce=<级别>
kubectl label --overwrite ns <命名空间> pod-security.kubernetes.io/audit=<级别>
kubectl label --overwrite ns <命名空间> pod-security.kubernetes.io/warn=<级别>
kubectl label --overwrite ns <命名空间> pod-security.kubernetes.io/enforce-version=<版本>
标签键固定为 pod-security.kubernetes.io/<模式>,值是级别;<模式>-version 用来把该模式的策略定义钉在某个 Kubernetes 小版本(如 v1.37)或 latest。
apiVersion: v1
kind: Namespace
metadata:
name: my-baseline-namespace
labels:
# 当前实际拦截的级别
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.37
# audit/warn 指向「下一步要达到的级别」,用来预先观察影响
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.37
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.37
这个「enforce 落后一档、audit/warn 领先一档」的写法是官方推荐姿势:拦截范围保持保守,同时持续收集「如果切到 restricted 会打掉哪些工作负载」。
从零到 enforce 最稳的路径是四步走,不要一步到位:
# 1. 全集群先只开 audit + warn,不设 enforce
kubectl label --overwrite ns --all \
pod-security.kubernetes.io/audit=baseline \
pod-security.kubernetes.io/warn=baseline
# 2. 找出还没被评估过的命名空间(不含 enforce 标签的)
kubectl get namespaces --selector='!pod-security.kubernetes.io/enforce'
# 3. 切换前先用服务端 dry-run 预演,它会照常执行检查并返回警告
kubectl label --dry-run=server --overwrite ns --all \
pod-security.kubernetes.io/enforce=baseline
# 4. 逐个命名空间落地,并钉住策略版本
kubectl label --overwrite ns my-namespace \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.37
被 enforce 拦截时,报错会逐条列出原因,直接照着改即可:
Error from server (Forbidden): error when creating "pod.yaml": pods "nginx" is forbidden:
violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false
(container "nginx" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type
to "RuntimeDefault" or "Localhost")
warn 模式下同样的文本会以 Warning: 开头出现在 kubectl 输出里,请求本身成功。
常见违规与对应修法:
allowPrivilegeEscalation != false → 容器加 securityContext.allowPrivilegeEscalation: false
unrestricted capabilities → 容器加 capabilities.drop: ["ALL"]
runAsNonRoot != true → Pod 或容器加 runAsNonRoot: true
seccompProfile → 加 seccompProfile.type: RuntimeDefault
privileged → 去掉 privileged: true,按需 add 单个 capability
hostPath volumes → 改用 PVC、emptyDir 或 projected 卷
hostNetwork / hostPID / hostIPC → 改为 false,或整体迁到 privileged 命名空间
标签只能配级别,豁免名单和默认级别必须写在 apiserver 的 AdmissionConfiguration 文件里,通过 --admission-control-config-file 传入:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1 # v1.25+;1.23/1.24 用 v1beta1
kind: PodSecurityConfiguration
defaults: # 命名空间没打标签时生效
enforce: "privileged"
enforce-version: "latest"
audit: "privileged"
audit-version: "latest"
warn: "privileged"
warn-version: "latest"
exemptions:
usernames: [] # 已认证用户名,豁免
runtimeClasses: [] # 运行时类名,豁免
namespaces: [] # 命名空间,整体豁免
默认文件里三档默认值都是 privileged,即没打标签的命名空间不受任何约束。要全集群兜底,应把 defaults.enforce 改成 baseline。
PSA 会向 apiserver 暴露三个指标,适合做「违规趋势」看板:
pod_security_evaluations_total 实际执行的策略评估次数(不含被豁免的)
pod_security_exemptions_total 命中豁免的请求数
pod_security_errors_total 评估出错次数(出错时可能回退到最新 restricted 配置)
enforce 不作用于工作负载对象,只作用于 Pod。创建 Deployment 时不会被拦,是它的控制器建 Pod 时才失败 —— 表现为 Deployment 反复报 ReplicaFailure,而不是 kubectl apply 报错。audit 和 warn 则会作用于 Deployment/Job 等工作负载,方便提前发现。enforce: baseline 而不配 audit/warn,并不会产生任何警告;配了 warn: restricted 也不代表拦截变严了。上生产前务必确认三者的级别是你想要的。restricted 的默认值全是「不满足即违规」。runAsNonRoot 不写、capabilities 整块不写、seccompProfile 不写,全部算违规 —— 不存在「不写就走运行时默认」的宽容。runAsNonRoot: true 只检查有效用户,不检查镜像里的 USER。runAsUser: 0 会直接被拒;kubelet 还会额外拒绝启动「镜像 USER 解析为 root」的容器。enforce 级别或版本时,apiserver 会重新评估该命名空间内已有的 Pod 并把违规以警告形式返回,但不会杀进程。已有工作负载需要自行滚动重建才会合规。usernames 通常拦不住真实流量。绝大多数 Pod 由控制器创建,豁免某个终端用户只在他直接建 Pod 时生效;真正要豁免的是 system:serviceaccount:kube-system:replicaset-controller 这类控制器身份,但那等于豁免了所有能建 Deployment 的人,极危险。privileged。不写 AdmissionConfiguration 又不打标签的命名空间,等于完全没有保护,别以为「装了 PSA 就安全了」。baseline 并非完全无害。它仍允许 hostPort、hostPath 之外的多数常规配置,以及 runAsUser: 0,只是堵住了已知提权路径;要防容器内提权必须上 restricted。*-version 不写就等于 latest,策略会随集群升级自动跟进。生产环境建议显式钉住版本,否则一次小版本升级可能突然引入新的检查项,把原本能跑的工作负载拦在门外。runAsNonRoot)在 PSA 中不存在,不满足就是拒绝,必须自己把字段补齐。hostNetwork/privileged 的组件,应放进独立的 privileged 命名空间,并通过 exemptions.namespaces 或标签显式豁免,而不是为了它们把整个业务空间的 enforce 降档。kubectl — Kubernetes集群管理工具securitycontext — Pod 与容器安全上下文namespace — 命名空间管理rbac — 基于角色的访问控制kube-apiserver — 准入控制器与 apiserver 参数