早上例行过了一遍核心集群的 API Server 审计日志,发现某个业务 Namespace 下出现了一连串被拒绝的 pods/exec 和 pods/create 请求。顺着链路查下去,发现是开发侧为了图方便,直接在一个 CI/CD 专用的 ServiceAccount 上绑定了过于宽松的 RBAC 权限。
在 K8S 环境里,安全边界从来不是靠口头约定的。今天刚好借着这个案例,把 K8S 底层的权限提权路径拆解一下,并谈谈如何通过 Pod Security Standards (PSS) 和 Admission Webhook 构建实质性的防御闭环。
一、RBAC 最小权限的伪命题与提权路径
很多人配置 RBAC 时,觉得只要不给 cluster-admin,不给 secrets 的读取权限就是安全的。但实际上,在 Kubernetes 中,只要拥有创建 Pod 的权限,如果没有严格的准入控制,就等同于拥有了 Node 的 Root 权限,进而可以接管整个集群。
看一眼开发试图下发但被拦截的 Pod YAML:
apiVersion: v1
kind: Pod
metadata:
name: debug-pod
spec:
hostNetwork: true
hostPID: true
containers:
- name: shell
image: alpine:latest
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: host-root
volumes:
- name: host-root
hostPath:
path: /
type: Directory
如果集群只依赖基础的 RBAC 且允许下发上述资源,会发生什么?
-
hostPID: true打破了 PID Namespace 的隔离。 -
privileged: true让容器内的进程获取了 Node 宿主机上的所有 Capabilities。 -
hostPath直接将宿主机的根目录/挂载到了容器的/host。 攻击者进入容器后,只需要执行一条命令:chroot /host nsenter -t 1 -m -u -n -i sh这直接切换到了宿主机 init 进程(PID 1)的 Mount、UTS、Network、IPC 命名空间。此时,攻击者已经拿到了 Node 的 Root 权限,可以随意读取/etc/kubernetes/pki/下的证书,或者复用 kubelet 的凭证与 API Server 通信,完成对整个集群的控制。 这就是为什么 RBAC 的最小权限原则(Least Privilege)必须配合更底层的资源控制。
二、从 PSP 到 PSS:内置的准入拦截机制
早年间我们用 PodSecurityPolicy (PSP) 来防范这种越权,但由于 PSP 的 API 设计过于复杂且容易导致权限混淆,官方在 1.25 版本彻底将其移除,替换为了 Pod Security Admission (PSA/PSS)。
PSS 不是一个独立运行的组件,它本质上是 kube-apiserver 内部的一个 Admission Controller。官方预定义了三个安全等级:Privileged、Baseline、Restricted。
为了在特定的 Namespace 阻断上述提权行为,我通常会在系统层面直接打 Label 强制启用 Restricted 模式:
kubectl label --overwrite ns cicd-pipeline \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
底层拦截逻辑分析:
当 API Server 接收到创建 Pod 的请求时,请求会经历 Authentication -> Authorization (RBAC) -> Mutating Admission -> Object Schema Validation -> Validating Admission 这个标准管道。
PSA 作用于 Validating Admission 阶段。它会提取目标 Namespace 的 Label,当检测到 enforce=restricted 时,会按严格的策略遍历 Pod Spec。只要发现 hostPID: true 或者 privileged: true,直接返回 HTTP 403。
在 API Server 的审计日志中,你可以清晰看到拦截过程产生的记录:
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "Metadata",
"stage": "ResponseComplete",
"requestURI": "/api/v1/namespaces/cicd-pipeline/pods",
"verb": "create",
"user": {
"username": "system:serviceaccount:cicd-pipeline:deployer"
},
"responseStatus": {
"metadata": {},
"status": "Failure",
"reason": "Forbidden",
"code": 403
},
"annotations": {
"pod-security.kubernetes.io/enforce-policy": "restricted"
}
}
三、Admission Webhook:弥补 PSS 的盲区
PSS 解决了 “Pod 自身特权逃逸” 的问题,但它的粒度太粗了。如果我的需求是:
-
强制所有镜像必须从公司内部的私有仓库(例如
harbor.internal.com)拉取。 -
阻止未经授权的用户修改特定的
ConfigMap。 -
动态检查某些资源配置是否符合安全合规标准。 这三个需求 PSS 都做不到,必须通过自定义的
ValidatingAdmissionWebhook来实现。 Webhook 的本质是一个 HTTPS Server。API Server 在 Validating 阶段,会将请求封装成一个AdmissionReview对象,通过 POST 请求发给你的 Webhook,然后根据返回的allowed: true/false决定是否放行。 下面是一个拦截非私有仓库镜像的核心代码片段(Go 语言):
func servevalidate(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
if data, err := io.ReadAll(r.Body); err == nil {
body = data
}
}
// 解析 API Server 发来的 AdmissionReview
ar := v1.AdmissionReview{}
json.Unmarshal(body, &ar)
// 反序列化具体的 Pod 对象
var pod corev1.Pod
json.Unmarshal(ar.Request.Object.Raw, &pod)
allowed := true
var resultMsg string
// 检查镜像前缀
for _, container := range pod.Spec.Containers {
if !strings.HasPrefix(container.Image, "harbor.internal.com/") {
allowed = false
resultMsg = fmt.Sprintf("Image %s is strictly forbidden. Use harbor.internal.com.", container.Image)
break
}
}
// 构造响应
admissionResponse := v1.AdmissionResponse{
UID: ar.Request.UID,
Allowed: allowed,
Result: &metav1.Status{
Message: resultMsg,
},
}
ar.Response = &admissionResponse
respData, _ := json.Marshal(ar)
w.Write(respData)
}
要让这个 Webhook 生效,我们需要向集群提交 ValidatingWebhookConfiguration 配置:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: image-policy-validator
webhooks:
- name: image-policy.k8s.internal
clientConfig:
service:
name: security-webhook
namespace: security-system
path: "/validate"
caBundle:
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system", "security-system"]
四、配置 Webhook 的高危陷阱
在上述 YAML 中,我特意写了两个关键配置,这也是无数人在生产环境踩过大坑的地方:
-
namespaceSelector: 必须排除掉kube-system甚至你部署 Webhook 所在的 Namespace。 -
failurePolicy: Fail: 代表如果 Webhook 服务不可用,API Server 会拒绝所有发往该 Webhook 的请求。 如果你的 Webhook 所在节点宕机,或者证书过期,且你没有排除核心 Namespace,会导致连kube-system下的组件(比如 CoreDNS 扩容、CNI 组件重启)都无法创建 Pod,整个集群陷入死锁。到那时,你连部署一个新的 Webhook 实例去恢复它都做不到,只能通过直接修改 API Server 的静态 Pod 配置文件绕过拦截。 安全体系的构建是一环扣一环的。RBAC 控制谁能访问,PSS 限制能运行什么形态的实体,Admission Webhook 则补齐了业务侧定制化的安全约束。把这三者结合好,才能在日常运维中从容应对各种不规范的操作和潜在的恶意越权。收拾完这堆审计告警,该去跟研发团队对齐新的权限管控流程了。