Automating Kubernetes Goat on Raspberry Pi with Ansible
Previously, I walked through manually deploying Kubernetes Goat onto my Raspberry Pi Kubernetes cluster built with ClusterHAT and K3s.
That worked well, but after rebuilding the cluster several times while experimenting with:
- Traefik
- NFS storage
- Node labels
- Security tooling
- K3s upgrades
…I quickly realised I wanted a repeatable deployment process.
That’s where Ansible came in.
This follow-up takes the earlier manual deployment and automates the entire process:
- Deploy Kubernetes Goat
- Configure Traefik
- Expose scenarios externally
- Configure allowlists
- Restart services
- Validate accessibility
The result is a reusable Kubernetes security lab that can be rebuilt almost instantly.
A Quick Warning About Rebuilding Kubernetes Clusters
Before going further, it’s important to note:
You generally should not rebuild Kubernetes clusters regularly in production or long-lived environments.
Frequent rebuilding can:
- Disrupt workloads
- Break persistent storage
- Cause certificate or token issues
- Lose cluster state
- Introduce configuration drift
In my case, this cluster is:
- A home lab
- Disposable
- Used for experimentation
- Rebuilt frequently for testing
That makes automation extremely valuable.
For reusable or production clusters, infrastructure should usually be:
- Maintained incrementally
- Upgraded carefully
- Managed declaratively over time
This automation is designed for rapid lab deployment and recovery—not operational production Kubernetes.
Why Automate Kubernetes Goat?
Kubernetes Goat is intentionally vulnerable.
That means I regularly:
- Break it
- Reset it
- Reconfigure networking
- Experiment with scans
- Test detection tooling
- Try new ingress approaches
Being able to redeploy everything from scratch with a single command saves a huge amount of time.
Instead of:
- manually cloning repositories
- reapplying manifests
- configuring Traefik
- rebuilding middleware
- exposing services
…I can now run:
ansible-playbook traefik-kubernetes-goat.yaml…and rebuild the environment automatically.
What the Playbook Automates
The playbook handles:
Kubernetes Goat Deployment
It:
- clones the Kubernetes Goat repository
- runs the setup scripts
- deploys all vulnerable workloads
Traefik CRD Installation
Traefik CRDs are required for:
IngressRouteMiddleware
Without them, Traefik custom resources fail.
The playbook installs them automatically.
Traefik EntryPoints
The deployment dynamically creates:
- ports
1230-1240 - dedicated Traefik entrypoints
- hostPort bindings
This exposes each scenario externally.
IP Allowlist Middleware
Because Kubernetes Goat is intentionally vulnerable, access is restricted using:
- internal IP ranges
- Traefik middleware
- source range filtering
This keeps the environment isolated inside the lab network.
IngressRoute Creation
Each service receives:
- a dedicated ingress route
- a dedicated external port
- direct mapping to the backend service
This makes testing dramatically easier.
Why External Exposure Matters
One of the most useful aspects of Kubernetes Goat is integrating external tooling.
Exposing the scenarios allows:
- OWASP ZAP
- Burp Suite
- Nikto
- Nmap
- Trivy
- kube-hunter
- Custom scripts
…to interact directly with the vulnerable applications.
This turns the Raspberry Pi cluster into a miniature Kubernetes security testing environment.
HostPort Challenges on K3s
One issue I ran into was that:
- Traefik entrypoints existed
- Kubernetes services existed
- IngressRoutes existed
…but the ports still returned connection refused.
The problem was that K3s Traefik does not automatically bind arbitrary ports on the host network.
The solution was adding:
hostPort: 1230for each service entrypoint.
Without this, the services were only reachable internally.
Once added, the Raspberry Pi controller correctly listened on:
123012311232- etc.
Why This Is Useful for Home Labs
This kind of automation works particularly well for:
- Raspberry Pi clusters
- Disposable Kubernetes labs
- Security testing environments
- Kubernetes learning
- Classroom demos
- Rapid experimentation
A full rebuild becomes:
- predictable
- reproducible
- quick
And when something inevitably breaks, recovery is painless.
Things I Would Improve Later
A few areas I’d likely improve in the future:
Move to GitOps
Using:
- Gitlab
- ArgoCD
- Flux
…would make deployments cleaner and more Kubernetes-native.
Add TLS
Right now this is internal-only HTTP.
Adding:
- cert-manager
- self-signed certs
- local PKI
…would improve realism. Traefik could also be used to configure the certificates as well.
Dynamic Service Discovery
Currently the service mappings are static.
Automatically generating routes from Kubernetes labels would be cleaner.
Add Monitoring and Detection
This cluster would pair extremely well with:
- Falco
- Grafana
- Loki
- Prometheus
- OpenTelemetry
Especially during active testing.
Final Thoughts
This project started as:
- a Raspberry Pi cluster
- a Kubernetes experiment
- a security playground
It gradually evolved into a fully automated disposable Kubernetes lab.
That’s one of the things I enjoy most about home lab environments:
they naturally grow alongside your interests.
Automating Kubernetes Goat with Ansible makes rebuilding the cluster nearly effortless and encourages experimentation without fear of breaking things permanently.
And honestly, there’s still something deeply entertaining about watching a tiny Raspberry Pi cluster run intentionally vulnerable Kubernetes workloads while external scanners hammer away at it.
Full Ansible Playbook
Below is the complete playbook used to automate the deployment.
---
# ==========================================================
# Kubernetes Goat Traefik Exposure for K3s
#
# Features:
# - Installs Traefik CRDs
# - Adds Traefik entrypoints
# - Binds host ports 1230-1240
# - Creates IP allowlist middleware
# - Creates IngressRoutes
# - Restarts Traefik
# - Verifies ports are listening
#
# Usage:
# ansible-playbook traefik-kubernetes-goat.yaml
#
# ==========================================================
- name: Configure Traefik for Kubernetes Goat
hosts: controllers
become: yes
vars:
kubeconfig_path: /home/pi/kubeconfig
kubernetes_goat_install_path: /home/pi/
kubernetes_goat_path: "{{ kubernetes_goat_install_path }}kubernetes-goat/"
# ======================================================
# Allowlist IP Ranges
# ======================================================
traefik_allowlist:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"
# ======================================================
# Kubernetes Goat Services
# ======================================================
goat_services:
- { port: 1230, name: "insecure-api", service: "insecure-api" }
- { port: 1231, name: "vulnerable-dashboard", service: "vulnerable-dashboard" }
- { port: 1232, name: "ssrf-app", service: "ssrf-app" }
- { port: 1233, name: "xxe-app", service: "xxe-app" }
- { port: 1234, name: "rbac-demo", service: "rbac-demo" }
- { port: 1235, name: "privilege-escalation", service: "privilege-escalation" }
- { port: 1236, name: "secrets-demo", service: "secrets-demo" }
- { port: 1237, name: "container-escape", service: "container-escape" }
- { port: 1238, name: "crypto-miner", service: "crypto-miner" }
- { port: 1239, name: "exposed-etcd", service: "exposed-etcd" }
- { port: 1240, name: "jwt-none-demo", service: "jwt-none-demo" }
tasks:
# ======================================================
# Download and setup GOAT
# ======================================================
- name: Clone the Repo
ansible.builtin.git:
repo: 'https://github.com/madhuakula/kubernetes-goat.git'
dest: "{{ kubernetes_goat_path }}"
update: yes
environment:
GIT_TERMINAL_PROMPT: 0
# ======================================================
# Run the setup script for GOAT
# ======================================================
- name: Setup Localhost listeners
shell: |
bash {{ kubernetes_goat_path }}/setup-kubernetes-goat.sh
register: setup_output
# ======================================================
# Install Traefik CRDs
# ======================================================
- name: Download Traefik CRDs
get_url:
url: https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
dest: /tmp/traefik-crds.yaml
mode: '0644'
- name: Apply Traefik CRDs
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl apply -f /tmp/traefik-crds.yaml
- name: Wait for Middleware CRD
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl get crd middlewares.traefik.containo.us
register: middleware_crd
retries: 10
delay: 5
until: middleware_crd.rc == 0
# ======================================================
# Configure Traefik
# ======================================================
- name: Create Traefik HelmChartConfig
copy:
dest: /var/lib/rancher/k3s/server/manifests/traefik-config.yaml
content: |
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
additionalArguments:
{% for item in goat_services %}
- "--entrypoints.goat{{ item.port }}.address=:{{ item.port }}/tcp"
{% endfor %}
ports:
{% for item in goat_services %}
goat{{ item.port }}:
port: {{ item.port }}
expose: true
exposedPort: {{ item.port }}
hostPort: {{ item.port }}
protocol: TCP
{% endfor %}
# ======================================================
# Restart Traefik
# ======================================================
- name: Restart Traefik
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl rollout restart deployment traefik -n kube-system
- name: Wait for Traefik rollout
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl rollout status deployment traefik -n kube-system
register: traefik_rollout
retries: 20
delay: 10
until: traefik_rollout.rc == 0
# ======================================================
# Show Traefik Logs if Needed
# ======================================================
- name: Get Traefik logs
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl logs -n kube-system deploy/traefik --tail=50
register: traefik_logs
changed_when: false
- name: Display Traefik logs
debug:
var: traefik_logs.stdout_lines
# ======================================================
# Run the access script for GOAT
# - Delay for pods to start
# ======================================================
- name: Setup Localhost listeners
shell: |
bash {{ kubernetes_goat_path }}/access-kubernetes-goat.sh
register: access_output
# ======================================================
# Create Middleware
# ======================================================
- name: Create Allowlist Middleware YAML
copy:
dest: /tmp/goat-allowlist.yaml
content: |
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: goat-allowlist
namespace: default
spec:
ipWhiteList:
sourceRange:
{% for ip in traefik_allowlist %}
- {{ ip }}
{% endfor %}
- name: Apply Middleware
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl apply -f /tmp/goat-allowlist.yaml
# ======================================================
# Create IngressRoutes
# ======================================================
- name: Create IngressRoutes YAML
copy:
dest: /tmp/goat-ingressroutes.yaml
content: |
{% for item in goat_services %}
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: goat-{{ item.port }}
namespace: default
spec:
entryPoints:
- goat{{ item.port }}
routes:
- match: PathPrefix(`/`)
kind: Rule
middlewares:
- name: goat-allowlist
services:
- name: {{ item.service }}
port: 80
---
{% endfor %}
- name: Apply IngressRoutes
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl apply -f /tmp/goat-ingressroutes.yaml
# ======================================================
# Verify Listening Ports
# ======================================================
- name: Check listening ports
shell: |
ss -tulpn | grep {{ item.port }}
register: listening_ports
loop: "{{ goat_services }}"
changed_when: false
failed_when: false
- name: Display listening ports
debug:
msg: |
Port {{ item.item.port }}:
{{ item.stdout | default('NOT LISTENING') }}
loop: "{{ listening_ports.results }}"
# ======================================================
# Verify Traefik Service
# ======================================================
- name: Get Traefik service
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl get svc traefik -n kube-system -o wide
register: traefik_service
changed_when: false
- name: Show Traefik service
debug:
var: traefik_service.stdout_lines
# ======================================================
# Verify IngressRoutes
# ======================================================
- name: Verify IngressRoutes
shell: |
KUBECONFIG={{ kubeconfig_path }} kubectl get ingressroutes
register: ingressroutes
changed_when: false
- name: Show IngressRoutes
debug:
var: ingressroutes.stdout_lines
# ======================================================
# Final Access Information
# ======================================================
- name: Display access URLs
debug:
msg: |
Kubernetes Goat service '{{ item.name }}'
External URL:
http://{{ inventory_hostname }}:{{ item.port }}
loop: "{{ goat_services }}"
About the author
Tim Wilkes is a UK-based security architect with over 15 years of experience in electronics, Linux, and Unix systems administration. Since 2021, he's been designing secure systems for a telecom company while indulging his passions for programming, automation, and 3D printing. Tim shares his projects, tinkering adventures, and tech insights here - partly as a personal log, and partly in the hopes that others will find them useful.
Want to connect or follow along?
LinkedIn: [phpsytems]
Twitter / X: [@timmehwimmy]
Mastodon: [@timmehwimmy@infosec.exchange]
If you've found a post helpful, consider supporting the blog - it's a part-time passion that your support helps keep alive.
⚠️ Disclaimer
This post may contain affiliate links. If you choose to purchase through them, I may earn a small commission at no extra cost to you. I only recommend items and services I’ve personally read or used and found valuable.
As an Amazon Associate I earn from qualifying purchases.