
Self-hosted OWASP CTF kit: one box, one free GitHub org, no cloud dependencies
A self-hosted control plane for security-learning events — one box, one free GitHub org.
Run it for a university, a high school, an OWASP chapter, a meetup.
Read AGENTS.md before you write code. It is the operating
manual: the exact commands CI runs, the failure modes this repo has
already hit, and the review invariants in
docs/reviewing.md. CLAUDE.md is a pointer to
the same file.
A change is ready when CI is green and every actionable CodeRabbit thread on the latest commit is resolved (or declined on the record). Commits follow Conventional Commits and carry no AI attribution.
Small, well-specified work is tagged
good first issue.
New modules start as an issue, not a PR — see
CONTRIBUTING.md.
A control plane, not a single game. The box gives an event its shared spine — a GitHub org, team registration, a live leaderboard, an organizer admin panel, and the scoring pipeline that feeds it. Modules plug challenge content into that spine, and any subset can run alone or together: patch-to-score Secure Development, a Quiz bank, a Jeopardy board, and externally hosted AI challenges. The module contract is the boundary between spine and content, so the box is built to host further modules — forensics, API-security, cloud — as they land.
Why it exists. The Secure Development module teaches defence rather than attack, and it is a genuinely good way to teach secure coding. Until now, running one meant standing up Vercel, Upstash, Lambda and DynamoDB, holding the cloud bill, and having access to a private scoring image. That is a reasonable ask for a conference with a budget. It is an unreasonable ask for a university security course, a high-school club, an OWASP chapter night, or a weekend workshop.
This kit removes it. Everything runs from Docker Compose on one machine you already have — a laptop, a spare desktop, a small VPS — plus one free GitHub org for the forks. The rubrics for all six targets ship inside the box, so there is no private image to request and no scoring code to write. Nothing is billed, nothing phones home, and when the event ends you archive the repos and stop the stack.
Who it's for: anyone who wants to run this event and does not want to become a cloud operator to do it — course instructors, club organizers, OWASP chapter leads, workshop facilitators, security teams running an internal training day.
Deployed and exercised end to end; not yet run for a real cohort. The
full scoring path ships in-kit — the scorer's bearer-authed POST /score, the
self-contained scoring workflow for the forks, the poll transport — and
scripts/smoke.sh drives that whole pipeline against mocks. Beyond that,
the kit runs continuously on a hosted box from the same Compose file this
repo ships, GET /health reports the exact revision serving it, and an
end-to-end pass over that live instance is where a batch of real defects were
found and fixed — the sort a mocked suite cannot see.
What has not happened is a real event: a cohort of contestants opening real PRs against real forks, at once, for hours. That is the gap between "the pipeline works" and "the pipeline works at 40 people". Two caveats are open rather than buried: the Security Shepherd result matcher has a stated residual limit (an unusually-phrased refusal can still read as a solve — it can under-credit a correct patch, never award a free point), and the load profile of a full cohort is untested. Detail and current state: Status and upstream dependencies.
What it does that those don't: patch-to-score defence training graded through GitHub pull requests, a module contract for mixing game types on one leaderboard, and a control plane you own end to end — one box, one free org, no cloud bill, no telemetry.
This project is not affiliated with or endorsed by the OWASP Foundation. Four of the six vulnerable targets are OWASP projects (Juice Shop, WebGoat, Security Shepherd, VulnerableApp); DVWA and VAmPI are community projects.
See it running in two minutes — no GitHub org, no OAuth app, nothing to
configure. You need Docker with Compose v2 and openssl:
git clone https://github.com/OWASP/owasp-ctf-in-a-box
cd owasp-ctf-in-a-box
./scripts/dev-stack up
It writes throwaway local secrets, builds the scorer and app images, brings
the stack up, seeds a demo leaderboard through the scorer's real scoring API,
and prints the URL to open. You should see the leaderboard with seeded teams
and a score-over-time graph; ./scripts/dev-stack score <login> juice-shop 3
lands three more solves live. ./scripts/dev-stack down tears it down.
Run a real event with the guided wizard. Add the gh
CLI (authenticated), plus one free GitHub org
if the event runs Secure Development; ./setup/ctf-setup.sh check verifies
the tooling first:
./setup/ctf-setup.sh # guided, prompts for values, resumable
It asks for each value as it goes — your box URL, the event org, the admin
logins, whether you run Secure Development, the GitHub credentials — writes
.env, does
every automatable step, guides you through the GitHub-UI ones, and resumes if
you stop and come back. Everything else (the event's name, which modules run,
which targets) is a runtime /admin setting, so there is no config file to
edit. It asks only what you actually need: an event without Secure
Development needs no org, no forks, and no scorer image, and is never asked
about them. Preview any mutating step with --dry-run — it narrates steps
4–9 from a .env that is already complete, and refuses (by design) when
there is no admin login, or when Secure Development is on with no org. The
wizard closes by running ./setup/ctf-setup.sh doctor — a per-fork status
matrix you can re-run at any time — and then offers an optional fly.io
deploy (default no), so putting the same event on a public hostname is a
guided flow — the hostname, a previewed deploy, then a confirmation — rather
than a trip through the deploy docs.
Want the details? Every discrete subcommand, each UI-only step, and how
the two GitHub apps differ:
docs/hosting.md.
In a cloud instead? docs/aws.md (Terraform: ECS Fargate,
ElastiCache and an ALB — apply up / destroy down) or
docs/fly.md (one Fly machine).
Secure Development — fork a deliberately vulnerable app, find the flaw, patch it, open a PR. A GitHub Action in the fork runs the target's rubric against the patch and the score lands on the leaderboard (~30 s later in poll mode). Six targets, 321 challenges; stock scores 0, a correct patch earns its points — gated in both directions. Needs the GitHub org and the scoring pipeline.
Quiz — single- and multi-select security questions, graded in the app
the moment they're answered (all-or-nothing on multi-select), with an attempt
cap and retry cooldown. Authored from /admin one at a time or imported and
exported as one JSON bundle. Needs no GitHub, no forks, no pipeline.
Jeopardy — a board of organizer-authored flags in
categories. Submissions are trimmed and normalised, casing forgiven unless a
flag is marked case-sensitive (its card says so), with a submission cooldown
and optional paid hints. Same /admin + JSON-bundle authoring as the quiz.
Needs no GitHub either.
AI — prompt-injection and guardrail challenges hosted outside the box. Each contestant's challenge page mints them a personal launch link to the external site; a solve reports back to the leaderboard, either through that site's own callback or a flag typed back into the app. Needs no GitHub, no forks, no pipeline.
Around whichever modules you enable, the platform provides: team
self-registration with captains, join codes and /join/<code> links (solo
play is a team of one; a flag solved by several teammates counts once); the
live leaderboard with a CTFd-style score-over-time graph from real per-solve
timestamps; the allowlisted /admin panel — freeze, scoring and registration
windows, hints and costs, team cap, cooldowns, module content, per-contestant
support actions, an activity stream and engagement metrics — all runtime, no
rebuild; and a capped audit log on every admin action.
| Contestant breakdown | Challenge browser |
|---|---|
![]() | ![]() |
| Jeopardy flag board | Quiz |
|---|---|
![]() | ![]() |
Captured from the contestant app running locally via scripts/dev-stack up
with seeded demo players. Targets and fork links are event-config driven; the
event name and the rest of its branding are admin-panel settings.
One Docker Compose stack: Caddy terminates TLS in front of the Next.js app;
the app talks to Redis only through srh (an Upstash-compatible REST proxy) —
the network is split so nothing internet-facing has a route to redis:6379.
Quiz, Jeopardy and AI grade inside the app and bank points straight to Redis.
Secure Development is graded outside the box: the contestant's fork runs a
GitHub Action that boots the target, runs the rubric against the patch, and
posts a machine-readable score comment on the PR. The sync poller pulls
those comments — zero inbound network surface, so the box works behind NAT and
on venue wifi (that is the only transport: push ingest was removed in v0.6,
see #377). The score
enters through a single audited writer:
the scorer's bearer-authed POST /score, which validates and writes
monotonically — solves are never un-solved by a later failing run.
The full picture — components, the nine-step score data flow, the security model — is in docs/architecture.md.
This module's content is a set of vulnerable targets and their scoring
rubrics. Contestants pick a target, fork the org's copy, patch it, and
open a PR. Each target's challenges are executable node:test suites, priced
by difficulty.
Counts are maintained by hand and pinned to the vendored rubric by
apps/web/src/lib/tests/apps-catalogue.test.ts — re-check them
after a vendor-rubric.sh bump. Reference patches
that prove a correct fix scores (the positive-direction gate) live separately
under patches/.
The rubrics live in scorer/rubric.owasp/, vendored from
OWASP-CTF/dc34-owasp-secure-development-ctf
and pinned to the single upstream commit recorded in
scorer/rubric.owasp/PROVENANCE.md. Re-vendor against a newer commit with:
./scripts/vendor-rubric.sh --all --ref <sha>
Two rubric shapes are supported at once, and a single rubric directory may mix
them: <target>.yaml files use the declarative HTTP request/expect probe
grammar, and <target>/tests/challenges/ directories use executable tests
priced by catalogue.<target>.json. Authoring guide:
docs/scorer.md.
On rubric secrecy. These rubrics are public. The targets are open source and their solutions are already published, so the kit treats rubric privacy as protection against check-gaming rather than against knowing the answers — an accepted trade-off for a self-hosted event. Override with your own private rubric at any time:
cp -r /path/to/private-rubric scorer/rubric
docker build -t ghcr.io/<org>/score:latest --build-arg RUBRIC_DIR=rubric scorer/
scorer/rubric/ is gitignored and reserved for exactly this.
Once the stack is up at your EVENT_URL:
/admin: freeze the leaderboard, open and close
registration, set the schedule, author quiz questions, classic challenges
and ai challenges — and when one contestant gets stuck, fix that one
contestant rather than resetting the event.docker compose logs -f sync (it runs with
secure-development enabled). All state lives in named Docker volumes, so
a box reboot loses nothing../setup/ctf-setup.sh teardown archives the target
repos — then uninstall the GitHub App and delete the org's Actions secrets
yourself. An event without secure-development has no forks to archive.Teams, the admin panel, verifying the kit before the day, and the local dev-stack are all covered in docs/operations.md; prerequisites, the score transport, OAuth setup and event config in docs/hosting.md.
The full reasoning, alternatives, and trade-offs are recorded as numbered ADRs in docs/decisions.md.
Rendered at owasp.github.io/owasp-ctf-in-a-box.
Contributions welcome — CONTRIBUTING.md covers the dev environment, the CI gates, and how to propose a module; CODE_OF_CONDUCT.md applies.
Agents should follow AGENTS.md. The commands below match CI;
make help lists the same targets.
Each service tests independently (Node 22 across the board):
(cd sync && npm ci && npm test)
(cd scorer && npm ci && npm test && node tools/vacuous-sweep.mjs)
./scripts/acceptance-scorer.sh # from the repo root — the script lives in scripts/
(cd apps/web && corepack pnpm install --frozen-lockfile && corepack pnpm lint && corepack pnpm test)
./scripts/smoke.sh # the full poll pipeline, end to end
Found a vulnerability in the kit itself? SECURITY.md — the targets' vulnerabilities are intentional and out of scope.
MIT — see LICENSE. The rubric content under scorer/rubric.owasp/
is vendored from the upstream
OWASP-CTF
event, pinned to the commit in scorer/rubric.owasp/PROVENANCE.md — this kit
exists because that event was worth running more than once. The vulnerable
targets are not vendored: events fork them from their own upstreams
(Juice Shop,
WebGoat,
DVWA,
Security Shepherd,
VulnerableApp,
VAmPI), and each keeps its own license.
OWASP® is a registered trademark of the OWASP Foundation; this project is not
affiliated with or endorsed by it.
| Target | Challenges | Points | Notes |
|---|
vulnerableapp | 110 | 187 | Largest target; scored 8-way parallel |
webgoat | 69 | 137 | Two-stage build: Maven, then the fork's runtime-only Dockerfile |
dvwa | 55 | 108 | Needs a MariaDB sibling and a schema init |
securityshepherd | 40 | 79 | HTTPS, three-container stack, strictly serial |
juice-shop | 38 | 141 | The only target whose difficulty runs to 6 stars |
vampi | 9 | 16 | Self-contained; the quickest end-to-end proof |
| Total | 321 | 668 | Every event provisions all six; pick a subset in /admin → Secure Development → Targets |
| Read this when you're… | Document |
|---|
| Standing the kit up | docs/hosting.md — prerequisites, the wizard and every discrete step, how scores reach the box, the GitHub OAuth app, event config |
| Deploying to a cloud | docs/aws.md (Terraform: ECS Fargate + ElastiCache + ALB) · docs/fly.md (one Fly machine) |
| About to open the doors | docs/security-checklist.md — the one-page pre-event walk |
| Running the event | docs/operations.md — teams, the admin panel, the quiz/classic/ai organizer guides, verifying, teardown |
| Understanding the system | docs/architecture.md — diagram, score data flow, Redis keys, security model, testing strategy |
| Writing a rubric | docs/scorer.md — serve + judge modes, both rubric grammars, authoring and building |
| Building a new module | docs/modules.md — the platform/module contract |
| Asking "why is it like this?" | docs/decisions.md — numbered ADRs |