Skip to content
LP
Blog / Cloud Security

Kubernetes hardening from an attacker's viewpoint

CIS Benchmarks tell you what to configure. They don't tell you what an attacker does first when they land a shell in your cluster. Here's the priority order a red teamer walks — pod compromise to cluster admin — and the specific hardening controls that break each step.

LeetProtect Research · ·10 min read

Kubernetes hardening guides read like tax code. Ninety percent of them are correct and nobody reads them. The reason: they’re written as configuration checklists, not as a threat model. The CIS Kubernetes Benchmark has 122 controls. The subset that actually matters when someone lands in your cluster is closer to fifteen. This post is the attacker’s version.

The frame: assume the attacker has a shell in one pod. That’s where the interesting question starts. What can they reach? What controls stop them? Which of the 122 CIS items matter for this step? For the cloud side of the same question, see AWS IAM attack paths — cluster compromise and cloud IAM compromise chain together in most real breaches.

The attacker’s step order

1. Recon the pod

First shell in a container, the attacker enumerates:

  • What identity am I? env | grep -i aws\|azure\|gcp, cat /var/run/secrets/kubernetes.io/serviceaccount/token, curl 169.254.169.254/latest/meta-data/.
  • What can I reach on the network? nslookup kubernetes.default.svc, port-scan the pod CIDR, check for cluster-local services (etcd, kubelet, cloud metadata).
  • What runs in this container? Capabilities (capsh --print), mounted paths (mount), running processes.

This is loud but almost never blocked in the wild. A pod that talks to 169.254.169.254 should be an alert. It usually isn’t.

Hardening that breaks this step:

  • Egress network policies with default-deny to the metadata IP and to kubernetes.default.svc from non-privileged pods.
  • automountServiceAccountToken: false unless the pod actually needs the API.
  • Cloud IMDSv2 enforcement (see AWS IAM post).

2. Read the service-account token

If the pod has a mounted service-account token — most do by default — the attacker uses it against the API server:

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
curl --cacert $CA -H "Authorization: Bearer $TOKEN" \
  https://kubernetes.default.svc/api/v1/namespaces/$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)/pods

If that returns pod data — a very common default — the attacker now enumerates every workload’s environment variables, image, mounted volumes, and (crucially) the service accounts other pods use.

Hardening that breaks this step:

  • automountServiceAccountToken: false at the pod spec, plus the ServiceAccount object.
  • Bind the service account to a Role with permissions scoped to what the workload actually uses. Not get pods. Not list configmaps. The minimum verb set.
  • Use TokenRequest API with short-lived, audience-bound tokens instead of long-lived JWTs.

3. Escalate through RBAC

Even a “read-only” service account can be a stepping stone. The API server enforces exactly what RBAC says — no more, no less. The exploitable patterns:

  • get/list on Secrets in a namespace with cloud credentials → the attacker now has AWS/GCP/Azure keys.
  • create pods in any namespace → the attacker can launch a privileged pod that mounts the host filesystem, then read /etc/kubernetes/pki/ on the node. Every kubelet key. Every etcd cert.
  • bind on ClusterRoles they don’t have — RBAC escalation via ClusterRoleBinding: kubectl create clusterrolebinding pwn --clusterrole=cluster-admin --serviceaccount=default:mine.
  • impersonate verbs on users, groups, or service accounts.
  • update on pod/exec or pod/attach in a namespace running privileged workloads.

Any one of these is game over for the namespace. Several are game over for the cluster.

Hardening that breaks this step:

  • Regularly audit ClusterRole and Role definitions. Look for * verbs, * resources, and bind/impersonate/escalate verbs anywhere.
  • Set --enable-admission-plugins=NodeRestriction,PodSecurity on the API server. Enforce PodSecurity: restricted on all namespaces without a documented exemption.
  • Use OPA Gatekeeper or Kyverno to block ServiceAccounts from being bound to cluster-admin outside a small allow-list.

4. Escape to the node

The classic T1611 — Escape to Host. Now that the attacker can create pods, they launch:

apiVersion: v1
kind: Pod
metadata: {name: escape, namespace: default}
spec:
  hostPID: true
  hostNetwork: true
  containers:
  - name: escape
    image: alpine
    command: ["nsenter", "--target", "1", "--mount", "--uts", "--ipc", "--net", "--pid", "--", "sh"]
    securityContext: {privileged: true}
    volumeMounts: [{name: host, mountPath: /host}]
  volumes: [{name: host, hostPath: {path: /}}]

