A subtle (and frankly surprising) Kubernetes authorization behavior has resurfaced as a practical cluster-compromise path: an identity granted nodes/proxy access with an HTTP Get can be leveraged to execute commands in Pods across the cluster—effectively turning what many teams treat as “read-only node telemetry access” into remote code execution (RCE).
This isn’t being treated like a traditional CVE you can patch away. The recommended long-term direction is fine-grained kubelet authorization (KEP-2862 / KubeletFineGrainedAuthz), with upstream commentary pointing to future releases to make broad nodes/proxy less necessary.
References
The New Stack:
Technical deep dives:
- https://grahamhelton.com/blog/nodes-proxy-rce
- https://labs.iximiuz.com/tutorials/nodes-proxy-rce-c9e436a9
What’s happening (in plain English)
The key idea
Kubernetes exposes a “nodes/proxy” capability: where workloads can communicate directly with kubelet endpoints on nodes. Some of those proxied endpoints involve WebSockets (which start as an HTTP GET handshake). In this scenario, authorization ends up effectively being evaluated against the handshake method rather than the real operation that follows—allowing a token with nodes/proxy get resource access to do much more than teams assume.
Why nodes/proxy is especially risky
Kubernetes’ kubelet auth mapping makes this clearer:
- Many kubelet paths map cleanly to subresources like nodes/metrics, nodes/stats, nodes/log
- Anything else tends to fall into the coarse bucket: nodes/proxy (Kubernetes)
In other words, “nodes/proxy” is the “catch-all,” which is exactly what you don’t want floating around in widely-used ServiceAccounts.
Why platform teams should care: observability is a common blast-radius multiplier
This is not an “edge-case permission.” The uncomfortable reality is that many monitoring/telemetry agents and Helm charts request broad node access because it’s convenient and historically “just works.” Multiple write-ups highlight how monitoring stacks become an attack vector when they carry node-level privileges.
So the real risk pattern is:
- A monitoring agent (or its token) is compromised
- The token has nodes/proxy get access
- The attacker pivots to RCE across workloads and targets privileged/system Pods
Prevention with Kyverno: make “dangerous RBAC” unshippable
Kyverno can’t change kubelet internals, but it can prevent the most common failure mode: shipping RBAC that grants nodes/proxy (or worse, nodes/*) broadly.
Policy 1: Block nodes/proxy (and nodes/*) when verbs include get or *
Start in Audit, then move to Enforce after you’ve fixed chart RBAC.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-nodes-proxy
annotations:
policies.kyverno.io/title: "Block Dangerous nodes/proxy GET Permissions"
policies.kyverno.io/category: "Security"
policies.kyverno.io/severity: "high"
policies.kyverno.io/subject: "ClusterRole, Role"
policies.kyverno.io/description: >-
Blocks ClusterRoles and Roles from having nodes/proxy GET permissions which can be exploited
for remote code execution in any Pod. Use fine-grained alternatives like nodes/metrics or nodes/stats.
spec:
validationFailureAction: Audit
background: true
rules:
- name: block-nodes-proxy-get-permissions
match:
any:
- resources:
kinds:
- ClusterRole
- Role
exclude:
any:
# Exclude system roles that legitimately need this access
- resources:
kinds:
- ClusterRole
- Role
names:
- "system:*"
- resources:
kinds:
- ClusterRole
- Role
selector:
matchLabels:
"app.kubernetes.io/managed-by": "kube-apiserver"
- resources:
kinds:
- ClusterRole
- Role
selector:
matchLabels:
"kubernetes.io/bootstrapping": "rbac-defaults"
validate:
message: >-
ClusterRole/Role "{{ request.object.metadata.name }}" contains dangerous permissions:
Either nodes/proxy with GET verb, wildcard resources (*) with GET verb, or nodes/* with GET verb.
This can lead to remote code execution in any Pod. Use fine-grained alternatives like
nodes/metrics, nodes/stats, or specific monitoring endpoints.
deny:
conditions:
any:
# Check if any rule has dangerous combination
- key: "{{ request.object.rules[?contains(resources || `[]`, 'nodes/proxy') && contains(verbs || `[]`, 'get')] }}"
operator: NotEquals
value: []
- key: "{{ request.object.rules[?contains(resources || `[]`, 'nodes/proxy') && contains(verbs || `[]`, '*')] }}"
operator: NotEquals
value: []
- key: "{{ request.object.rules[?contains(resources || `[]`, 'nodes/*') && contains(verbs || `[]`, 'get')] }}"
operator: NotEquals
value: []
- key: "{{ request.object.rules[?contains(resources || `[]`, 'nodes/*') && contains(verbs || `[]`, '*')] }}"
operator: NotEquals
value: []
- key: "{{ request.object.rules[?contains(resources || `[]`, '*') && contains(verbs || `[]`, 'get')] }}"
operator: NotEquals
value: []
- key: "{{ request.object.rules[?contains(resources || `[]`, '*') && contains(verbs || `[]`, '*')] }}"
operator: NotEquals
value: []
Policy 2: Require fine-grained node telemetry access where possible
A pragmatic approach is: allow nodes/metrics but block nodes/proxy.
You can justify this internally because core components already use fine-grained subresources. For example, metrics-server’s RBAC includes nodes/metrics rather than nodes/proxy.
Defense-in-depth best practices (don’t skip these)
1) Inventory who already has nodes/proxy
Run an RBAC audit now (before enforcing policies). Your “oh no” list is typically:
- monitoring / logging / tracing agents
- “platform helper” ServiceAccounts
- legacy troubleshooting roles
- any wildcard nodes/*
2) Prefer Metrics API and fine-grained subresources
Where tools support it, shift to:
- Metrics API (metrics.k8s.io)
- kubelet subresources like nodes/metrics, nodes/stats, nodes/log
3) Network containment: reduce kubelet reachability
Even if RBAC slips, your goal is to reduce the paths a compromised Pod can use to hit kubelets/node IPs. Default-deny egress + explicit allows (DNS, required SaaS endpoints, etc.) meaningfully reduces blast radius. This can easily be done by using Kyverno resource generation policy.
4) Audit logging (know what your audit can see)
Kubernetes auditing records requests to the Kubernetes API (via the control plane). If an attacker’s activity is happening via kubelet paths in ways that don’t look like pods/exec, your existing detections may not light up the way you expect—so you want both: audit logging plus RBAC guardrails to stop risky permissions at the source.
5) Keep privileged landing zones small
If “exec anywhere” is possible, the difference between a bad day and a catastrophic day is whether attackers can land inside privileged/system Pods. Enforce Pod Security Standards (Baseline/Restricted) and minimize privileged DaemonSets wherever you can.
A short “how-to” using nctl ai: generate the Kyverno policy + tests, then iterate fast
The easiest way to operationalize this is to treat it like any other policy rollout: generate → test locally → deploy in Audit → enforce. Download nctl and launch it. You can learn more about how nctl works in our documentation.
» nctl ai
👋 Hi, I am your Nirmata AI Platform Engineering Assistant!
I can help you automate security, compliance, and operational best practices across your clusters and pipelines.
💡 Here are some tasks I can do for you, or ask anything:
▶ scan clusters
▶ generate policies and tests
▶ optimize costs
💡 type 'help' to see commands for working in nctl ai
>
Step 1: Generate policy + tests with one prompt
Next, here is a sample prompt:
You are a Kyverno policy author. Create Kyverno policies and kyverno-cli tests to mitigate Kubernetes nodes/proxy GET risk.
Requirements:
1) Write a ClusterPolicy named restrict-nodes-proxy.
2) Deny any Role or ClusterRole that grants access to resources: nodes/proxy OR nodes/* AND verbs include: get OR *.
3) The validation message must recommend alternatives: nodes/metrics, nodes/stats, nodes/log, or metrics.k8s.io.
4) Output a kyverno-cli test suite including:
- failing ClusterRole granting nodes/proxy with verb get
- failing ClusterRole granting nodes/* with verb *
- passing ClusterRole that uses nodes/metrics only
5) Output as separate YAML documents with filenames in comments:
- policies/restrict-nodes-proxy.yaml
- tests/bad-nodes-proxy-get.yaml
- tests/bad-nodes-wildcard.yaml
- tests/good-nodes-metrics.yaml
- tests/kyverno-test.yaml
No placeholders. Ensure kyverno-test.yaml references exact policy and rule names.
Step 2: Save files into a tiny test harness
nodes-proxy-guardrails/
policies/
restrict-nodes-proxy.yaml
tests/
bad-nodes-proxy-get.yaml
bad-nodes-wildcard.yaml
good-nodes-metrics.yaml
kyverno-test.yaml
Step 3: Run tests locally
You can also run tests in nctl ai
Prompt:
> run the tests and make sure they pass
You will see something like this:
🏃 Running Kyverno tests
📫 Tool Output [run_kyverno_tests]:
┌────┬──────┬──────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┬────────┐
│ ID │ Pass │ Policy │ Resource │ Reason │
├────┼──────┼──────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┼────────┤
│ 1 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/Role/default/safe-role │ Ok │
│ 2 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/ClusterRole/default/safe-cluster-role │ Ok │
│ 3 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/Role/kube-system/nodes-proxy-get-role │ Ok │
│ 4 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/ClusterRole/default/nodes-proxy-wildcard-role │ Ok │
│ 5 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/ClusterRole/default/nodes-star-get-role │ Ok │
│ 6 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/Role/monitoring/nodes-star-wildcard-role │ Ok │
│ 7 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/ClusterRole/default/mixed-dangerous-role │ Ok │
│ 8 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/Role/default/nodes-proxy-other-verbs │ Ok │
│ 9 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/ClusterRole/default/nodes-star-other-verbs │ Ok │
│ 10 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/ClusterRole/default/multiple-rules-with-violation │ Ok │
│ 11 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/Role/default/empty-rules-role │ Ok │
│ 12 │ ✅ │ restrict-nodes-proxy │ rbac.authorization.k8s.io/v1/ClusterRole/default/metrics-api-role │ Ok │
└────┴──────┴──────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┴────────┘
📈 Summary: 12 passed, 0 failed (Total: 12 tests)
✅ All tests passed successfully!
Test Results Summary:
• 12 tests passed, 0 failed
• Policy correctly blocks dangerous RBAC permissions:
• ❌ nodes/proxy with get verb (failed as expected)
• ❌ nodes/* with * verb (failed as expected)
• ❌ Mixed roles containing prohibited permissions (failed as expected)
• Policy allows safe alternatives:
• ✅ nodes/metrics , nodes/stats , nodes/log (passed as expected)
• ✅ metrics.k8s.io API group resources (passed as expected)
• ✅ Other verbs on nodes/proxy like list , watch (passed as expected)
The restrict-nodes-proxy policy is working correctly - it successfully mitigates Kubernetes nodes/proxy GET risks by denying dangerous RBAC configurations while allowing safer monitoring alternatives.
If you have kyverno CLI:
kyverno test . -v 2
Expected:
- bad RBAC cases fail
- good RBAC case passes
Step 4: Iterate using real RBAC from your cluster
Grab an actual ClusterRole from a monitoring chart and feed it into nctl ai:
Prompt:
Given this ClusterRole YAML, rewrite it to remove nodes/proxy while preserving monitoring functionality using fine-grained alternatives where possible. Also add a kyverno-cli test that ensures the old ClusterRole fails and the rewritten ClusterRole passes. <PASTE CLUSTERROLE YAML HERE>
Step 5: Roll out safely (Audit → Enforce)
- Deploy Kyverno policy in Audit for 1–2 weeks
- Fix offending charts/roles (or isolate them behind explicit, reviewed exceptions)
- Flip policy to Enforce and prevent regression
Additional Precautions
You can also use Kyverno to automatically generate network policies that block attackers from accessing the kubelet. This way, only tools that require network access are allowed to talk to the kubelet.
Conclusion
The nodes/proxy GET → WebSocket behavior is a sharp reminder that “read-only” Kubernetes RBAC isn’t always read-only—and that widely granted telemetry permissions can become a path to cluster-wide command execution. The most immediate and effective mitigation is to eliminate risky RBAC at the source: use Kyverno to block nodes/proxy (and nodes/*) grants with get or *, and steer teams toward fine-grained alternatives like nodes/metrics, nodes/stats, nodes/log, and the Metrics API.
When issues like this break, speed matters—but so does correctness. nctl ai can accelerate your response by generating Kyverno policies with runnable kyverno-cli tests, so you can validate guardrails locally and roll them out safely (Audit → Enforce) across clusters.
You can find the policy and test examples here:
Try nctl ai today—use it to generate a restrict-nodes-proxy policy + tests in minutes, validate it in CI, and prevent this class of “telemetry-to-RCE” risk from creeping back into your clusters.
