Back to Blog
Red Team Kubernetes — Attack Path

Lateral Movement
on Kubernetes

One compromised pod is not the incident. It is the beachhead. In a flat cluster network, the attacker scans services, harvests service account context, pivots through trusted workloads, and keeps moving until RBAC, NetworkPolicy, or runtime detection finally says no.

Riad DAHMANIk8sec Security ResearchMay 202616 min readtag-red-team
Lateral Movement on Kubernetes attack path overview
Overview — compromised workload to sensitive asset through pod network reachability, service discovery, token abuse, and weak segmentation.

Why Lateral Movement Works in Kubernetes

The uncomfortable truth: Kubernetes networking is permissive by default. A pod can usually talk to every other pod, every ClusterIP service, CoreDNS, and the Kubernetes API server. That model is excellent for distributed systems. It is also excellent for an attacker who has landed inside one container.

This is not a CVE. It is worse than a CVE. It is intended behavior that becomes exploitable when platform teams do not define trust boundaries.

The attacker does not need cluster-admin on step one. They need execution inside one workload. After that, the cluster itself provides the reconnaissance surface: DNS names, service routes, exposed ports, mounted tokens, metadata endpoints, and predictable internal naming.

Flat east-west traffic
Without default-deny NetworkPolicy, compromised pods can probe internal services across namespaces.
Auto-mounted tokens
Service account JWTs give the attacker an authenticated API identity, even when the app never needed Kubernetes API access.
DNS-driven recon
CoreDNS turns service discovery into an attacker feature. Namespaces, service names, and ports become the internal map.
Policy breaks the chain
NetworkPolicy, scoped RBAC, token hardening, and runtime alerts turn movement into failed attempts instead of silent pivots.

Threat Model: What the Attacker Actually Does

Assume the attacker has command execution inside one low-value pod: a vulnerable web app, exposed debug endpoint, poisoned image, CI runner, SSRF primitive, or leaked kubeconfig. The workload runs as non-root. The container does not have Docker socket access. That still does not make it safe.

1. FootholdRCE inside app pod
2. EnumerateDNS, routes, API, env
3. Test RBACcan-i, token review
4. Pivotinternal HTTP, DB, queues
5. Impactsecrets, data, deploy abuse

The attacker moves by abusing what the cluster already trusts: service-to-service connectivity, internal service names, service account bindings, and missing egress controls. No noisy kernel exploit is required.

The Kill Chain

Step 1 — Confirm API server reachability

# From inside a compromised pod
curl -sk https://kubernetes.default.svc.cluster.local/version
nslookup kubernetes.default.svc.cluster.local

If this returns the API server version, the pod can reach the control plane endpoint. That is normal. It also means the attacker can test the mounted token.

Step 2 — Inspect the mounted identity

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

If the response lists pods, services, secrets, configmaps, or jobs, the attacker has a live cluster identity. If it returns forbidden, the attacker still has network reachability and DNS recon.

Step 3 — Map internal services

# DNS brute force from the pod
for ns in default prod staging monitoring kube-system; do
  for svc in api backend redis postgres mysql vault grafana prometheus; do
    nslookup ${svc}.${ns}.svc.cluster.local 2>/dev/null | grep -q Address && \
      echo "FOUND ${svc}.${ns}.svc.cluster.local"
  done
done

Step 4 — Probe sensitive workloads

for target in \
  vault.security.svc.cluster.local:8200 \
  prometheus.monitoring.svc.cluster.local:9090 \
  grafana.monitoring.svc.cluster.local:3000 \
  redis.cache.svc.cluster.local:6379; do
  timeout 2 bash -c "cat < /dev/null > /dev/tcp/${target/:/ }" 2>/dev/null && echo "OPEN $target"
done

Default-deny egress should make this boring. In many clusters, it lights up internal services that were never meant to face an untrusted workload.

What the Attacker Enumerates

TargetWhy it mattersDefensive failure
ServiceAccount tokenAuthenticated API identityautomountServiceAccountToken: true everywhere
Services and endpointsInternal routing mapNo NetworkPolicy default deny
ConfigMapsURLs, feature flags, internal hostnamesOverbroad read RBAC
SecretsDatabase passwords, cloud credentialsClusterRoleBinding to app identities
Metrics stackEnvironment leakage, service topologyMonitoring exposed inside cluster
CI/CD runnersPath to source, registry, deployment tokensRunner pod allowed broad egress

The highest-risk targets are not always production databases. CI runners, internal admin panels, monitoring stacks, and service mesh control planes often provide better pivot value.

Detection Commands and Queries

You detect lateral movement by looking for behavior that normal app pods should not perform: service discovery bursts, API probing, unusual DNS lookups, pod-to-pod scans, and connections into namespaces the workload has no business touching.

Hunt for pods with API access they do not need
kubectl get pods -A -o json | jq -r '
  .items[] |
  select(.spec.automountServiceAccountToken != false) |
  [.metadata.namespace,.metadata.name,.spec.serviceAccountName] | @tsv'
Find service accounts with dangerous verbs
kubectl get clusterrole,role -A -o yaml | grep -E "resources:|verbs:|secrets|pods/exec|impersonate|bind|escalate" -n
Check whether a compromised identity can list secrets
kubectl auth can-i list secrets \
  --as=system:serviceaccount:<namespace>:<serviceaccount> \
  -n <namespace>
Find namespaces with zero NetworkPolicies
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  count=$(kubectl get networkpolicy -n "$ns" --no-headers 2>/dev/null | wc -l)
  [ "$count" -eq 0 ] && echo "NO NETWORKPOLICY: $ns"
done

Hardening: Break the Movement Path

The goal is not to make compromise impossible. The goal is to make one compromised pod stay one compromised pod.

1. Start with default deny ingress and egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

2. Allow only required frontend to backend flow

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

3. Permit DNS, then nothing else by default

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

4. Disable service account tokens by default

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: production
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      automountServiceAccountToken: false
      serviceAccountName: app-sa

5. Scope RBAC to the exact workload need

# Bad: application identity can read all secrets
kubectl auth can-i list secrets \
  --as=system:serviceaccount:production:app-sa -A

# Expected answer for most app pods: no

Do not bind app identities to broad ClusterRoles. Avoid wildcard verbs and wildcard resources. Never grant pods/exec, secrets, impersonate, bind, or escalate to runtime workloads unless you can defend the blast radius.

Red Team Validation Checklist

ControlTestExpected result
Default denyProbe random service from app podTimeout
DNS-only egressResolve services, then connect to themDNS works, traffic blocked
Token hardeningRead service account token pathFile missing for apps that do not need API
RBAC minimumkubectl auth can-i list secretsNo
Runtime controlsRun nslookup, curl, API probe from an app podBlocked, logged, or justified by exception
Namespace isolationConnect from dev namespace to prod serviceTimeout

Final Word

Most clusters are not breached because Kubernetes is exotic. They are breached because the internal network is trusted like a private LAN from 2008. A compromised pod should hit a wall. In too many clusters, it gets a map, a token, DNS, and a clean route to production. Lateral movement is not a malware feature. It is what your cluster allows after the first mistake.

Riad DAHMANI — k8sec Security Research
k8sec maps Kubernetes attack paths across RBAC, service accounts, pod reachability, and runtime signals — so lateral movement is visible before it becomes cluster compromise.

Explore k8sec Platform