kubectl proxy’s Filter Gap and Your kubelet /run Attack Surface
PR #143187 (and its associated backport PRs) closes a gap in the safety filter of kubectl proxy that let a local process or a web page run commands inside your containers through the kubelet's /run endpoint. It is a one-line change to a default regex, and there is no CVE or CVSS score attached, but it can still be a sharp edge.
Like the recent kubectl cp CVE, this bug lives on the client, not the cluster. Nothing about your control plane changes the risk. What matters is who runs kubectl proxy, with which credentials, and what else can reach their localhost.
How Does kubectl proxy Work?
kubectl proxy opens an unauthenticated HTTP listener, by default on 127.0.0.1:8001, and forwards everything it accepts to the API server using your kubeconfig credentials. A request takes this path:
- A client on your machine sends a plain HTTP request to
localhost:8001. No token, no client certificate. - The proxy checks the request against 3 default filters: an allowed-host list (
localhost, 127.0.0.1, [::1]), a rejected-path list, and a rejected-method list. - If the request passes, the proxy attaches your credentials and forwards it to the API server.
- For a
nodes/<node>/proxy/...path, the API server forwards it again to that node's kubelet, which handles the remainder of the path as its own API.
Step 1 is where the problem is. The listener cannot tell your terminal from any other process on the box, or from a browser tab that fires a cross-origin POST at localhost. Whoever reaches the port borrows your identity.
That makes step 2 the real security boundary. The path filter exists to keep the most dangerous endpoints, the ones that run commands in containers, out of reach of anything that is not you. Until this PR, it blocked pod exec and attach but not the kubelet's /run endpoint reached through node proxy, which does the same job in a single POST.
The History of the kubectl proxy Filter
The filter is old and deliberately simple. Its own escape hatch, --disable-filter, warns in its help text that turning it off can leave you open to XSRF attacks on an accessible port. The defaults reflect what was considered dangerous when they were written: pod exec and attach, matched by DefaultPathRejectRE.
What that list missed is that the API server offers a second road to the same place. The nodes/proxy subresource passes requests straight through to the kubelet, and the kubelet exposes its own command-execution endpoints: /exec, /attach, /portForward, and /run. Pod exec is just the polite front door to the kubelet's /exec.
How Serious Is This Really?
The kubelet's /run endpoint takes a single POST with the command in a query string and returns its output. Using this, a browser can send a cross-origin request without the normal CORS preflight, and the proxy's host check passes because the Host header really is localhost. The response is opaque to the attacking page, but the command still runs.
3 things have to line up for that to hurt you:
- You are running
kubectl proxyon its default filters. - Your credentials allow
nodes/proxyon the target node.cluster-adminand many platform-admin roles do. - The attacker knows, or can guess, a node, namespace, pod, and container name. In practice,
kube-systemis an obvious thing to guess as that is where most hosted Kubernetes solutions deploy their node management pods.
That is a narrow window, which is likely why this shipped as a routine bug fix rather than a CVE. But it is the same pattern as the kubectl cp issue: a client tool quietly extends cluster trust to whatever can talk to it. It is very low effort to close, so it got closed.
The main concern is that this can unintentionally leak elevated permissions to a cluster for a local LLM agent, rather than a remote actor. A remote actor could conceivably use a cross-site attack, but this would be unlikely to succeed. If accomplished, though, it would have the potential to grant a reverse shell proxy on the cluster, probably with root or other elevated permissions since kube-system is a likely target, but the likelihood is low. I consider the true danger that an LLM would notice its availability and use it to accomplish tasks it thinks are valuable on a cluster that the user doesn’t know it now has access to.
Reading the Diff
The whole fix is one appended pattern in staging/src/k8s.io/kubectl/pkg/proxy/proxy_server.go:
// DefaultPathRejectRE is the default set of paths to reject.
-DefaultPathRejectRE = "^/api/.*/pods/.*/exec,^/api/.*/pods/.*/attach"
+DefaultPathRejectRE = "^/api/.*/pods/.*/exec,^/api/.*/pods/.*/attach,^/api/.*/nodes/.*/proxy/run"A new TestAccept case pins the behavior: a POST from localhost to /api/v1/nodes/somenode/proxy/run/somenamespace/somepod/somecontainer?cmd=id must now be rejected. The PR is 12 lines added and 1 removed, was approved by kubectl approver @soltysh, and merged with all 13 checks green.
Am I Affected?
You are exposed if you run kubectl proxy with default filters, using credentials that can create on nodes/proxy, on a machine where you also browse the web or run code you don't fully trust. Every OS is affected, and the server version does not matter, since the filter lives in the client. Check yours with:
kubectl version --client
As of this writing the fix is on master for v1.38 and with backport PRs for 1.35–1.37 that have not been merged yet. Until you can upgrade:
- Pass the fixed regex yourself.
kubectl proxy --reject-paths='^/api/.*/pods/.*/exec,^/api/.*/pods/.*/attach,^/api/.*/nodes/.*/proxy/run'. Note that setting--reject-pathsreplaces the defaults, so keep theexecandattachentries. - Go further if you like. Adding
^/api/.*/nodes/.*/proxyblocks node proxy entirely, which also covers theexec,attach, andportForward. - Shrink the credentials. Run the proxy under a context that lacks
nodes/proxy, and audit RBAC for who holds it. - Don't leave it running. Start it when you need it and stop it when you're done; never pair it with
--disable-filteror a non-loopback--address.
The Bigger Lesson: Localhost Is Cluster Attack Surface
In short, kubectl proxy turns your kubeconfig into an unauthenticated service on localhost, and its only protection is a list of paths it refuses to forward. That list knew about pod exec and attach but not the kubelet's /run behind node proxy.
The takeaway rhymes with kubectl cp. Client tools that hold cluster credentials should treat every input, whether a tar stream from a container or an HTTP request on a loopback port, as attacker controlled. Your laptop is part of the cluster's attack surface, and the tools on it deserve the same patching and scrutiny as anything running in production.
FAQ
What does PR #143187 change in kubectl proxy?
It appends one pattern to the default reject filter (DefaultPathRejectRE) so kubectl proxy now blocks POSTs to the kubelet’s /run endpoint reached through nodes/proxy, the path that can run commands in a container. The fix lands in v1.38, with backports to 1.35 through 1.37 pending.
Is there a CVE for the kubectl proxy /run issue?
No CVE or CVSS score was assigned. Maintainers shipped it as a routine bug fix because exploitation needs a narrow set of conditions to line up, but it can still extend your cluster credentials to anything that can reach the proxy’s unauthenticated localhost port.
Who is affected by the kubectl proxy filter gap?
Anyone running kubectl proxy on default filters, with credentials that allow nodes/proxy on a node, on a machine where they also browse the web or run untrusted code. The client version matters, not the server, since the filter lives in the client. Check yours with kubectl version --client.
How do I mitigate the kubectl proxy /run gap before upgrading?
Pass the fixed regex yourself via --reject-paths, keeping the exec and attach entries, or block node proxy entirely with the pattern ^/api/./nodes/./proxy. Run the proxy under a context without nodes/proxy, stop it when idle, and never pair it with --disable-filter or a non-loopback address.

-3.avif)