Time: ~3 hours · Difficulty: Intermediate · Stack: Docker · kind · kubectl · Kubernetes Goat · kube-bench · kubescape
You can learn the overwhelming majority of Kubernetes attack and defense on your laptop, for free. kind ("Kubernetes IN Docker") spins up a real, conformant cluster inside Docker containers in about thirty seconds. Kubernetes Goat is a deliberately vulnerable cluster full of realistic scenarios - leaked secrets, SSRF, container escape, over-permissive RBAC. This walkthrough builds the cluster, walks several Goat scenarios hands-on, then flips to defense: you baseline the cluster with kube-bench and kubescape and lock a namespace down with Pod Security admission and NetworkPolicies.
Laptop only. Kubernetes Goat is intentionally exploitable. Run it on a local kind cluster you can delete - never on a shared, cloud, or production cluster, and never expose its ports beyond localhost.
What you will build
- A multi-node kind cluster you can create and destroy at will.
- Kubernetes Goat deployed and reachable through local port-forwards.
- Hands-on completion of several attack scenarios, with the exact commands.
- A CIS benchmark (kube-bench) and posture (kubescape) baseline of the cluster.
- A hardened namespace: default-deny NetworkPolicy plus restricted Pod Security.
Prerequisites
- Docker - kind runs each node as a container.
- kubectl - the Kubernetes CLI.
- kind - installed below.
- git - to clone Kubernetes Goat.
- Skim the Kubernetes security reference first for the threat-model context - what RBAC, admission control, and the service-account token actually are.
Step-by-step
1. Install kind and kubectl
# macOS via Homebrew (see kubernetes.io / kind.sigs.k8s.io for Linux and Windows) brew install kubectl kind # kubectl = the cluster client; kind = "Kubernetes IN Docker", the cluster itself kind --version # confirm kind installed kubectl version --client # --client = print only the local CLI version, don't try to reach a cluster yet
2. Create a multi-node cluster
kind-config.yaml:
kind: Cluster # this file describes a kind cluster apiVersion: kind.x-k8s.io/v1alpha4 # the kind config schema version (not the Kubernetes version) nodes: # each list entry becomes one Docker container running a full kubelet - role: control-plane # runs the API server, scheduler, etcd - the cluster's brain - role: worker # runs your workloads (pods) - role: worker # a second worker, so you can watch pods get scheduled across nodes
kind create cluster --name goat --config kind-config.yaml # build the 3-node cluster from the file above (~30s) kubectl get nodes # list nodes; expect one control-plane and two workers, all "Ready" kubectl cluster-info --context kind-goat # --context picks this cluster; prints the API-server URL to confirm you're wired up
Each "node" is a Docker container running a full kubelet. A multi-node cluster lets you observe pod scheduling and NetworkPolicy behavior that a single node hides.
3. Deploy Kubernetes Goat
git clone https://github.com/madhuakula/kubernetes-goat.git # download the deliberately-vulnerable scenarios cd kubernetes-goat bash setup-kubernetes-goat.sh # deploy every vulnerable workload into your cluster kubectl get pods # list pods; re-run until they all show STATUS = Running bash access-kubernetes-goat.sh # open kubectl port-forwards so you can reach the scenarios from your browser
With the port-forwards running, open http://localhost:1234 for the Goat scenario hub. Each scenario has a short brief and a hint page.
4. Scenario: secrets exposed to a pod
Walk the "Sensitive keys in codebases" and "DIND (Docker-in-Docker) exploitation" scenarios. The core lesson is how much a single compromised pod can see:
# what secrets can the default service account list?
kubectl get secrets # list Secret objects in the current namespace
kubectl get secret <name> -o jsonpath='{.data}' | jq # -o jsonpath pulls out just the .data map; pipe to jq to read it
# (values are base64 - `| base64 -d` to decode a field)
# from inside a compromised pod, read the mounted service-account token
kubectl exec -it <pod-name> -- sh # exec a shell in the pod; -it = interactive TTY, everything after -- runs inside
# ...then, inside the pod, read the token Kubernetes auto-mounts for API calls:
cat /var/run/secrets/kubernetes.io/serviceaccount/token # the JWT the pod uses to authenticate to the API server
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace # which namespace this pod (and token) belongs toEvery pod gets a service-account token mounted at that path by default. If the pod is compromised and its service account is over-permissive, the attacker inherits those API rights. That single fact drives most in-cluster privilege escalation.
5. Scenario: map the RBAC blast radius
# what can this service account actually do? kubectl auth can-i --list \ # --list = dump every verb/resource this identity is allowed --as=system:serviceaccount:default:default # --as = impersonate; format is system:serviceaccount:<namespace>:<name> # check one specific dangerous verb kubectl auth can-i create pods \ # asks a yes/no question: can this identity create pods? --as=system:serviceaccount:default:default # a "yes" here from a pod's own SA is a real privilege-escalation finding
kubectl auth can-i is the single most useful RBAC command. Running it as a service account shows you the exact blast radius of a pod compromise. If a default service account can create pods or get secrets cluster-wide, that is your finding.
6. Scenario: spot a container-escape path
# inspect a pod spec for the classic escape enablers kubectl get pod <pod-name> -o yaml \ # -o yaml = dump the pod's full spec | grep -iE 'hostPath|privileged|hostNetwork|hostPID|securityContext' -A2 # -i case-insensitive, -E regex, -A2 show 2 lines of context
A hostPath mount of /, privileged: true, or hostPID: true each turns a container compromise into node compromise. Kubernetes Goat's "container escape to the host system" scenario walks the full exploitation; the grep above is how you find the same red flags in any real pod spec.
7. Baseline the cluster with kube-bench and kubescape
# kube-bench: runs the CIS Kubernetes Benchmark as an in-cluster Job kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml # -f = apply the manifest straight from the URL kubectl wait --for=condition=complete job/kube-bench --timeout=120s # block until the Job finishes (or 120s passes) kubectl logs job/kube-bench # the benchmark results are written to the Job's logs # kubescape: posture scan mapped to the NSA/CISA and MITRE ATT&CK frameworks brew install kubescape kubescape scan --format html --output kubescape-report.html # scan the live cluster and write a shareable HTML report
kube-bench grades the cluster against the CIS benchmark control by control; kubescape maps findings to attack frameworks and produces a shareable report. Trivy can also scan the whole cluster in one shot: trivy k8s --report summary cluster. Reading these reports - deciding what is a real risk versus benchmark noise on a kind cluster - is the actual skill.
8. Harden a namespace: Pod Security admission
kubectl create namespace secure-app # a fresh namespace to apply the policy to
kubectl label namespace secure-app \ # Pod Security admission is switched on via namespace labels
pod-security.kubernetes.io/enforce=restricted \ # enforce = actually block pods that violate the "restricted" profile
pod-security.kubernetes.io/warn=restricted # warn = also print a warning to whoever applies a bad pod
# now try to run a privileged pod there - admission rejects it before it schedules
kubectl -n secure-app run bad --image=nginx \ # -n = target the labeled namespace
--overrides='{"spec":{"containers":[{"name":"bad","image":"nginx","securityContext":{"privileged":true}}]}}' # patch in privileged:true - the exact thing "restricted" forbidsPod Security admission is built into modern Kubernetes. The restricted profile blocks privileged containers, host namespaces, and hostPath mounts - the exact things you exploited in step 6. Watch the API server reject the bad pod with a clear policy violation.
9. Harden the network: default-deny
default-deny.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: secure-app # this policy only governs pods in the secure-app namespace
spec:
podSelector: {} # {} = an empty selector matches EVERY pod in the namespace
policyTypes: [Ingress, Egress] # govern both inbound and outbound traffic...
# ...and because no allow rules follow, both are denied by defaultImportant caveat most tutorials skip: kind's default CNI (kindnet) does not enforce NetworkPolicy, so the policy above will appear to do nothing. To actually see traffic get denied, recreate the cluster with the default CNI disabled and install Calico or Cilium:
# add `networking: {disableDefaultCNI: true}` to kind-config.yaml, then:
kind create cluster --name goat --config kind-config.yaml # rebuild the cluster with NO default network plugin
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml # install Calico, a CNI that DOES enforce NetworkPolicy
kubectl -n secure-app apply -f default-deny.yaml # re-apply the policy; now traffic actually gets blocked10. Tear it down
kind delete cluster --name goat # destroy the cluster and its Docker containers - complete, instant, and free
Instant, complete, and free. Recreate the whole lab in thirty seconds whenever you want a clean slate.
Common mistakes
- Running Kubernetes Goat on a real or shared cluster. It is deliberately vulnerable - laptop kind only.
- Expecting NetworkPolicy to work under kindnet. You need Calico or Cilium for enforcement.
- Skipping
kubectl auth can-i --list. It is the fastest way to understand what a compromised pod can reach. - Treating kube-bench output as a to-do list. Many controls are noise on a throwaway kind cluster; judgment about what matters is the point.
- Leaving the cluster running. It costs nothing but eats RAM -
kind delete clusterwhen done.
Where next
- Kubernetes security reference - the concepts behind every command here.
- Container security - image scanning and the escape threat model.
- Free AWS lab with LocalStack - the same $0 approach for cloud APIs.
- Cloud pentesting and the CTF directory for more targets.
