Cloud Security Wire
AWS Azure GCP RSS
AWSAzureGCP Misconfiguration critical

Ray AI Cluster Security: Hardening Against ShadowRay 2.0 and CVE-2023-48022

Oligo Security's Black Hat USA 2026 disclosure documented over 200,000 internet-exposed Ray servers, a self-propagating botnet using Ray's own orchestration APIs, and 240GB of exfiltrated AI models and source code. If your organisation runs Ray for LLM inference, training, or multi-agent workloads, this guide covers the cloud-specific attack surface and how to close it.

By Cloud Security Wire · ·
#ray#shadowray#CVE-2023-48022#AI-infrastructure#LLM#botnet#cryptojacking#cloud-security#kubernetes#GPU-security#unauthenticated-api#access-control
Critical Severity

This issue has been assessed as critical severity. Review affected configurations immediately.

ShadowRay 2.0 is a direct consequence of a configuration problem that has been documented since 2023. Ray’s Jobs API and dashboard (port 8265 by default) have no authentication. Any host that can reach port 8265 can submit arbitrary Python code to run across the cluster with the permissions of the Ray worker process. CVE-2023-48022 formalised this as a vulnerability. Three years later, Oligo Security presented at Black Hat USA 2026 with a count of over 200,000 internet-exposed Ray servers, a self-propagating botnet that uses Ray’s own scheduling primitives to replicate across clusters, and evidence of 240GB of exfiltrated AI models, source code, and datasets.

This is not a patching problem. There is no patch that closes the exposure in the same way a CVE patch does — because the “vulnerability” is Ray’s design for trusted internal environments, combined with operators exposing it to the internet. The fix is configuration and network controls.

The Exposure Model

Ray runs a cluster with several network-accessible components:

ComponentDefault PortAuthRisk
Ray Dashboard8265NoneRead cluster state, submit jobs
Ray Jobs API8265/api/jobsNoneSubmit arbitrary Python, execute as Ray worker
Ray GCS (Global Control Service)6379NoneCluster metadata, node discovery
Ray Object Store8076NoneRead/write distributed object store
Ray Client10001NoneDirect Python interaction with cluster

The Jobs API is the primary exploit vector. A single HTTP POST to /api/jobs/ with a JSON body containing Python code executes that code across the cluster. No authentication. No token. No configuration change needed by the attacker.

ShadowRay 2.0 extends this by using Ray’s NodeAffinitySchedulingStrategy to distribute malware payloads across every worker node in the cluster after initial access — turning a single API call into full cluster compromise.

Hardening: AWS

Security Group Configuration

The foundational fix: restrict port 8265 and the Ray cluster ports to your VPC CIDR or specific management CIDRs. Nothing else.

resource "aws_security_group_rule" "ray_dashboard_restricted" {
  type              = "ingress"
  from_port         = 8265
  to_port           = 8265
  protocol          = "tcp"
  cidr_blocks       = [var.vpc_cidr]  # Never use 0.0.0.0/0
  security_group_id = aws_security_group.ray_cluster.id
  description       = "Ray dashboard — VPC-internal only"
}

resource "aws_security_group_rule" "ray_gcs_restricted" {
  type              = "ingress"
  from_port         = 6379
  to_port           = 6379
  protocol          = "tcp"
  cidr_blocks       = [var.vpc_cidr]
  security_group_id = aws_security_group.ray_cluster.id
  description       = "Ray GCS — VPC-internal only"
}

# No internet exposure rule — if you find this in your SG, remove it
# NEVER: cidr_blocks = ["0.0.0.0/0"]

IAM Role for Ray Workers