That pod’s nsenter gives a root shell on the underlying node. From there: kubelet keys, etcd certs, cluster admin. Ten seconds if PodSecurity isn’t enforced.

Hardening that breaks this step:

  • PodSecurity: restricted at the namespace level. Blocks privileged, hostPath, hostPID/Network, capabilities-add, runAsNonRoot enforcement.
  • Or Kyverno / OPA policies that enforce the same intent with better messaging.
  • gVisor or Kata Containers as the runtime for untrusted workloads — turns node escape into a much harder problem.

Note: PodSecurityPolicies are gone since 1.25. If your cluster still relies on them, you have a bigger problem.

5. Pivot to cloud

The moment the attacker is on the node, they can reach:

  • The node’s kubelet identity — a cloud IAM role or managed identity with permissions on the underlying VPC.
  • The cluster’s control-plane cloud credentials, if the node has an over-privileged instance profile.
  • The metadata service without the workload identity restrictions that the pod had.

This is where cluster compromise chains into cloud compromise. In every AWS EKS engagement we run, we test whether the node role has more permissions than the cluster needs. It usually does — because someone gave the CSI driver, autoscaler, or external-dns broad permissions at cluster-role level and never scoped them down.

Hardening that breaks this step:

  • IRSA / Workload Identity / Azure Workload Identity. Bind IAM permissions to service accounts, not nodes. Then the node role has almost nothing.
  • Constrain the node role to only what the kubelet needs (autoscaling, joining the cluster).
  • Never give the node role broad s3:*, ec2:*, or iam:* permissions.

6. Persistence

Once cluster-admin, persistence is trivial and often ignored:

  • ClusterRoleBinding that grants a benign-looking name access to everything.
  • MutatingWebhookConfiguration that injects the attacker’s payload into every new pod.
  • CronJob in kube-system that reissues credentials.
  • ServiceAccount token stored offline for reuse after remediation.

Remediation checklists that don’t rotate every certificate and every JWT after a cluster compromise leave the door open. Assume anything the attacker touched is still owned until proven otherwise.

The fifteen controls that actually matter

Distilled from the 122 CIS items, the ones that break the above chain at the earliest step:

  1. automountServiceAccountToken: false by default
  2. PodSecurity: restricted on every namespace
  3. Egress NetworkPolicy default-deny to metadata endpoints
  4. Egress NetworkPolicy default-deny to kube-apiserver from non-privileged workloads
  5. IRSA / Workload Identity for cloud permissions (never rely on node IAM)
  6. Constrained node role (kubelet-only)
  7. --enable-admission-plugins=NodeRestriction
  8. TokenRequest API with short-lived, audience-bound tokens
  9. Regular RBAC audit — no bind/impersonate/escalate verbs outside audited roles
  10. OPA Gatekeeper / Kyverno on ClusterRoleBinding creation
  11. IMDSv2-required at cloud level
  12. Encrypted etcd + audit logging enabled + logs shipped
  13. Runtime detection (Falco, Tetragon) with alerts on nsenter, hostPath mount from a container, kubectl exec into system pods
  14. AlwaysPullImages admission plugin so image tag reuse can’t inject
  15. Regular privileged-workload inventory — everything with privileged: true, hostPath, hostNetwork, hostPID

Fifteen is the number your defenders should be able to say yes to before you claim the cluster is hardened. If your last cloud assessment produced a report that didn’t map to this chain — the assessor was reading you the CIS Benchmark, not testing your cluster.

What a serious Kubernetes red team engagement looks like

Not a scanner. Not a CIS checklist read-back. An operator lands in a representative pod, walks steps 1–6, and produces a report that tells you exactly which of the fifteen controls above you have, which you’re missing, and — if they got to cluster admin — how they’d persist and pivot to cloud.

If your Kubernetes environment is production and hasn’t seen an assessment shaped like that, it hasn’t been assessed.

> MAPPING
MITRE ATT&CK
T1611: Escape to HostT1613: Container and Resource DiscoveryT1610: Deploy ContainerT1552: Unsecured CredentialsT1053.007: Container Orchestration JobT1078.004: Valid Accounts (Cloud)
TAGS
KubernetesRBACContainer EscapeCISRed Team
[ ENGAGE ]

Ready to test this in your own environment?

Scope an engagement and we'll bring the same rigor to your stack.

Scope an engagement