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 kind --version kubectl version --client
2. Create a multi-node cluster
kind-config.yaml:
kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: worker
kind create cluster --name goat --config kind-config.yaml kubectl get nodes # one control-plane, two workers kubectl cluster-info --context kind-goat
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 cd kubernetes-goat bash setup-kubernetes-goat.sh # deploys all the vulnerable workloads kubectl get pods # watch them reach Running bash access-kubernetes-goat.sh # sets up the kubectl port-forwards
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
kubectl get secret <name> -o jsonpath='{.data}' | jq
# from inside a compromised pod, read the mounted service-account token
kubectl exec -it <pod-name> -- sh
# ...then, inside the pod:
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/namespaceEvery 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 \ --as=system:serviceaccount:default:default # check one specific dangerous verb kubectl auth can-i create pods \ --as=system:serviceaccount:default:default
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 \ | grep -iE 'hostPath|privileged|hostNetwork|hostPID|securityContext' -A2
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 a Job kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml kubectl wait --for=condition=complete job/kube-bench --timeout=120s kubectl logs job/kube-bench # kubescape: posture scan against the NSA/CISA and MITRE frameworks brew install kubescape kubescape scan --format html --output kubescape-report.html
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
kubectl label namespace secure-app \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted
# now try to run a privileged pod there - admission rejects it
kubectl -n secure-app run bad --image=nginx \
--overrides='{"spec":{"containers":[{"name":"bad","image":"nginx","securityContext":{"privileged":true}}]}}' Pod 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
spec:
podSelector: {}
policyTypes: [Ingress, Egress]Important 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
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml
kubectl -n secure-app apply -f default-deny.yaml10. Tear it down
kind delete cluster --name goat
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.
