深入 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,强制覆盖镜像原有的设定。