diff --git a/README.md b/README.md
deleted file mode 100644
index b8b9cc3..0000000
--- a/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-

-
-Naughty or Nice CTF
-
-
-
Unwrap festive flags and unleash your inner hacker in a
-
Christmas CTF where being naughty or nice is all part of the game.
-
diff --git a/ctf.yml b/ctf.yml
deleted file mode 100644
index 96b24fc..0000000
--- a/ctf.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-version: 1
-defaults:
- category: "misc"
- points: 100
- is_visible: false
- complexity: 5
- service_webshell: false
- service_export_flag: true
- service_flag_env: "FLAG"
\ No newline at end of file
diff --git a/k8s/00-namespace.yaml b/k8s/00-namespace.yaml
new file mode 100644
index 0000000..e4d7d8a
--- /dev/null
+++ b/k8s/00-namespace.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: ctf
+ labels:
+ app.kubernetes.io/part-of: noc-ctf
diff --git a/k8s/01-flags-secret.yaml b/k8s/01-flags-secret.yaml
new file mode 100644
index 0000000..d1cb89a
--- /dev/null
+++ b/k8s/01-flags-secret.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: ctf-flags
+ namespace: ctf
+type: Opaque
+stringData:
+ # entrypoint.sh in each image writes $FLAG to the flag file the challenge reads.
+ chal1-flag: "CTF{pyth0n_sst1_1nj3ct10n_rul3z}" # Second Track
+ chal2-flag: "CTF{sst1_w1th_f1lt3rs_m4d3_1t_s3cur3}" # Second Track Aftermath
+ chal3-flag: "CTF{sst1_n0_c0mm4nd_3x3cuti0n_just_r34d}" # Second Track Reborn
+ chal4-flag: "CTF{lf1_php_s3ss10n_p01s0n1ng}" # Big Software Foundation
+ chal5-flag: "CTF{xss_m4d3_34s13r_w1th_pupp3t33r}" # Elf's Blog
diff --git a/k8s/README.md b/k8s/README.md
new file mode 100644
index 0000000..e6c6916
--- /dev/null
+++ b/k8s/README.md
@@ -0,0 +1,118 @@
+# 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`.
diff --git a/k8s/build-images.sh b/k8s/build-images.sh
new file mode 100755
index 0000000..e69cca7
--- /dev/null
+++ b/k8s/build-images.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+# Build all web challenge images and make them available to k3s.
+#
+# k3s uses containerd, not the Docker daemon, so a locally-built Docker image
+# is NOT visible to k3s until it is imported into containerd (or pushed to a
+# registry). This script builds each image and imports it directly.
+#
+# Run this ON the k3s node (needs docker + the k3s binary).
+#
+# For a MULTI-NODE cluster, import-per-node does not scale: set REGISTRY to a
+# registry reachable by every node, e.g.
+# REGISTRY=registry.entysec.com/ctf ./build-images.sh
+# then change imagePullPolicy to IfNotPresent (already set) and prefix the
+# image names in the manifests with $REGISTRY/.
+set -euo pipefail
+
+# Repo root = parent of this script's directory.
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+REGISTRY="${REGISTRY:-}" # empty => build + import into local k3s containerd
+
+# image-tag -> build context (relative to repo root)
+declare -a CHALLENGES=(
+ "ctf-second-track:latest|Web/Easy/Second Track"
+ "ctf-second-track-aftermath:latest|Web/Medium/Second Track Aftermath"
+ "ctf-second-track-reborn:latest|Web/Hard/Second Track Reborn"
+ "ctf-big-software-foundation:latest|Web/Easy/Big Software Foundation"
+ "ctf-elfs-blog:latest|Web/Hard/Elfs' Blog"
+)
+
+for entry in "${CHALLENGES[@]}"; do
+ tag="${entry%%|*}"
+ ctx="${entry#*|}"
+ image="${REGISTRY:+$REGISTRY/}$tag"
+
+ echo "==> Building $image (context: $ctx)"
+ docker build -t "$image" "$ROOT/$ctx"
+
+ if [ -n "$REGISTRY" ]; then
+ echo "==> Pushing $image"
+ docker push "$image"
+ else
+ echo "==> Importing $image into k3s containerd"
+ # Pipe the Docker image straight into k3s' containerd image store.
+ docker save "$image" | sudo k3s ctr images import -
+ fi
+done
+
+echo "==> Done. Images ready for k3s."
diff --git a/k8s/chal1-second-track.yaml b/k8s/chal1-second-track.yaml
new file mode 100644
index 0000000..244a114
--- /dev/null
+++ b/k8s/chal1-second-track.yaml
@@ -0,0 +1,60 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: chal1-second-track
+ namespace: ctf
+ labels: { app: chal1-second-track }
+spec:
+ replicas: 1
+ selector:
+ matchLabels: { app: chal1-second-track }
+ template:
+ metadata:
+ labels: { app: chal1-second-track }
+ spec:
+ containers:
+ - name: app
+ image: ctf-second-track:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ env:
+ - name: PORT
+ value: "80"
+ - name: FLAG
+ valueFrom:
+ secretKeyRef: { name: ctf-flags, key: chal1-flag }
+ resources:
+ requests: { cpu: "50m", memory: "64Mi" }
+ limits: { cpu: "500m", memory: "256Mi" }
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chal1-second-track
+ namespace: ctf
+spec:
+ selector: { app: chal1-second-track }
+ ports:
+ - port: 80
+ targetPort: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: chal1-second-track
+ namespace: ctf
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+spec:
+ ingressClassName: traefik
+ rules:
+ - host: chal1.entysec.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: chal1-second-track
+ port: { number: 80 }
diff --git a/k8s/chal2-second-track-aftermath.yaml b/k8s/chal2-second-track-aftermath.yaml
new file mode 100644
index 0000000..d344a3e
--- /dev/null
+++ b/k8s/chal2-second-track-aftermath.yaml
@@ -0,0 +1,60 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: chal2-aftermath
+ namespace: ctf
+ labels: { app: chal2-aftermath }
+spec:
+ replicas: 1
+ selector:
+ matchLabels: { app: chal2-aftermath }
+ template:
+ metadata:
+ labels: { app: chal2-aftermath }
+ spec:
+ containers:
+ - name: app
+ image: ctf-second-track-aftermath:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ env:
+ - name: PORT
+ value: "80"
+ - name: FLAG
+ valueFrom:
+ secretKeyRef: { name: ctf-flags, key: chal2-flag }
+ resources:
+ requests: { cpu: "50m", memory: "64Mi" }
+ limits: { cpu: "500m", memory: "256Mi" }
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chal2-aftermath
+ namespace: ctf
+spec:
+ selector: { app: chal2-aftermath }
+ ports:
+ - port: 80
+ targetPort: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: chal2-aftermath
+ namespace: ctf
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+spec:
+ ingressClassName: traefik
+ rules:
+ - host: chal2.entysec.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: chal2-aftermath
+ port: { number: 80 }
diff --git a/k8s/chal3-second-track-reborn.yaml b/k8s/chal3-second-track-reborn.yaml
new file mode 100644
index 0000000..ea6850c
--- /dev/null
+++ b/k8s/chal3-second-track-reborn.yaml
@@ -0,0 +1,60 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: chal3-reborn
+ namespace: ctf
+ labels: { app: chal3-reborn }
+spec:
+ replicas: 1
+ selector:
+ matchLabels: { app: chal3-reborn }
+ template:
+ metadata:
+ labels: { app: chal3-reborn }
+ spec:
+ containers:
+ - name: app
+ image: ctf-second-track-reborn:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ env:
+ - name: PORT
+ value: "80"
+ - name: FLAG
+ valueFrom:
+ secretKeyRef: { name: ctf-flags, key: chal3-flag }
+ resources:
+ requests: { cpu: "50m", memory: "64Mi" }
+ limits: { cpu: "500m", memory: "256Mi" }
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chal3-reborn
+ namespace: ctf
+spec:
+ selector: { app: chal3-reborn }
+ ports:
+ - port: 80
+ targetPort: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: chal3-reborn
+ namespace: ctf
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+spec:
+ ingressClassName: traefik
+ rules:
+ - host: chal3.entysec.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: chal3-reborn
+ port: { number: 80 }
diff --git a/k8s/chal4-big-software-foundation.yaml b/k8s/chal4-big-software-foundation.yaml
new file mode 100644
index 0000000..4e86ec3
--- /dev/null
+++ b/k8s/chal4-big-software-foundation.yaml
@@ -0,0 +1,59 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: chal4-big-software
+ namespace: ctf
+ labels: { app: chal4-big-software }
+spec:
+ replicas: 1
+ selector:
+ matchLabels: { app: chal4-big-software }
+ template:
+ metadata:
+ labels: { app: chal4-big-software }
+ spec:
+ containers:
+ - name: app
+ image: ctf-big-software-foundation:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ env:
+ # PHP/Apache image; entrypoint writes $FLAG to /var/www/html/flag.txt
+ - name: FLAG
+ valueFrom:
+ secretKeyRef: { name: ctf-flags, key: chal4-flag }
+ resources:
+ requests: { cpu: "50m", memory: "64Mi" }
+ limits: { cpu: "500m", memory: "256Mi" }
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chal4-big-software
+ namespace: ctf
+spec:
+ selector: { app: chal4-big-software }
+ ports:
+ - port: 80
+ targetPort: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: chal4-big-software
+ namespace: ctf
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+spec:
+ ingressClassName: traefik
+ rules:
+ - host: chal4.entysec.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: chal4-big-software
+ port: { number: 80 }
diff --git a/k8s/chal5-elfs-blog.yaml b/k8s/chal5-elfs-blog.yaml
new file mode 100644
index 0000000..45e12b7
--- /dev/null
+++ b/k8s/chal5-elfs-blog.yaml
@@ -0,0 +1,74 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: chal5-elfs-blog
+ namespace: ctf
+ labels: { app: chal5-elfs-blog }
+spec:
+ replicas: 1 # keep at 1: writes comments.json to local fs
+ selector:
+ matchLabels: { app: chal5-elfs-blog }
+ template:
+ metadata:
+ labels: { app: chal5-elfs-blog }
+ spec:
+ containers:
+ - name: app
+ image: ctf-elfs-blog:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ env:
+ - name: FLAG
+ valueFrom:
+ secretKeyRef: { name: ctf-flags, key: chal5-flag }
+ - name: ADMIN_BOT_ENABLED
+ value: "1"
+ - name: ADMIN_BOT_INTERVAL
+ value: "15"
+ - name: BASE_URL
+ value: "http://127.0.0.1:80"
+ resources:
+ requests: { cpu: "100m", memory: "256Mi" }
+ limits: { cpu: "1", memory: "1Gi" } # headless Chromium is memory-hungry
+ # Chromium is launched with --disable-dev-shm-usage, so a large /dev/shm
+ # is not strictly required, but this avoids the default 64Mi shm limit.
+ volumeMounts:
+ - name: dshm
+ mountPath: /dev/shm
+ volumes:
+ - name: dshm
+ emptyDir:
+ medium: Memory
+ sizeLimit: 256Mi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chal5-elfs-blog
+ namespace: ctf
+spec:
+ selector: { app: chal5-elfs-blog }
+ ports:
+ - port: 80
+ targetPort: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: chal5-elfs-blog
+ namespace: ctf
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+spec:
+ ingressClassName: traefik
+ rules:
+ - host: chal5.entysec.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: chal5-elfs-blog
+ port: { number: 80 }
diff --git a/k8s/deploy.sh b/k8s/deploy.sh
new file mode 100755
index 0000000..056c0de
--- /dev/null
+++ b/k8s/deploy.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Apply all CTF web-challenge manifests to the cluster.
+# Assumes kubectl is configured against your k3s cluster and Traefik is installed.
+set -euo pipefail
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+kubectl apply -f "$DIR/00-namespace.yaml"
+kubectl apply -f "$DIR/01-flags-secret.yaml"
+kubectl apply -f "$DIR/chal1-second-track.yaml"
+kubectl apply -f "$DIR/chal2-second-track-aftermath.yaml"
+kubectl apply -f "$DIR/chal3-second-track-reborn.yaml"
+kubectl apply -f "$DIR/chal4-big-software-foundation.yaml"
+kubectl apply -f "$DIR/chal5-elfs-blog.yaml"
+
+echo "==> Applied. Watching rollout..."
+kubectl -n ctf rollout status deploy/chal1-second-track
+kubectl -n ctf rollout status deploy/chal2-aftermath
+kubectl -n ctf rollout status deploy/chal3-reborn
+kubectl -n ctf rollout status deploy/chal4-big-software
+kubectl -n ctf rollout status deploy/chal5-elfs-blog
+kubectl -n ctf get pods,ingress