排查某次 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 利用 hostPID 和 nsenter 直接切入宿主机 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),分为三个等级:Privileged、Baseline、Restricted。
要阻断上述逃逸,最快且最原生的方式是在 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 中的 securityContext、volumes 等字段,一旦发现违规属性即刻拒绝写入 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 安全,请将以下几条刻在运维基线上:
- AutomountServiceAccountToken = false:默认禁止 Pod 自动挂载 SA Token。90% 的业务 Pod 根本不需要和 API Server 通信,直接在 Pod/ServiceAccount 层级关闭它:
yaml apiVersion: v1 kind: ServiceAccount metadata: name: default automountServiceAccountToken: false -
AppArmor/Seccomp 默认开启:在
kubelet层面或 Pod SecurityContext 中强制开启RuntimeDefaultseccomp profile,从内核层面阉割非必要的 Syscall。 -
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 的 audit 和 warn 模式,而不是直接 enforce。
kubectl 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,强制覆盖镜像原有的设定。