Update
This commit is contained in:
parent
211e43c1a4
commit
c673ab58a8
@ -1,8 +0,0 @@
|
||||
<h3 align="center"><img src="docs/logo.png" alt="logo" width="50%"></h3>
|
||||
|
||||
<h3 align="center"><strong>Naughty or Nice CTF</strong></h4>
|
||||
|
||||
<p align="center">
|
||||
<br>Unwrap festive flags and unleash your inner hacker in a
|
||||
<br>Christmas CTF where being naughty or nice is all part of the game.
|
||||
</p>
|
||||
9
ctf.yml
9
ctf.yml
@ -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"
|
||||
6
k8s/00-namespace.yaml
Normal file
6
k8s/00-namespace.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ctf
|
||||
labels:
|
||||
app.kubernetes.io/part-of: noc-ctf
|
||||
13
k8s/01-flags-secret.yaml
Normal file
13
k8s/01-flags-secret.yaml
Normal file
@ -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
|
||||
118
k8s/README.md
Normal file
118
k8s/README.md
Normal file
@ -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://<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`.
|
||||
48
k8s/build-images.sh
Executable file
48
k8s/build-images.sh
Executable file
@ -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."
|
||||
60
k8s/chal1-second-track.yaml
Normal file
60
k8s/chal1-second-track.yaml
Normal file
@ -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 }
|
||||
60
k8s/chal2-second-track-aftermath.yaml
Normal file
60
k8s/chal2-second-track-aftermath.yaml
Normal file
@ -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 }
|
||||
60
k8s/chal3-second-track-reborn.yaml
Normal file
60
k8s/chal3-second-track-reborn.yaml
Normal file
@ -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 }
|
||||
59
k8s/chal4-big-software-foundation.yaml
Normal file
59
k8s/chal4-big-software-foundation.yaml
Normal file
@ -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 }
|
||||
74
k8s/chal5-elfs-blog.yaml
Normal file
74
k8s/chal5-elfs-blog.yaml
Normal file
@ -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 }
|
||||
21
k8s/deploy.sh
Executable file
21
k8s/deploy.sh
Executable file
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user