Home for web CTF challenges built for Secur0 Academy. Every challenge follows one shared contract so it deploys identically on Docker today and Kubernetes later — without rebuilding the challenge.
| Path | What it is |
|---|---|
requisites.md |
The authoring spec. Every rule (MUST) a challenge has to satisfy. Read first. |
challenge-template/ |
Copy-paste starter. Working skeleton pre-wired to pass the spec. |
<your-challenge>/ |
One folder per challenge (add your own). |
-
Read the spec —
requisites.md. The whole platform relies on the contract: challenge reads config from the environment and exposes one port it does not choose itself. -
Copy the template:
cp -r challenge-template my-challenge cd my-challenge -
Fill it in — find every placeholder and replace:
grep -rn "REPLACE\|replace-me" .
Swap in your vulnerable app (
src/), metadata (challenge.yml), docs (README.md), and the solution (solution/). Pin the base image digest inDockerfile/docker-compose.yml. -
Run it locally:
cp .env.example .env docker compose up --build curl http://localhost:8080/healthz # -> ok -
Tick every box in the challenge's
CHECKLIST.md. It maps 1:1 to the spec — a challenge is accepted only when all boxes pass.
A challenge talks to the platform through only three things:
PORT— internal port it binds (0.0.0.0, never a fixed host port)FLAG— injected at runtime, opaque, never baked into the imageGET /healthz— returns200when ready to serve
Everything else — host port mapping, per-student isolation, TTL, flag
injection — lives in the platform, never in the challenge. That's why the same
image runs unchanged under docker compose or Kubernetes.
- Never commit
.envor a real flag. Only.env.exampleis committed. - No fixed host port in committed files — use
${HOST_PORT}. - One public port per challenge; databases/caches stay internal.
- Non-root, no privileged, pin the base image by exact tag or digest.
- Flag must be unreachable without the intended exploit.
Full detail and the reasoning behind each rule: requisites.md.