Cloud Security Office Hours Banner

Kubernetes security lab: kind + Kubernetes Goat

Learn Kubernetes attack and defense on a real cluster that runs entirely on your laptop. Build it with kind, break it with Kubernetes Goat, then harden it - all for $0.

Jump to the build Back to Home Lab

· · Vendor-neutral

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.

On this page

  1. What you will build
  2. Prerequisites
  3. Step-by-step
  4. Common mistakes
  5. Where next

What you will build

Prerequisites

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/namespace

Every 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.yaml

10. 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

Where next