OSCTF
Open, self-hostable infrastructure for cybersecurity competitions, labs, and training. Every team gets its own isolated challenge container — its own port, its own network, its own flag — created on demand and torn down by a built-in scheduler. One Go binary, one compose file.
Stack: Go, Postgres, Redis, Docker, gRPC, React, MinIO
GitHub · Live
The problem: Most CTF platforms hand every team the same shared challenge instance. One team crashes the service and the whole event stalls; one team finds the flag and can hand it to the room. OSCTF gives each team its own container — its own port, its own network, an optional per-team unique flag — created on demand and lifecycle-managed by a built-in scheduler (TTL, extend, per-team quota, automatic teardown at event end). That per-team-instance model, not the scoreboard, is the reason to reach for it over a CTFd-class tool.
One binary: A Go modular monolith serves the JSON API and the embedded React dashboard together. Postgres is the source of truth, Redis holds ephemeral state (sessions, rate limits, the board cache), MinIO stores attachments, the host Docker daemon runs the per-team containers, and 0..N out-of-process plugins talk over gRPC. Deployment is a clone and a docker compose up — no cloud account, no license key, no external services.
Isolation that fails closed: Per-team network isolation is only enforceable on Linux. On Docker Desktop, a published port is reachable across networks, so the isolation simply isn't there. Rather than quietly degrade, OSCTF refuses to start container instances on any daemon whose isolation it can't verify. The override exists, is documented as local-trial-only, and logs loudly.
Correctness under concurrency: Flag submission computes a plugin challenge-type's verdict before the transaction, so a plugin call never happens inside the row lock. Inside SELECT … FOR UPDATE the deleted/swapped checks run before the solved/attempt checks, so a challenge changed mid-submit costs the player no attempt. The scoreboard is consistent by construction rather than by timing: plugin scores are locked at solve and read from a per-solve record, so the served board equals a from-scratch recompute over the solve log even with every plugin down, and a background worker backfills missing records off the read path.
Plugins: Four extensible types — auth, scoring, notification, and challenge type — over a versioned gRPC ABI, each proven by a reference implementation. The loader runs an eight-state supervisor with guarded transitions, register-on-ready / revert-before-death, a two-level in-flight budget, and an ordered shutdown (HTTP drain → plugin drain → background workers) under one shared budget. Boot runs in a goroutine and never gates HTTP serving. An auth plugin asserts an identity; the core decides what that may mean, and enforces it field by field.
Status: v0.1 (MVP) → v0.2 (per-team instances and the scheduler), hardened across v0.2.1 (security), v0.2.2 (concurrency), and v0.2.3 (scoreboard consistency). v0.3 shipped the canonical /api/v1 surface, scoped API tokens, and the plugin system. Next is the osctf CLI and an MCP server over API v1.
Roadmap — AI-security challenges: A challenge family whose target is a live LLM agent (a system prompt, optional tools, an optional retrieval corpus) that a competitor attacks over multiple turns — prompt injection, indirect injection, tool abuse, guardrail bypass, system-prompt extraction — instead of submitting a static flag. It lands as a ChallengeType plugin rather than a core change. Designed, not built.
The durable goal is the infrastructure layer, not the scoreboard. CTFs are the entry point; the aim is the open platform universities, communities, and companies build their security education on. Apache 2.0.
swayam.codes