Overview
Infrastructure built for running small-to-medium CTF competitions. The design priority was isolation: every team gets its own challenge instance with no shared state. Exploiting one team's environment should not affect another's.
Architecture
Each challenge is a Docker image with a defined interface: a TCP port, an HTTP endpoint, or a shell. The orchestrator spins instances per team on demand and assigns random external ports mapped through Nginx.
[Team A] -> nginx:10001 -> challenge-a:team-1 (container)
[Team B] -> nginx:10002 -> challenge-a:team-2 (container)
A lightweight Python service manages the lifecycle: spawn on first connection, idle timeout after 30 minutes, hard limit of 2 hours per team per challenge.
Scoreboard
The scoreboard is intentionally minimal. A SQLite database, a Python backend, and a single-page frontend. No real-time websockets, no live leaderboard animations. Scores update on page refresh.
Flags are validated server-side against per-team HMAC-signed tokens generated at deployment. The flag for a given challenge is different for every team, which eliminates flag sharing between teams at the protocol level.
Lessons
The per-container model is more expensive than a shared process model but worth it for CTFs involving pwn challenges. One bad exploit should not take down the challenge for everyone else.
The SQLite + no-realtime-updates scoreboard held up fine for 60 concurrent teams. The overhead from Docker per-team instances was the actual bottleneck, not the database.