# Deploying the Web CTF challenges to k3s (Traefik) Five web challenges, each a single-container HTTP service on port 80, exposed on its own subdomain via a Traefik Ingress. | Host | Challenge | Stack | Flag secret key | |----------------------|--------------------------|------------------------|-----------------| | `chal1.entysec.com` | Second Track (Easy) | Flask + gunicorn | `chal1-flag` | | `chal2.entysec.com` | Second Track Aftermath | Flask + gunicorn | `chal2-flag` | | `chal3.entysec.com` | Second Track Reborn | Flask + gunicorn | `chal3-flag` | | `chal4.entysec.com` | Big Software Foundation | PHP 7.2 + Apache | `chal4-flag` | | `chal5.entysec.com` | Elf's Blog | Node + Chromium + Flask (admin bot) | `chal5-flag` | Each challenge's `entrypoint.sh` reads the `FLAG` env var and writes it to the file the app expects — so the flags live only in the `ctf-flags` Secret, never baked into an image. ## Prerequisites - A running k3s cluster with Traefik installed (k3s ships it by default). - `kubectl` configured against the cluster. - Docker on the k3s node (to build images), **or** a container registry every node can reach (multi-node). ## 1. Build images and make them available to k3s k3s uses **containerd**, not the Docker daemon — a `docker build` alone is not visible to k3s. `build-images.sh` builds each image and imports it into k3s' containerd store. Run it on the k3s node: ```bash ./build-images.sh ``` Multi-node cluster? Use a registry instead of per-node import: ```bash REGISTRY=registry.entysec.com/ctf ./build-images.sh # then prefix the image: names in the chalN-*.yaml manifests with the same registry ``` ## 2. Deploy ```bash ./deploy.sh ``` That applies the namespace, the flags Secret, and all five challenges (each a Deployment + Service + Ingress), then waits for rollout. Deploy a single challenge: ```bash kubectl apply -f chal4-big-software-foundation.yaml ``` ## 3. Point DNS at the cluster Create DNS records (A/AAAA, or a `*.entysec.com` wildcard) for each host, pointing at the IP where Traefik's entrypoint is exposed (the k3s node / load balancer). Traefik routes by the `Host` header, so the subdomain must resolve to the cluster. Quick local test without DNS: ```bash curl -H 'Host: chal1.entysec.com' http:/// ``` ## 4. (Optional) HTTPS Two common options: - **cert-manager + Let's Encrypt** — add a `ClusterIssuer`, then a `tls:` block referencing a cert Secret on each Ingress and switch the router entrypoint annotation to `websecure`. - **Traefik default TLS** — set the annotation `traefik.ingress.kubernetes.io/router.tls: "true"` and add a `tls:` section. Example TLS-enabled Ingress (with cert-manager): ```yaml metadata: annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure cert-manager.io/cluster-issuer: letsencrypt-prod spec: tls: - hosts: [chal1.entysec.com] secretName: chal1-tls ``` ## Managing flags Flags live in `01-flags-secret.yaml`. After editing, re-apply and restart the affected deployment so the new flag is written on boot: ```bash kubectl apply -f 01-flags-secret.yaml kubectl -n ctf rollout restart deploy/chal1-second-track ``` ## Tear down ```bash kubectl delete namespace ctf ``` ## Notes - **Elf's Blog** runs gunicorn + a Puppeteer admin bot in one container. It needs more memory (limit 1Gi) and gets a tmpfs `/dev/shm`. Chromium already launches with `--no-sandbox`, so no extra pod privileges are required. Keep it at `replicas: 1` — it persists comments to the container filesystem. - The Flask apps read `PORT` (set to 80); the PHP app listens on Apache's default 80. All Services target container port 80. - To reset a challenge's state (e.g. Elf's Blog comments): `kubectl -n ctf rollout restart deploy/chal5-elfs-blog`.