From c673ab58a825331bd3a31e90b262ca1f518f4760 Mon Sep 17 00:00:00 2001 From: Ivan Nikolskiy Date: Sun, 12 Jul 2026 20:32:52 +0100 Subject: [PATCH] Update --- README.md | 8 -- ctf.yml | 9 -- k8s/00-namespace.yaml | 6 ++ k8s/01-flags-secret.yaml | 13 +++ k8s/README.md | 118 +++++++++++++++++++++++++ k8s/build-images.sh | 48 ++++++++++ k8s/chal1-second-track.yaml | 60 +++++++++++++ k8s/chal2-second-track-aftermath.yaml | 60 +++++++++++++ k8s/chal3-second-track-reborn.yaml | 60 +++++++++++++ k8s/chal4-big-software-foundation.yaml | 59 +++++++++++++ k8s/chal5-elfs-blog.yaml | 74 ++++++++++++++++ k8s/deploy.sh | 21 +++++ 12 files changed, 519 insertions(+), 17 deletions(-) delete mode 100644 README.md delete mode 100644 ctf.yml create mode 100644 k8s/00-namespace.yaml create mode 100644 k8s/01-flags-secret.yaml create mode 100644 k8s/README.md create mode 100755 k8s/build-images.sh create mode 100644 k8s/chal1-second-track.yaml create mode 100644 k8s/chal2-second-track-aftermath.yaml create mode 100644 k8s/chal3-second-track-reborn.yaml create mode 100644 k8s/chal4-big-software-foundation.yaml create mode 100644 k8s/chal5-elfs-blog.yaml create mode 100755 k8s/deploy.sh diff --git a/README.md b/README.md deleted file mode 100644 index b8b9cc3..0000000 --- a/README.md +++ /dev/null @@ -1,8 +0,0 @@ -

logo

- -

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