{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreif2ohamh7xdceehk6gct463dhtaypez3y6osrrqdasz43us4ntlaa",
    "uri": "at://did:plc:5y2ps7xhcqmc2d63b73ui72s/app.bsky.feed.post/3mnxc45inltd2"
  },
  "description": "Learn how to fully automate Kubernetes Goat deployment on a Raspberry Pi K3s cluster using Ansible, Traefik, and K3s for rapid rebuilds and repeatable security labs.",
  "path": "/automating-kubernetes-goat-on-raspberry-pi-with-ansible/",
  "publishedAt": "2026-06-10T18:00:11.000Z",
  "site": "https://blog.php-systems.com",
  "textContent": "Previously, I walked through manually deploying Kubernetes Goat onto my Raspberry Pi Kubernetes cluster built with ClusterHAT and K3s.\n\nThat worked well, but after rebuilding the cluster several times while experimenting with:\n\n  * Traefik\n  * NFS storage\n  * Node labels\n  * Security tooling\n  * K3s upgrades\n\n\n\n…I quickly realised I wanted a repeatable deployment process.\n\nThat’s where Ansible came in.\n\nThis follow-up takes the earlier manual deployment and automates the entire process:\n\n  * Deploy Kubernetes Goat\n  * Configure Traefik\n  * Expose scenarios externally\n  * Configure allowlists\n  * Restart services\n  * Validate accessibility\n\n\n\nThe result is a reusable Kubernetes security lab that can be rebuilt almost instantly.\n\n* * *\n\n# A Quick Warning About Rebuilding Kubernetes Clusters\n\nBefore going further, it’s important to note:\n\nYou generally should **not** rebuild Kubernetes clusters regularly in production or long-lived environments.\n\nFrequent rebuilding can:\n\n  * Disrupt workloads\n  * Break persistent storage\n  * Cause certificate or token issues\n  * Lose cluster state\n  * Introduce configuration drift\n\n\n\nIn my case, this cluster is:\n\n  * A home lab\n  * Disposable\n  * Used for experimentation\n  * Rebuilt frequently for testing\n\n\n\nThat makes automation extremely valuable.\n\nFor reusable or production clusters, infrastructure should usually be:\n\n  * Maintained incrementally\n  * Upgraded carefully\n  * Managed declaratively over time\n\n\n\nThis automation is designed for rapid lab deployment and recovery—not operational production Kubernetes.\n\n* * *\n\n# Why Automate Kubernetes Goat?\n\nKubernetes Goat is intentionally vulnerable.\n\nThat means I regularly:\n\n  * Break it\n  * Reset it\n  * Reconfigure networking\n  * Experiment with scans\n  * Test detection tooling\n  * Try new ingress approaches\n\n\n\nBeing able to redeploy everything from scratch with a single command saves a huge amount of time.\n\nInstead of:\n\n  * manually cloning repositories\n  * reapplying manifests\n  * configuring Traefik\n  * rebuilding middleware\n  * exposing services\n\n\n\n…I can now run:\n\n\n    ansible-playbook traefik-kubernetes-goat.yaml\n\n…and rebuild the environment automatically.\n\n* * *\n\n# What the Playbook Automates\n\nThe playbook handles:\n\n## Kubernetes Goat Deployment\n\nIt:\n\n  * clones the Kubernetes Goat repository\n  * runs the setup scripts\n  * deploys all vulnerable workloads\n\n\n\n* * *\n\n## Traefik CRD Installation\n\nTraefik CRDs are required for:\n\n  * `IngressRoute`\n  * `Middleware`\n\n\n\nWithout them, Traefik custom resources fail.\n\nThe playbook installs them automatically.\n\n* * *\n\n## Traefik EntryPoints\n\nThe deployment dynamically creates:\n\n  * ports `1230-1240`\n  * dedicated Traefik entrypoints\n  * hostPort bindings\n\n\n\nThis exposes each scenario externally.\n\n* * *\n\n## IP Allowlist Middleware\n\nBecause Kubernetes Goat is intentionally vulnerable, access is restricted using:\n\n  * internal IP ranges\n  * Traefik middleware\n  * source range filtering\n\n\n\nThis keeps the environment isolated inside the lab network.\n\n* * *\n\n## IngressRoute Creation\n\nEach service receives:\n\n  * a dedicated ingress route\n  * a dedicated external port\n  * direct mapping to the backend service\n\n\n\nThis makes testing dramatically easier.\n\n* * *\n\n# Why External Exposure Matters\n\nOne of the most useful aspects of Kubernetes Goat is integrating external tooling.\n\nExposing the scenarios allows:\n\n  * OWASP ZAP\n  * Burp Suite\n  * Nikto\n  * Nmap\n  * Trivy\n  * kube-hunter\n  * Custom scripts\n\n\n\n…to interact directly with the vulnerable applications.\n\nThis turns the Raspberry Pi cluster into a miniature Kubernetes security testing environment.\n\n* * *\n\n# HostPort Challenges on K3s\n\nOne issue I ran into was that:\n\n  * Traefik entrypoints existed\n  * Kubernetes services existed\n  * IngressRoutes existed\n\n\n\n…but the ports still returned `connection refused`.\n\nThe problem was that K3s Traefik does not automatically bind arbitrary ports on the host network.\n\nThe solution was adding:\n\n\n    hostPort: 1230\n\nfor each service entrypoint.\n\nWithout this, the services were only reachable internally.\n\nOnce added, the Raspberry Pi controller correctly listened on:\n\n  * `1230`\n  * `1231`\n  * `1232`\n  * etc.\n\n\n\n* * *\n\n# Why This Is Useful for Home Labs\n\nThis kind of automation works particularly well for:\n\n  * Raspberry Pi clusters\n  * Disposable Kubernetes labs\n  * Security testing environments\n  * Kubernetes learning\n  * Classroom demos\n  * Rapid experimentation\n\n\n\nA full rebuild becomes:\n\n  * predictable\n  * reproducible\n  * quick\n\n\n\nAnd when something inevitably breaks, recovery is painless.\n\n* * *\n\n# Things I Would Improve Later\n\nA few areas I’d likely improve in the future:\n\n## Move to GitOps\n\nUsing:\n\n  * Gitlab\n  * ArgoCD\n  * Flux\n\n\n\n…would make deployments cleaner and more Kubernetes-native.\n\n* * *\n\n## Add TLS\n\nRight now this is internal-only HTTP.\n\nAdding:\n\n  * cert-manager\n  * self-signed certs\n  * local PKI\n\n\n\n…would improve realism. Traefik could also be used to configure the certificates as well.\n\n* * *\n\n## Dynamic Service Discovery\n\nCurrently the service mappings are static.\n\nAutomatically generating routes from Kubernetes labels would be cleaner.\n\n* * *\n\n## Add Monitoring and Detection\n\nThis cluster would pair extremely well with:\n\n  * Falco\n  * Grafana\n  * Loki\n  * Prometheus\n  * OpenTelemetry\n\n\n\nEspecially during active testing.\n\n* * *\n\n# Final Thoughts\n\nThis project started as:\n\n  * a Raspberry Pi cluster\n  * a Kubernetes experiment\n  * a security playground\n\n\n\nIt gradually evolved into a fully automated disposable Kubernetes lab.\n\nThat’s one of the things I enjoy most about home lab environments:\nthey naturally grow alongside your interests.\n\nAutomating Kubernetes Goat with Ansible makes rebuilding the cluster nearly effortless and encourages experimentation without fear of breaking things permanently.\n\nAnd 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.\n\n* * *\n\n# Full Ansible Playbook\n\nBelow is the complete playbook used to automate the deployment.\n\n\n    ---\n    # ==========================================================\n    # Kubernetes Goat Traefik Exposure for K3s\n    #\n    # Features:\n    # - Installs Traefik CRDs\n    # - Adds Traefik entrypoints\n    # - Binds host ports 1230-1240\n    # - Creates IP allowlist middleware\n    # - Creates IngressRoutes\n    # - Restarts Traefik\n    # - Verifies ports are listening\n    #\n    # Usage:\n    # ansible-playbook traefik-kubernetes-goat.yaml\n    #\n    # ==========================================================\n\n    - name: Configure Traefik for Kubernetes Goat\n      hosts: controllers\n      become: yes\n\n      vars:\n        kubeconfig_path: /home/pi/kubeconfig\n        kubernetes_goat_install_path: /home/pi/\n        kubernetes_goat_path: \"{{ kubernetes_goat_install_path }}kubernetes-goat/\"\n\n        # ======================================================\n        # Allowlist IP Ranges\n        # ======================================================\n\n        traefik_allowlist:\n          - \"192.168.0.0/16\"\n          - \"10.0.0.0/8\"\n          - \"172.16.0.0/12\"\n\n        # ======================================================\n        # Kubernetes Goat Services\n        # ======================================================\n\n        goat_services:\n          - { port: 1230, name: \"insecure-api\", service: \"insecure-api\" }\n          - { port: 1231, name: \"vulnerable-dashboard\", service: \"vulnerable-dashboard\" }\n          - { port: 1232, name: \"ssrf-app\", service: \"ssrf-app\" }\n          - { port: 1233, name: \"xxe-app\", service: \"xxe-app\" }\n          - { port: 1234, name: \"rbac-demo\", service: \"rbac-demo\" }\n          - { port: 1235, name: \"privilege-escalation\", service: \"privilege-escalation\" }\n          - { port: 1236, name: \"secrets-demo\", service: \"secrets-demo\" }\n          - { port: 1237, name: \"container-escape\", service: \"container-escape\" }\n          - { port: 1238, name: \"crypto-miner\", service: \"crypto-miner\" }\n          - { port: 1239, name: \"exposed-etcd\", service: \"exposed-etcd\" }\n          - { port: 1240, name: \"jwt-none-demo\", service: \"jwt-none-demo\" }\n\n      tasks:\n\n        # ======================================================\n        # Download and setup GOAT\n        # ======================================================\n\n        - name: Clone the Repo\n          ansible.builtin.git:\n            repo: 'https://github.com/madhuakula/kubernetes-goat.git'\n            dest: \"{{ kubernetes_goat_path }}\"\n            update: yes\n          environment:\n            GIT_TERMINAL_PROMPT: 0\n\n        # ======================================================\n        # Run the setup script for GOAT\n        # ======================================================\n\n        - name: Setup Localhost listeners\n          shell: |\n            bash {{ kubernetes_goat_path }}/setup-kubernetes-goat.sh\n          register: setup_output\n\n        # ======================================================\n        # Install Traefik CRDs\n        # ======================================================\n\n        - name: Download Traefik CRDs\n          get_url:\n            url: https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml\n            dest: /tmp/traefik-crds.yaml\n            mode: '0644'\n\n        - name: Apply Traefik CRDs\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl apply -f /tmp/traefik-crds.yaml\n\n        - name: Wait for Middleware CRD\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl get crd middlewares.traefik.containo.us\n          register: middleware_crd\n          retries: 10\n          delay: 5\n          until: middleware_crd.rc == 0\n\n        # ======================================================\n        # Configure Traefik\n        # ======================================================\n\n        - name: Create Traefik HelmChartConfig\n          copy:\n            dest: /var/lib/rancher/k3s/server/manifests/traefik-config.yaml\n            content: |\n              apiVersion: helm.cattle.io/v1\n              kind: HelmChartConfig\n\n              metadata:\n                name: traefik\n                namespace: kube-system\n\n              spec:\n                valuesContent: |-\n                  additionalArguments:\n              {% for item in goat_services %}\n                    - \"--entrypoints.goat{{ item.port }}.address=:{{ item.port }}/tcp\"\n              {% endfor %}\n\n                  ports:\n              {% for item in goat_services %}\n                    goat{{ item.port }}:\n                      port: {{ item.port }}\n                      expose: true\n                      exposedPort: {{ item.port }}\n                      hostPort: {{ item.port }}\n                      protocol: TCP\n              {% endfor %}\n\n        # ======================================================\n        # Restart Traefik\n        # ======================================================\n\n        - name: Restart Traefik\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl rollout restart deployment traefik -n kube-system\n\n        - name: Wait for Traefik rollout\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl rollout status deployment traefik -n kube-system\n          register: traefik_rollout\n          retries: 20\n          delay: 10\n          until: traefik_rollout.rc == 0\n\n        # ======================================================\n        # Show Traefik Logs if Needed\n        # ======================================================\n\n        - name: Get Traefik logs\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl logs -n kube-system deploy/traefik --tail=50\n          register: traefik_logs\n          changed_when: false\n\n        - name: Display Traefik logs\n          debug:\n            var: traefik_logs.stdout_lines\n\n        # ======================================================\n        # Run the access script for GOAT\n        #  - Delay for pods to start\n        # ======================================================\n\n        - name: Setup Localhost listeners\n          shell: |\n            bash {{ kubernetes_goat_path }}/access-kubernetes-goat.sh\n          register: access_output\n\n        # ======================================================\n        # Create Middleware\n        # ======================================================\n\n        - name: Create Allowlist Middleware YAML\n          copy:\n            dest: /tmp/goat-allowlist.yaml\n            content: |\n              apiVersion: traefik.containo.us/v1alpha1\n              kind: Middleware\n\n              metadata:\n                name: goat-allowlist\n                namespace: default\n\n              spec:\n                ipWhiteList:\n                  sourceRange:\n              {% for ip in traefik_allowlist %}\n                    - {{ ip }}\n              {% endfor %}\n\n        - name: Apply Middleware\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl apply -f /tmp/goat-allowlist.yaml\n\n        # ======================================================\n        # Create IngressRoutes\n        # ======================================================\n\n        - name: Create IngressRoutes YAML\n          copy:\n            dest: /tmp/goat-ingressroutes.yaml\n            content: |\n              {% for item in goat_services %}\n              apiVersion: traefik.containo.us/v1alpha1\n              kind: IngressRoute\n\n              metadata:\n                name: goat-{{ item.port }}\n                namespace: default\n\n              spec:\n                entryPoints:\n                  - goat{{ item.port }}\n\n                routes:\n                  - match: PathPrefix(`/`)\n                    kind: Rule\n\n                    middlewares:\n                      - name: goat-allowlist\n\n                    services:\n                      - name: {{ item.service }}\n                        port: 80\n\n              ---\n              {% endfor %}\n\n        - name: Apply IngressRoutes\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl apply -f /tmp/goat-ingressroutes.yaml\n\n        # ======================================================\n        # Verify Listening Ports\n        # ======================================================\n\n        - name: Check listening ports\n          shell: |\n            ss -tulpn | grep {{ item.port }}\n          register: listening_ports\n          loop: \"{{ goat_services }}\"\n          changed_when: false\n          failed_when: false\n\n        - name: Display listening ports\n          debug:\n            msg: |\n              Port {{ item.item.port }}:\n\n              {{ item.stdout | default('NOT LISTENING') }}\n          loop: \"{{ listening_ports.results }}\"\n\n        # ======================================================\n        # Verify Traefik Service\n        # ======================================================\n\n        - name: Get Traefik service\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl get svc traefik -n kube-system -o wide\n          register: traefik_service\n          changed_when: false\n       - name: Show Traefik service\n          debug:\n            var: traefik_service.stdout_lines\n\n        # ======================================================\n        # Verify IngressRoutes\n        # ======================================================\n\n        - name: Verify IngressRoutes\n          shell: |\n            KUBECONFIG={{ kubeconfig_path }} kubectl get ingressroutes\n          register: ingressroutes\n          changed_when: false\n\n        - name: Show IngressRoutes\n          debug:\n            var: ingressroutes.stdout_lines\n\n        # ======================================================\n        # Final Access Information\n        # ======================================================\n\n        - name: Display access URLs\n          debug:\n            msg: |\n              Kubernetes Goat service '{{ item.name }}'\n\n              External URL:\n              http://{{ inventory_hostname }}:{{ item.port }}\n          loop: \"{{ goat_services }}\"",
  "title": "Automating Kubernetes Goat on Raspberry Pi with Ansible",
  "updatedAt": "2026-06-10T18:00:11.566Z"
}