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:

  • IngressRoute
  • Middleware

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: 1230

for each service entrypoint.

Without this, the services were only reachable internally.

Once added, the Raspberry Pi controller correctly listened on:

  • 1230
  • 1231
  • 1232
  • 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.