NoN-CTF/k8s/README.md
2026-07-12 20:32:52 +01:00

119 lines
3.8 KiB
Markdown

# 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://<NODE_IP>/
```
## 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`.