A security researcher needed exactly one RBAC permission to run id as root inside any pod on a reachable node: nodes/proxy with the get verb — the same "read-only" grant handed to practically every Prometheus node-exporter, log shipper, and health-check sidecar in production. No pods/exec permission. No create verb. Just:
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)Disclosed publicly on January 26, 2026, the bug is a mismatch between how WebSocket connections establish themselves and how the kubelet checks permissions. A WebSocket handshake is, at the protocol level, an HTTP GET — and the kubelet authorizes that GET as a read (get verb) without a follow-up check for the create verb a real POST /exec would require. Anyone holding nodes/proxy GET could open a WebSocket straight to the kubelet's exec endpoint and skip the RBAC check entirely.
Kubernetes' security team closed it without a CVE: "working as intended." Their reasoning wasn't that it's harmless — it's that nodes/proxy was never scoped narrowly enough for the fix to be a patch. The actual fix was already in flight as KEP-2862, and on April 22, 2026, it shipped: Fine-Grained Kubelet API Authorization graduated to GA in Kubernetes v1.36 ("Haru").
If you run a self-hosted PaaS that grants tenants their own kubectl exec or log access on shared nodes, this release is the one to read closely — but not for the reason the changelog headline suggests. GA fixes the monitoring-agent blast radius. It does not fix the tenant-exec blast radius. Same permission-implies-more-than-you-granted pattern as the bug above — just still open where it matters most for a multi-tenant platform.
What actually changed: nodes/proxy stops being one giant permission
Before v1.36, kubelet API authorization was almost comically coarse. Every kubelet HTTP endpoint except four (/stats, /metrics, /logs, /spec) mapped to a single RBAC subresource: nodes/proxy. That one permission covered health checks, container stats, configz, pods, and — because it fell into the catch-all — exec, attach, run, and portforward too. Grant nodes/proxy get to a monitoring agent so it could scrape kubelet stats, and you'd also handed it a path to that WebSocket exploit above.
The KubeletFineGrainedAuthz feature gate has been working through the standard maturity ladder: alpha in v1.32, beta (default-on) in v1.33, and now GA in v1.36 with the gate locked enabled. GA splits several previously-bundled endpoints into their own subresources:
| Kubelet API | Old subresource | New subresource (v1.36 GA) |
|---|---|---|
/stats/* | nodes/proxy* | nodes/stats |
/metrics/* | nodes/proxy* | nodes/metrics |
/logs/* | nodes/proxy* | nodes/log |
/spec/* | nodes/proxy* | nodes/spec |
/checkpoint/* | nodes/proxy | nodes/checkpoint |
/pods, /runningPods/ | nodes/proxy | nodes/pods |
/healthz | nodes/proxy | nodes/healthz |
/configz | nodes/proxy | nodes/configz |
/exec, /attach, /run, /portforward | nodes/proxy | still nodes/proxy |
*(stats, metrics, logs, spec already had dedicated subresources pre-GA; GA's new additions are checkpoint, pods, healthz, and configz.)
The kubelet does a dual check: it authorizes against the specific subresource first, and only falls back to nodes/proxy if that fails. So existing bindings that already grant nodes/proxy keep working unchanged — nothing breaks on upgrade — but a cluster admin can now write a monitoring role that never touches nodes/proxy at all:
# Before: one permission, way more than a scraper needs
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-agent
rules:
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["get"]
---
# After: scoped to exactly what it reads
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-agent
rules:
- apiGroups: [""]
resources: ["nodes/metrics", "nodes/stats"]
verbs: ["get"]That's a real, shippable win. A compromised or misconfigured node-exporter DaemonSet granted only nodes/metrics can no longer pivot into arbitrary command execution on the node — the WebSocket trick above has nothing to attach to, because the fine-grained check for /exec still routes to nodes/proxy, and this role was never granted it. For the observability half of the fleet, GA genuinely closes the gap the January disclosure exposed.
The gap it deliberately leaves open: exec, attach, and run stay bundled
Look again at the last row of that table. /exec, /attach, /run, and /portforward — the endpoints that let a caller run commands inside a container — did not get their own subresources in GA. They're still all nodes/proxy, exactly as coarse as before.
This isn't an oversight the next KEP will clean up. It's a deliberate design choice, stated in the KEP itself: read-only endpoints (configz, healthz, pods) were worth splitting apart because a caller might legitimately need one without the others, but the code-execution endpoints were left as one bundle because "there is no use case where having just one of those makes sense." If you can exec into a container, the KEP authors reasoned, scoping out attach or portforward separately buys nothing — you already have a shell.
That reasoning is sound for the problem GA set out to solve — least-privilege for tooling that only ever needs to read. But it means the TODO question a self-hosted PaaS operator actually has — does fine-grained kubelet authz shrink the blast radius of a tenant's kubectl exec grant? — has a flat no for an answer. A tenant role that includes exec today carries exactly the same nodes/proxy-shaped reach it always did: the same endpoint group that, until this release, also covered stats and health checks for monitoring agents. GA moved the monitoring agents out of that bundle. It left tenant exec sitting exactly where the WebSocket bug found it.
Put differently: the fix for "a permission I thought was narrow turned out to unlock code execution" shipped for the scraper use case and not for the exec use case — because for exec, there was never a narrower permission to grant in the first place. nodes/proxy for an exec-granted tenant isn't a bug waiting on a KEP. It's the floor.
What has to carry the weight instead: audit trail, not RBAC scoping
If kubelet authorization won't shrink the exec permission, the honest move for a platform granting tenants — or AI agents — their own exec/log credentials is to stop expecting RBAC to do that job and put the control where it actually works: the API server's audit log, not the kubelet's authorizer.
Two concrete pieces matter here, and neither shipped in v1.36 — they're standing infrastructure a platform has to run regardless of kubelet authz maturity:
1. Audit policy at RequestResponse for exec-shaped subresources. Kubernetes' audit policy supports per-resource logging levels, and the documented recommendation is to log pods/exec, pods/attach, and pods/portforward at RequestResponse — capturing the full request body, not just metadata:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward"]That gets you who requested an exec session and when in the API server's own log, independent of whatever the kubelet's RBAC check allowed.
2. Session attribution, because kubectl exec doesn't carry it by default. The API-server audit record tells you who opened the session — it doesn't tell you what they typed once inside. And by default, the requesting user's identity isn't injected into the exec session itself, which makes attributing in-session commands to a specific caller hard after the fact. The pattern that closes this — used by tools like the Security Profiles Operator's audit webhook — is a mutating admission webhook on exec/attach requests that injects the request's authenticated UID as an environment variable into the session, so whatever's watching the container (an in-node audit sidecar, a syscall monitor) can tie actions back to a caller instead of just "root, from somewhere."
Neither of these is new tooling nodes/proxy GA obsoletes — they're the mechanism that was already necessary before this release and remains necessary after it. GA changes what a monitoring agent needs; it doesn't change what a shell-granting platform needs to keep running.
What a self-hosted PaaS should actually do with this release
The upgrade path is genuinely low-effort for the part GA does cover, and worth doing:
- Audit existing
nodes/proxygrants against the new subresource table above. Any role that only ever calls/metrics,/stats,/logs,/healthz,/configz, or/podscan be rewritten to the specific subresource and losenodes/proxyentirely. - Don't relax exec-granting roles because of this release. A tenant or agent role that includes
pods/execneeds the same audit-policy and session-attribution posture it needed in v1.35 — GA changed nothing about that surface. - Treat the audit trail as the actual control, not the RBAC grant. If you're building toward AI agents holding exec/log credentials for their own sandboxes — deploying, checking logs, rolling back their own workload without a human approving each step — the thing that makes that safe to grant isn't a narrower kubelet permission that doesn't exist yet. It's knowing, after the fact and in full, exactly what the agent did with the shell it was given.
Bex runs tenant apps as pods on Cluster-API-managed nodes you own, and the same distinction applies directly to its own roadmap toward agent-operated exec and log access: fine-grained kubelet authorization is worth adopting for every scraper and log shipper in the fleet today, and it's simply not the control that makes handing a credential to an autonomous agent safe. That control is the audit log.
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.
Sources
- Kubernetes v1.36: Fine-Grained Kubelet API Authorization Graduates to GA
- Kubernetes v1.36: ハル (Haru) release announcement
- Kubelet authentication/authorization reference docs
- KEP-2862: Fine-grained Kubelet API Authorization
- Kubernetes Remote Code Execution Via Nodes/Proxy GET Permission — grahamhelton.com
- Your Monitoring Stack Just Became an RCE Vector — Edera
- When "Read-Only" Isn't: K8s nodes/proxy GET to RCE — Horizon3.ai
- Kubernetes Auditing task documentation
- Auditing user activity in pods and nodes with the Security-Profiles-Operator — CNCF