Your "read-only" monitoring agent can open a root shell in any pod on the node. Not through a CVE — upstream closed the report as working as intended — but through a nodes/proxy GET grant that ships by default in at least 69 widely used Helm charts.
Kubernetes 1.36 finally graduates the architectural answer, fine-grained kubelet API authorization (KEP-2862), to GA with the feature gate locked on. But GA changes nothing by itself: every old nodes/proxy grant keeps working through a compatibility fallback. The upgrade hands your fleet the tool; the migration is still yours to run. Here is the map, the rewrite table, and the five-step playbook.
Why GET means RCE: the WebSocket verb-mapping gap
The kubelet exposes an HTTPS API on every node with endpoints of wildly varying sensitivity: /metrics and /healthz sit next to /exec and /run. Historically, kubelet authorization mapped nearly all of those paths to one coarse RBAC subresource, nodes/proxy. Any DaemonSet that scraped metrics or tailed logs needed the same permission that also authorizes executing commands in any container on the node.
In early 2026, security researcher Graham Helton demonstrated that the situation is worse than over-broad permissions. A service account holding only nodes/proxy with the get verb — the minimal grant routinely handed to monitoring tools — can execute arbitrary commands in any pod on every node it can reach. The mechanism is a mismatch between WebSocket semantics and RBAC verb mapping:
- WebSockets require an initial HTTP
GETwithConnection: Upgradeheaders, even when the operation that follows is a write. - The kubelet sees that
GETto/exec, maps it to the RBAC verbgetonnodes/proxy, and the check passes. - The connection upgrades to a WebSocket, the exec handler takes over, and no second check for
createever happens.
A single websocat command against wss://$NODE_IP:10250/exec/... then returns uid=0(root). Worse, the direct-to-kubelet path never produces a pods/exec audit record — only subjectaccessreviews entries — so the execution is significantly stealthier than going through the API server proxy path, which at least logs the full URI.
Helton reported this through Kubernetes' HackerOne program. The Security Team, after discussion with SIG Auth and SIG Node, closed it as "Won't Fix (Working as Intended)" with no CVE: patching just this path would require brittle double-authorization logic across both kubelet and API server. The long-term answer they pointed to is KEP-2862 — making nodes/proxy obsolete for read-only agents so the dangerous grant simply stops being handed out. Horizon3's February 2026 writeup, which turned the technique into a first-class NodeZero check (H3-2026-0002), puts it bluntly: treat nodes/proxy GET as a high-risk execution capability and harden around it, because the behavior is here to stay.
What 1.36 GA actually ships: the endpoint-to-subresource map
With KubeletFineGrainedAuthz — alpha in 1.32, beta and on-by-default in 1.33, and now GA with the gate locked to enabled in the April 2026 "Haru" release — the kubelet performs a fine-grained authorization check before falling back to nodes/proxy. Each commonly used endpoint gets its own dedicated subresource:
| Kubelet API | Resource | Subresource |
|---|---|---|
/stats/* | nodes | stats |
/metrics/* | nodes | metrics |
/logs/* | nodes | log |
/pods, /runningPods/ | nodes | pods (+ proxy fallback) |
/healthz | nodes | healthz (+ proxy fallback) |
/configz | nodes | configz (+ proxy fallback) |
/spec/* | nodes | spec |
/checkpoint/* | nodes | checkpoint |
everything else (/exec, /run, /attach, /portforward, …) | nodes | proxy |
The dual-check design is what makes the upgrade safe: the kubelet first sends a SubjectAccessReview for the specific subresource, and only if that fails does it retry against nodes/proxy. Existing workloads keep working untouched, the API server's kubelet client is unaffected (it holds nodes/proxy via system:kubelet-api-admin, which 1.36 automatically extends with all nine subresources), and mixed-version fleets degrade gracefully to the proxy fallback.
In practice, the rewrite for a metrics agent is exactly as small as it should be:
# Before: node-level superuser for a metrics scrape
rules:
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["get"]# After: least privilege, exec capability gone
rules:
- apiGroups: [""]
resources: ["nodes/metrics", "nodes/stats"]
verbs: ["get"]That is the whole core of the migration: every agent keeps exactly the endpoints it calls, and loses the /exec path it never needed.
What GA does not fix
Three caveats decide how far this migration actually gets you, and all three come straight from the upstream design:
- No fine-grained equivalent exists for
/exec,/run,/attach, or/portforward. Any workload that legitimately needs those — a debugging sidecar, an operator that shells into pods — still requiresnodes/proxy. GA shrinks the set of proxy-holders; it does not eliminate the grant. - The WebSocket verb-mapping behavior is unchanged. A service account that keeps
nodes/proxy GETafter the upgrade is exactly as exploitable as before. The GA announcement is explicit that this risk persists in the default RBAC of dozens of widely deployed charts. - The fallback means nothing breaks — and nothing is fixed automatically. Because the kubelet retries denied fine-grained checks against
nodes/proxy, every legacy grant continues to authorize exactly what it authorized yesterday. GA is the starting gun for migration, not remediation itself.
Read those together and the conclusion is uncomfortable but clear: upgrading to 1.36 without rewriting RBAC buys you a locked-on feature gate and zero reduction in blast radius. The security win is entirely in the grants you change afterward.
The migration playbook: five steps for a fleet with untrusted tenants
On a Cluster-API-managed fleet running third-party and tenant-adjacent node agents — log shippers, monitoring DaemonSets, platform-owned node tooling — the migration is a bounded project, not a research effort. Work it in this order:
1. Inventory every nodes/proxy holder. ClusterRoles first, then the charts that install them:
kubectl get clusterroles -o json | jq -r '
.items[] | select(.rules[]? | .resources[]? | contains("nodes/proxy"))
| .metadata.name'Then grep the values files of every observability chart you deploy — Prometheus, Grafana, Datadog, Elastic, OpenTelemetry collectors — because the next helm upgrade will happily reinstall the grant you just removed by hand. Helton's scan found 69 public charts shipping nodes/proxy; assume yours is one until proven otherwise.
2. Rewrite each agent to the subresources it actually calls. The mapping for the common agent types:
| Agent type | Endpoints it calls | Least-privilege grant (verb: get) |
|---|---|---|
| Metrics scraper (Prometheus, Datadog agent, OTel) | /metrics/*, /stats/* | nodes/metrics, nodes/stats |
| Log shipper (Fluent Bit, Fluentd) | /logs/*, /pods for metadata | nodes/log, nodes/pods |
| Health checker | /healthz | nodes/healthz |
| Pod inventory / autoscaler-style tooling | /pods, /runningPods/ | nodes/pods |
| Platform-owned node tooling (incl. an MCP server's node-level tools) | only what it calls — commonly /pods, /stats/* | nodes/pods, nodes/stats — never proxy |
| Anything needing exec/attach/portforward | /exec, /attach, … | nodes/proxy on a dedicated service account, documented and audited |
The last row is the honest one: proxy-holders should become a short, named list of workloads that genuinely execute into pods, each on its own service account, instead of the default grant every DaemonSet inherits.
3. Verify per node pool, not per cluster. Confirm the gate is active from any node:
curl -sk --header "Authorization: Bearer $TOKEN" \
https://$NODE_IP:10250/metrics \
| grep 'KubeletFineGrainedAuthz'
# kubernetes_feature_enabled{name="KubeletFineGrainedAuthz",stage="GA"} 1Then roll the rewritten roles one node pool at a time and watch each agent's scrape success before moving on. The proxy fallback is your safety net here in reverse: if a rewritten agent breaks, it means you missed an endpoint it calls, and the fix is adding the subresource — not restoring proxy.
4. Enforce so the grant never comes back. Add an admission policy (Kyverno, OPA Gatekeeper, or your GitOps repo's CI check) that flags or rejects new ClusterRole/Role rules granting nodes/proxy to subjects outside your named exception list. The GA announcement explicitly anticipates policy engines taking this role; Horizon3's remediation guidance says the same. Prevention beats the next audit, because new agents get added constantly and every one of them defaults to the broad grant.
5. Restrict the network path to port 10250 anyway. RBAC is necessary but not sufficient: with a NetworkPolicy (or host firewall) that allows kubelet traffic only from the API server and explicitly authorized monitoring infrastructure, even a future nodes/proxy leak cannot be reached from tenant namespaces. Alert on any unexpected traffic to :10250 from application namespaces — on a multi-tenant fleet, that connection attempt is the attack, caught early.
Where the ecosystem stands
Vendor charts are mid-migration, which is exactly why step 1 says to grep rather than assume. Datadog's operator ships an opt-in fineGrainedAuthorization flag. The AWS observability Helm charts, kindnet, and cluster-autoscaler all carry open issues tracking the switch from nodes/proxy to the fine-grained subresources. OpenFaaS published guidance for its clusterRole: true installs. The pattern is consistent: every maintainer agrees the new model is correct, almost nobody has flipped the default yet. Until your charts do, pin the rewritten roles in your own values and re-check on every upgrade — upstream will not do this for you.
Why a multi-tenant PaaS adopts first
For a single-tenant cluster, a compromised monitoring agent is already game over through a dozen other paths, so scoping kubelet access can feel like hygiene. For a self-hosted platform running untrusted tenant workloads next to shared node agents, the math is different: the DaemonSet that scrapes every node is the single identity whose compromise crosses every tenant boundary at once, and nodes/proxy GET hands that identity unaudited exec into every pod — including other tenants' containers and control-plane-adjacent system pods. Least-privilege kubelet access is one of the few controls that shrinks that blast radius without changing a single workload.
There is also the audit calendar. "Monitoring agents hold node-level exec" is the kind of finding that lands in the middle of a SOC 2 review with a remediation deadline attached. Adopting the GA default now — scoped roles, an admission policy proving no new proxy grants, network restriction on :10250 — turns that future finding into evidence you already hold. The upstream trajectory is unambiguous: fine-grained subresources are the recommended model, policy engines are being pointed at the old grant, and nodes/proxy for read-only agents is on a path from "default" to "deprecated" to "flagged." Platforms that migrate on their own schedule do it in an afternoon per agent; platforms that wait do it under an auditor's deadline.
Kubernetes 1.36 locked the gate on. The only remaining question is whether your fleet's RBAC reflects it.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



