On the weekend of the 1st of August 2026, we ran Universal CTF, a 32-hour jeopardy CTF on the managed Kubernetes service provided by Infomaniak. This is our second one. Our first iteration at hosting a CTF happened here: How (not) to host your CTF. Different challenges, different infrastructure, a fresh set of mistakes to learn from.
It runs on UCTF, the platform we built so anyone can host their own CTF: managed CTFd, per-team instanced challenges, storage and git sync included. The cluster underneath is sponsored by Infomaniak, whose managed Kubernetes I also presented in a previous post. If you want to run a CTF without building all of this yourself, that is what UCTF is for.
608 players in 314 teams worked through 38 challenges across 10 categories. Teams that needed their own isolated environment got one on demand, so across the event the cluster scheduled 3,807 short-lived pods.
We recorded the whole thing as it happened: the CTFd database for the competition side, a Prometheus stack for the infrastructure side. What follows is that recording, played back. Press play and drag the timelines.
By the numbers
A few headline figures. Fewer than half the registered teams made it onto the scoreboard, about one submission in four was a correct flag, and HackSmiths opened the board 84 seconds after the start.
The leaderboard race
Every solve, played back fast. Each bar is a team and the leader is gold. Change the speed or drag the timeline: you can watch the scramble in the first hour and the sprint during the second wave, with a long quiet stretch overnight in between. The title came down to 0 points, with Aju speed finishing on 7,456.
Living infrastructure
Each cube is one live pod on the cluster. Challenge instances are coloured by category, and the CTFd platform itself (the web replicas, MariaDB, Redis) sits in slate. A cube drops onto whichever node the scheduler picked, then disappears once the team is done with it. Floor tiles go from green to red as a node fills up. Drag to orbit the cluster, and hover or click a node to see what is running on it.
At the busiest moment 48 isolated environments were running at once, and the cluster barely noticed: 8.6 CPU cores and 48.3 GiB of memory across 35 nodes.
Traffic flow
Player traffic hitting the cluster. Particles run from the internet through the gateway to the challenge pods and back, and their density follows the real throughput at that point in time. It topped out at 67.9 MB/s inbound, with 808.6 GB received and 315.3 GB sent across the weekend.
How UCTF runs it
Everything you just saw ran on UCTF, the platform we built for hosting CTFs without wiring up your own infrastructure. A few Go services on the cluster do the heavy lifting, all of them shipped with UCTF. Monitoring is the one thing we ran ourselves, because the platform does not cover it yet.
The cluster itself is set up with two choices that shaped what we could build: vcluster for tenant isolation and Sysbox as the container runtime. Together they made realistic Kubernetes challenges possible.
flowchart TB fnl["Fenouil<br/>workspace UI"] --> bas["Basilic<br/>API · auth · Stripe"] bas --> gs["GitSync"] bas --> saf["Safran"] gs["GitSync<br/>GitHub repo → Flux CD"] --> can["Cannelle<br/>Kubernetes operator"] can --> ctfd["CTFd platform"] can --> pods["Challenge pods<br/>per-team · isolated"] saf["Safran<br/>S3-compatible broker"] --> r2["Cloudflare R2"] ctfd -.-> saf pods -.-> saf
GitSync Challenges are YAML files in a GitHub repo, not fields in a form. You push a commit, Flux CD reconciles it into the cluster, and Cannelle picks it up from there. No deploy button, no copy-paste. Every update is a commit and every deploy is a push.
Safran An S3-compatible proxy between the cluster and the upstream object store (Cloudflare R2, in our case). Each tenant gets a virtual bucket with its own credentials, access rules, and storage quotas. CTFd and challenge pods talk to it like it is plain S3. It barely matters that the underlying store is R2.
Cannelle A Kubernetes operator that turns challenge definitions into actual pods and services. For instanced challenges it spins up a per-team pod with its own flag. For static challenges it creates a CTFd record and stops there. It manages the network policy, TTL expiry, health probes, and the full lifecycle of every challenge container. 3,807 pods came and went over the weekend, all handled by Cannelle. Nobody typed kubectl.
vcluster Each CTF event gets its own virtual Kubernetes cluster, not a shared namespace on a single cluster. Every team pod, every CTFd deployment, every Cannelle resource lives inside one of these vclusters with its own API server, its own RBAC, and its own set of nodes. From the operator’s perspective it is just another Kubernetes. From ours it is one tenant among a few dozen on the same host, completely isolated from the others.
Sysbox The cluster nodes run Sysbox as their OCI runtime instead of runc. This makes nested containers work without privilege escalation. A challenge pod can run Docker inside itself, or even a full K3s cluster. We used this to build Kubernetes challenges that deployed a 3-pod cluster with real networking between them plus an init pod that provisioned it. From the solver’s perspective it was just a K3s cluster they could kubectl into. The scheduler treated it as a handful of pods on regular nodes.
First blood
The first team to solve each challenge, in the order it happened. 36 first bloods over the 32 hours, and two challenges nobody solved at all.
Charts
The same data as plain charts, if you would rather read it than watch it.
Solve activity over time
Correct flags per 15 minute window. A big spike at the open, a quiet night, another bump in the morning.
Difficulty map
Each dot is a challenge: solves against its point value, which drops as more teams solve it. Hover for names.
Solves by category
Where the points went, across the ten categories.
Infrastructure through the event
Four measures on a shared time axis, each with its own scale: instances, CPU, memory and monitored targets.
Score distribution
Final score by rank. A steep top, then a long tail across all 146 scoring teams.
Engagement funnel
From sign-up to the scoreboard.
The data was captured from the CTFd MariaDB and a kube-prometheus stack, backed up with a repeatable script, restored locally, and rendered from static JSON. This page never queries a live service. Point values are the final dynamic scores. Times are UTC.