Ray worker nodes typically need S3 access for data and model storage, and may need ECR access to pull container images. Scope their IAM role to the minimum necessary:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::your-ml-data-bucket/*"
    },
    {
      "Effect": "Allow",
      "Action": ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"],
      "Resource": "arn:aws:ecr:region:account-id:repository/ray-workers"
    }
  ]
}

The ShadowRay campaign exfiltrates environment variables including AWS keys. If your Ray workers run with an instance profile that has broad permissions (S3 FullAccess, EC2 FullAccess, or worse), a compromised cluster gives attackers lateral movement into your entire cloud tenant.

Critically: do not store AWS access keys as environment variables in Ray job configurations. Use instance profiles. If you must pass credentials to Ray jobs, use AWS Secrets Manager and fetch them at runtime with short-lived tokens.

VPC Endpoint for S3

Route Ray’s S3 traffic through a VPC endpoint to prevent traffic traversing the public internet and to allow S3 bucket policies scoped to specific VPCs:

resource "aws_vpc_endpoint" "s3" {
  vpc_id       = var.vpc_id
  service_name = "com.amazonaws.${var.region}.s3"
  route_table_ids = [var.private_route_table_id]
}

resource "aws_s3_bucket_policy" "ml_data" {
  bucket = var.ml_data_bucket
  policy = jsonencode({
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = aws_iam_role.ray_worker.arn }
      Action    = ["s3:GetObject", "s3:PutObject"]
      Resource  = "${aws_s3_bucket.ml_data.arn}/*"
      Condition = {
        StringEquals = {
          "aws:sourceVpc" = var.vpc_id
        }
      }
    }]
  })
}

Hardening: Azure

Network Security Group

Equivalent to the AWS security group rule — block port 8265 from anything outside your VNET or specific management subnet:

resource nsgRayCluster 'Microsoft.Network/networkSecurityGroups@2023-04-01' = {
  name: 'nsg-ray-cluster'
  location: resourceGroup().location
  properties: {
    securityRules: [
      {
        name: 'DenyRayDashboardInternet'
        properties: {
          priority: 100
          protocol: 'Tcp'
          access: 'Deny'
          direction: 'Inbound'
          sourceAddressPrefix: 'Internet'
          destinationPortRange: '8265'
          destinationAddressPrefix: '*'
        }
      }
      {
        name: 'AllowRayDashboardVnet'
        properties: {
          priority: 110
          protocol: 'Tcp'
          access: 'Allow'
          direction: 'Inbound'
          sourceAddressPrefix: 'VirtualNetwork'
          destinationPortRange: '8265'
          destinationAddressPrefix: '*'
        }
      }
    ]
  }
}

Managed Identity for Ray Workers

Ray workers on Azure should authenticate to storage and other services via managed identity rather than stored credentials:

# In your Ray task code — use DefaultAzureCredential, not stored keys
from azure.identity import ManagedIdentityCredential
from azure.storage.blob import BlobServiceClient

credential = ManagedIdentityCredential()
blob_client = BlobServiceClient(
    account_url=f"https://{storage_account_name}.blob.core.windows.net",
    credential=credential
)

Hardening: GCP

Firewall Rules

GCP default firewall rules can allow broad ingress. Be explicit:

# firewall-deny-ray-internet.yaml
name: deny-ray-dashboard-from-internet
network: projects/PROJECT_ID/global/networks/VPC_NAME
direction: INGRESS
priority: 500
sourceRanges:
  - "0.0.0.0/0"
targetTags:
  - ray-cluster
denied:
  - IPProtocol: tcp
    ports: ["8265", "6379", "10001"]

Authentication Proxy Pattern

Ray does not natively support authentication on its dashboard or Jobs API. For organisations that need the dashboard accessible outside the VPC (for remote teams), deploy an authenticating reverse proxy in front of it.

Using nginx with OAuth2 Proxy:

server {
    listen 443 ssl;
    server_name ray-dashboard.internal.example.com;
    
    ssl_certificate /etc/ssl/certs/internal.crt;
    ssl_certificate_key /etc/ssl/private/internal.key;

    location /oauth2/ {
        proxy_pass http://oauth2-proxy:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location = /oauth2/auth {
        proxy_pass http://oauth2-proxy:4180;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
    }

    location / {
        auth_request /oauth2/auth;
        error_page 401 = /oauth2/sign_in;
        proxy_pass http://ray-head:8265;
        proxy_set_header Host $host;
    }
}

With oauth2-proxy configured against your identity provider (Entra ID, Google Workspace, Okta), this gates access to the Ray dashboard behind SSO without modifying Ray itself.

Kubernetes Deployments: NetworkPolicy

If you run Ray on Kubernetes (via KubeRay), add a NetworkPolicy to restrict ingress to the Ray head pod:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ray-head-network-policy
  namespace: ray-system
spec:
  podSelector:
    matchLabels:
      ray.io/node-type: head
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ray-system
        - podSelector: {}
      ports:
        - protocol: TCP
          port: 8265
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
      ports:
        - protocol: TCP
          port: 8265

This restricts dashboard access to pods within the ray-system namespace and a monitoring namespace, blocking any cross-namespace or external access.

Indicators of Compromise: ShadowRay 2.0

If you suspect a cluster is already compromised, look for:

Cron jobs:

crontab -l  # per user
cat /etc/cron.d/*
# Look for entries downloading from GitHub/GitLab and piping to bash, running every 15 minutes

Systemd services:

systemctl list-units --type=service | grep -E "dns-filter|health-monitor"
# ShadowRay uses these names for persistence services

Hidden binaries:

find /usr/local/bin /usr/bin /tmp -name ".*" -type f
# Leading-dot files are a common ShadowRay hiding pattern

Outbound miner connections:

ss -tnp | grep -E "3333|4444|14444|45560"
# Common XMRig pool ports
netstat -tnp | grep xmrig

Unexpected Ray jobs in queue:

ray job list  # from within the cluster
# Any jobs with names or entrypoints you don't recognise

CloudTrail / Cloud Audit Detection

In AWS, if a compromised Ray cluster exfiltrates credentials from the instance metadata service and uses them externally, CloudTrail will show API calls from unexpected source IPs:

// AWS CloudTrail via Sentinel
AWSCloudTrail
| where UserIdentityArn contains "ray-worker-role"
| where SourceIPAddress !startswith "10."  // your private range
| where SourceIPAddress !startswith "172.16."
| where EventName in ("GetObject", "ListBuckets", "DescribeInstances", "GetCallerIdentity")
| project TimeGenerated, SourceIPAddress, EventName, UserIdentityArn, Resources
| order by TimeGenerated desc

References

← All Analysis Subscribe via RSS