From zero to 18 flags: architecture, UX, challenge design, deployment, and launch strategy behind KubeArmor AI CTF.
Static workshops don't teach security. They teach patience. You sit through slides, nod at diagrams, and leave with zero hands-on reps. So instead of running another workshop, I built a full browser-first CTF platform in one day.
I work DevRel at KubeArmor. The goal was simple: give AI security learners something they could actually play, not just watch.
The result shipped with 3 tracks, 15 primary challenges, and 3 bonus flags, for 18 flags total. Registration and the scoreboard run on CTFd. The gameplay runs on a custom browser app I built. Solve checks are deterministic, not model-dependent, and the whole thing deploys through Docker.
This is the build journal: the decisions, the tradeoffs, and the exact order I built things in to make one day work without the platform falling apart under real players.
Why Browser First Decided Everything Else
The first decision was strategic, not technical. If participation has friction, conversion drops. A CTF that demands local setup loses people before they see a single challenge. So the event had to run entirely in the browser.
That single choice split the platform into three pieces: CTFd for registration and official scoring, a custom browser app for the actual game experience, and a backend runtime that keeps secrets and challenge logic server side. Players get a scoreboard workflow they already trust from other CTFs. I get to build AI-native challenge interaction without fighting a plugin system that was never designed for it. And because the runtime, not the browser, holds the challenge logic, solve checks stay deterministic no matter what a player's browser can see.
The Build Order That Kept One Day On Schedule
My rule for this build: do not start from individual challenges. Start from the platform they run on.
I have watched CTFs sink into content sprawl before the runtime even works: a pile of half-formed challenge ideas and no working platform under them. So I locked the scope first. Three tracks, five primary challenges each, one bonus per track, CTFd as the scoring source of truth, the browser app as where players actually solve. Any ambiguity at this stage would have cost hours of rework later, so I fixed it before writing a line of challenge content.
With scope locked, I built the runtime shell next: Express and EJS, server-rendered pages that are simple to debug under time pressure. I was not trying to win a framework debate. I was trying to ship.
Before authoring the full challenge catalog, I validated one complete solve path per track, end to end: Prompt Injection, Agent Workflow, and Hidden API. Only after those three slices worked did I fill out the rest of the challenge JSON and the bonus progression logic. Admin and operational controls, reset paths, session visibility, the solution library, came last, because those exist for organizers, not for the players who show up first.
The one rule I broke early and paid for: AI output varies from run to run, so solve logic can't rely purely on model text. I paired every prompt flow with a deterministic regex or action check, which is what kept challenges solvable even when the model's phrasing drifted mid-event.
Architecture: Browser Game, Deterministic Checks, CTFd Scoreboard
Every challenge runs in the browser: prompt manipulation tasks, constrained agent workflow attacks, hidden API tamper paths. Nobody needs a local AI setup to play.
Challenges themselves live as JSON, loaded dynamically, so I could tune metadata, system prompt behavior, flag values, solve patterns, and track-level bonus progression without touching platform code. When the live LLM went unavailable or gave an inconsistent response during testing, a fallback mode kept the deterministic checks running anyway. That fallback saved a testing session more than once: I could keep verifying challenge logic even when the model itself was misbehaving.
Provider keys stay server side, with no raw secrets exposed to the front end. Solve state tracks per user session, and admin access to the control operations is role-based.
The UI Rule: Understand The Objective In Ten Seconds
I treated this as a product, not a challenge dump. I have seen enough CTF sites that are a bare list of problem statements and a submit box, and that shape works for people who already know CTFs, not for someone trying it for the first time.
So the home page works as a mission board with progress context, not a login screen. Track pages show the challenge list and status in one view. Challenge pages keep the conversation, the solved state, and the prompt box in a single loop, so nobody has to switch tabs to remember what they were doing. A player should understand what to do within seconds, see their progress at a glance, and get from home to track to challenge in one click.
For organizers, I built an admin dashboard with rate limit visibility and user management. It also gives quick access to solution guides, because a platform that's hard to run doesn't get run twice.
Testing For Trust, Not Coverage
The fastest way to lose a CTF's credibility is inconsistent solve behavior. If someone submits the right answer and the platform doesn't register it, that player stops trusting the event. So I prioritized testing ergonomics over coverage metrics: deterministic solve checks in the runtime, reset paths for fast retesting, seeded demo users, visible solve status per challenge, and structured session storage I could inspect directly.
Then three passes, each from a different starting point. Could I solve from a blank state quickly, could a peer solve using only the hints I'd written, and could someone with zero context finish a challenge with nothing but what's on the page? That third pass is the one most builders skip under deadline pressure, and it's the one that catches the UX gaps a creator is blind to after staring at their own platform all day.
CTFd Integration And Deployment
The deployment model is intentionally hybrid. CTFd handles registration, team context, and the scoreboard, because that's a solved problem and players already trust it. The custom platform handles everything CTFd was never built for: AI interaction, challenge runtime, deterministic solve checks. Forcing that into a CTFd plugin would have meant fighting the plugin system instead of building the game.
The runtime ships as a containerized Docker Compose stack, with a persistent volume for session and solve state and a root-level environment file for provider keys and rate limits. That gave me a local Node process for fast iteration during the build and a repeatable container for the actual event, with predictable data paths for backup and restore. For a DevRel-led event, setup friction for the organizer running it matters as much as friction for the player joining it, so Docker went in on day one instead of getting bolted on afterward.
Difficulty Design
Difficulty was layered on purpose: easy challenges build early confidence, medium stages teach the deeper patterns, hard stages demand chaining and precision, and bonus flags reward finishing a full track.
In testing, prompt injection challenges felt like the fastest track to clear. The workflow and hidden API attacks demanded chained, multi-step reasoning, and took noticeably longer to work through. That gap was the intended shape, not an accident: a beginner should finish a meaningful chunk of the event, and an experienced player should still hit real depth by the end.
How I Actually Vibe-Coded This
I used GitHub Copilot for most of the implementation. The speed came from fixing the intent and boundaries of each slice before I ever opened the editor, then using Copilot as a fast implementation partner inside those boundaries rather than accepting whatever it generated first.
The loop, every time: I wrote a clear objective for one thin vertical slice, one challenge page, one solve path, one admin operation. I drafted candidates with Copilot, validated the behavior immediately in the browser and in session data, and kept only the code that matched the deterministic solve logic and the product flow.
Vibe coding gave me speed. Fixed scope, deterministic solve checks, and that thin-slice discipline kept the quality from sliding, because scope never drifted mid-build.
For UI exploration, I used Google Stitch as a design-thinking accelerator, not as final output. It generated layout directions and interaction ordering fast, and I translated the ideas I liked into the Express and EJS runtime by hand, which kept the reliability guarantees intact. Stitch helped most on the homepage's information hierarchy and the track page's scannability, and it shaped the rhythm of the challenge pages too.
How I Wrote Prompts During The Build
Prompt quality was the hidden multiplier. I didn't write one giant prompt and hope. Every prompt followed the same shape. The objective named what exactly should get built in this step. The context listed the files, routes, and constraints in play.
The guardrails named what must not change. The output contract set how I wanted the result formatted, and the verification named what should be true once it was done.
An example from the actual implementation work:
"Implement a deterministic solve check for this challenge route. Keep existing API shape unchanged. Do not move secrets client-side. Return minimal patch. Add only required validation and update solved state in session."
For content and UX iteration:
"Rewrite this challenge instruction so a first-time participant can act within 10 seconds without losing technical depth. Keep tone assertive and concise."
For debugging:
"Given this route and this observed behavior, list the top three likely failure points, then propose the smallest safe fix first."
Prompt granularity mattered more than anything else. Mixing an architecture decision with a copywriting pass and a debugging session in one prompt produced garbage every time. Smaller prompts and tighter loops meant fewer regressions to chase down later.
What I'd Improve Next
At larger scale, four things would need to change:
Analytics would need to go deeper, down to the per-challenge step and drop-off point.
The event would need richer anti-abuse controls, since a public run draws different behavior than an internal one.
Flags would need a one-click relay straight into the CTFd API, instead of the manual sync I ran during the event.
Reporting would need to cover learning outcomes after the event, not just the final scores.
The architecture held up because the split held: CTFd never had to pretend to be a game engine, and the custom runtime never had to pretend to be a scoreboard. That's the part I'd keep unchanged even at ten times the scale.





















