标签: Pod Security Admission

  • 深入 K8S 容器提权排查:hostPath 逃逸引发的 Node 接管与 Pod Security Admission 拦截实战

    排查某次 Node 被恶意接管事件发现,业务线侧漏的 ServiceAccount 凭据被利用,通过创建挂载宿主机根目录的特权 Pod 实现了 chroot 逃逸。本文直击 K8S 权限管控盲区,彻底解析从 RBAC 最小权限到 Pod Security Admission (PSA) 拦截,再到 OPA Gatekeeper 细粒度校验的防御链路。

    事故现场:一条 yaml 引发的宿主机沦陷

    某次排查集群异常高负载时,监控显示 Node node-192-168-10-55 上的 sshd 进程出现异常登录,sys CPU 持续飙升。登录审计日志(/var/log/audit/audit.log)追踪,发现该节点上被下发了一个未知的 Pod。

    还原攻击者留下的 Payload,这是一个典型的 hostPath 逃逸模型:

    apiVersion: v1
    kind: Pod
    metadata:
      name: debug-helper
      namespace: dev-team-a
    spec:
      hostNetwork: true
      hostPID: true
      containers:
      - name: root-shell
        image: alpine:3.18
        securityContext:
          privileged: true
        command: ["nsenter", "-t", "1", "-m", "-u", "-n", "-i", "sh", "-c", "echo 'ssh-rsa AAAAB3N...' >> /host/root/.ssh/authorized_keys && sleep infinity"]
        volumeMounts:
        - mountPath: /host
          name: host-root
      volumes:
      - name: host-root
        hostPath:
          path: /
          type: Directory
    

    该 Pod 利用 hostPIDnsenter 直接切入宿主机 1 号进程的 Namespace,并通过 hostPath 将恶意 SSH 公钥写入了 Node 节点的 /root/.ssh/authorized_keys。由于直接复用了宿主机网络(hostNetwork),攻击者绕过了所有的 CNI 隔离策略,直接通过 SSH 拿下了该 Node 的 Root 权限。

    为什么原生的 RBAC 拦不住 hostPath 逃逸?

    很多运维认为配好 RBAC 就能高枕无忧,这是对 K8S 认证授权机制最大的误解。

    查看涉事 Namespace 的 RBAC 配置:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: dev-pod-manager
      namespace: dev-team-a
    rules:
    - apiGroups: [""]
      resources: ["pods", "pods/log", "pods/exec"]
      verbs: ["create", "get", "list", "delete"]
    

    RBAC (Role-Based Access Control) 的作用域在 API Server 请求生命周期的 Authz (授权) 阶段,它只校验“谁(Who)能对什么资源(What)执行什么动作(Verb)”。在这个案例中,开发账号确实拥有 create pods 的权限。

    但是,RBAC 无法解析资源的 Payload(负载内容)。它不关心你要创建的 Pod 是一个普通的 Nginx,还是一个挂载了宿主机 /etc 目录的特权核弹。要拦截恶意 Payload,必须在 API Server 的 Admission Control(准入控制) 阶段下功夫。

    实施第一道防线:Pod Security Admission (PSA)

    在 K8S v1.25+ 中,PodSecurityPolicy (PSP) 已被彻底移除,取而代之的是内置的 Pod Security Admission (PSA)。PSA 实现了官方定义的 Pod Security Standards (PSS),分为三个等级:PrivilegedBaselineRestricted

    要阻断上述逃逸,最快且最原生的方式是在 Namespace 级别强制启用 Restricted(严格)或 Baseline(基线)策略。

    执行以下命令为目标 Namespace 打上 PSA 标签:

    kubectl label namespace dev-team-a \
      pod-security.kubernetes.io/enforce=restricted \
      pod-security.kubernetes.io/enforce-version=latest \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest
    

    再次尝试下发之前的恶意 Pod,API Server 会在 Validating 阶段直接阻断并返回标准的 403 报错:

    Error from server (Forbidden): error when creating "evil-pod.yaml": pods "debug-helper" is forbidden: violates PodSecurity "restricted:latest": 
    privileged (container "root-shell" must not set securityContext.privileged=true), 
    host namespaces (hostNetwork=true, hostPID=true), 
    hostPath volumes (volume "host-root")
    

    底层机制:当请求到达 API Server,完成 RBAC 鉴权后,会进入 PodSecurity Admission Controller。它会读取所在 Namespace 的 labels,根据定义的 PSS 等级去校验 Pod Spec 中的 securityContextvolumes 等字段,一旦发现违规属性即刻拒绝写入 etcd。

    实施第二道防线:基于 OPA Gatekeeper 的细粒度准入

    PSA 的缺点在于颗粒度太粗。在真实业务场景中,某些特殊的 DaemonSet(如 Promtail 日志采集、CSI 存储插件)确实需要 hostPath。如果我们一刀切开启 Restricted,业务会大面积瘫痪。此时,我们需要基于 Webhook 的细粒度准入控制器(如 OPA Gatekeeper v3.14+)。

    通过 Rego 语言编写策略,我们可以实现:“禁止使用 hostPath,除非该 Pod 属于特定的 ServiceAccount 且挂载特定路径”。

    1. 部署 ConstraintTemplate (定义规则模板)

    apiVersion: templates.gatekeeper.sh/v1
    kind: ConstraintTemplate
    metadata:
      name: k8sblockhostpath
    spec:
      crd:
        spec:
          names:
            kind: K8sBlockHostPath
      targets:
        - target: admission.k8s.gatekeeper.sh
          rego: |
            package k8sblockhostpath
    
            violation[{"msg": msg}] {
              volume := input.review.object.spec.volumes[_]
              has_key(volume, "hostPath")
              not is_exempt(input.review)
              msg := sprintf("HostPath volume is forbidden: %v", [volume.name])
            }
    
            has_key(obj, k) {
              _ = obj[k]
            }
    
            # 允许 kube-system 命名空间下的请求豁免
            is_exempt(review) {
              review.object.metadata.namespace == "kube-system"
            }
    

    2. 下发 Constraint (绑定策略)

    apiVersion: constraints.gatekeeper.sh/v1beta1
    kind: K8sBlockHostPath
    metadata:
      name: block-hostpath-all-namespaces
    spec:
      match:
        kinds:
          - apiGroups: [""]
            kinds: ["Pod"]
    

    原理剖析:Gatekeeper 以 ValidatingWebhookConfiguration 注册到集群。当 API Server 处理 Pod 创建请求时,会发起 HTTPS POST 请求将 AdmissionReview 结构体发送给 Gatekeeper。Gatekeeper 将 JSON 数据喂给内部的 OPA 引擎执行 Rego 脚本校验,如果 violation 规则命中,则向 API Server 返回 Allowed: false 并附带 msg

    防御性加固最佳实践

    在 20 年的架构和排障经历中,我见过太多因权限配置不当引发的集群雪崩和安全事故。针对 K8S 安全,请将以下几条刻在运维基线上:

    1. AutomountServiceAccountToken = false:默认禁止 Pod 自动挂载 SA Token。90% 的业务 Pod 根本不需要和 API Server 通信,直接在 Pod/ServiceAccount 层级关闭它: yaml apiVersion: v1 kind: ServiceAccount metadata: name: default automountServiceAccountToken: false
    2. AppArmor/Seccomp 默认开启:在 kubelet 层面或 Pod SecurityContext 中强制开启 RuntimeDefault seccomp profile,从内核层面阉割非必要的 Syscall。

    3. No-Root 运行:强制要求开发将镜像内的用户改为普通用户,并在 Pod 的 securityContext 中强制声明 runAsNonRoot: true,结合 allowPrivilegeEscalation: false 锁死提权路径。

    常见问题

    Q1: PSS restricted 模式导致合法的基础设施组件(如 Promtail, CSI Node)无法启动怎么办? A1: 基础设施组件通常部署在独立的 Namespace(如 monitoring, kube-system)。PSA 策略是基于 Namespace 打标的,你可以为这些特定的 Namespace 设置 pod-security.kubernetes.io/enforce=privileged,并在集群层级通过 RBAC 严格限制谁有权限在这些特权 Namespace 中创建资源。

    Q2: 如何在不影响现有业务的情况下,平滑推行 PSS 策略? A2: 使用 PSA 的 auditwarn 模式,而不是直接 enforcekubectl label ns dev pod-security.kubernetes.io/warn=restricted pod-security.kubernetes.io/audit=restricted 这样违规的 Pod 依然可以创建,但会在 kube-apiserver 的 Audit Log 中产生告警,并在客户端 kubectl 抛出 Warning。通过聚合分析 Audit Log,揪出不合规的业务端,推动改造后再切为 enforce

    Q3: 为什么配置了 Mutating Webhook 给 Pod 注入 runAsNonRoot: true,但 Pod 依然以 Root 运行? A3: 这通常是因为镜像 Dockerfile 的 USER 指令依然是 root(UID 0)。runAsNonRoot: true 只是一个校验指令,它在 Kubelet 启动容器前会检查 UID,如果是 0 就会直接报错启动失败(Error: container has runAsNonRoot and image will run as root)。要真正改变运行用户,你的 Mutating Webhook 应该注入具体的 runAsUser: 1000,强制覆盖镜像原有的设定。