I am the Vice President of DDC — Digital Defence Club at Chaitanya Bharathi Institute of Technology (CBIT). this year in feb , we organised a national-level cybersecurity conclave: a full-day event combining workshops, a hackathon, and a 36-hour CTF running in parallel.
My job had two parts. First: build the entire CTF platform from scratch. Second: be on-site monitoring the live competition for the full 36 hours while 100+ participants competed.
This is the engineering post-mortem. What I built, why I built it the way I did, and what I learned running a CTF at scale under a real deadline with real stakes.
The Event: CyberFest 2026
CyberFest was structured as three back-to-back tracks:
- Workshops — Security professionals and students running hands-on sessions (web exploitation, forensics, OSINT, malware analysis)
- Hackathon — A parallel 24-hour build challenge
- CTF — Our 36-hour Capture The Flag, which I both built and monitored live
The CTF ran on a platform I wrote entirely myself — backend, frontend, admin panel, scoreboard, challenge files, and auth flow. DDC as a club handled logistics and judging. I handled infrastructure.
The Constraints That Shaped Everything
Before writing a single line of code, I listed my hard constraints:
- Teams, not individuals. CyberFest runs on team-based participation. Auth had to be by team name—no passwords, no email verification noise.
- Admin-gated registration. A shared admin key would let me gate who could register. With a national-level event, you cannot have open registration.
- Mix of static and downloadable challenges. Some challenges would be pure text (OSINT). Others needed zip file downloads (forensics artefacts, APKs, binaries). The architecture had to handle both cleanly.
- Real-time scoreboard. A 36-hour competition with live teams needs a live scoreboard — not one that updates every 5 minutes.
- Zero budget. DDC is a student club. Deployed on Render free tier. MongoDB Atlas free cluster. No paid infrastructure.
Designing the Challenge Taxonomy
I landed on a tiered difficulty system with eight distinct categories. The final event ran 121 challenges — far more than the initial 51 I planned. As the event date approached, I kept writing more. Every new category got its own seed script.
| Tier / Category | Count | Points | Notes |
|---|---|---|---|
| Easy | 16 | 100 pts | Forensics, Reversing, Crypto, Misc, Stego |
| Medium | 35 | 200 pts | Crypto, Forensics, Stego, Reversing, Misc |
| Hard | 10 | 300 pts | Reversing |
| Insane | 20 | 500 pts | Crypto |
| OSINT | 20 | 150 pts | Text-only, no files |
| Web | 11 | 200–400 pts | Web exploitation |
| APK | 8 | 250–400 pts | Android APK reversing |
| AI | 5 | 300 pts | LLM prompt injection / model analysis |
| Master Web | 11 | 500→50 pts | Dynamic scoring, decay-based |
| Total | ~121 | — | Deployed on Render + Vercel |
The Master Web category was the most ambitious. Instead of static points, I implemented dynamic scoring: a challenge starts at 500 points and decays with each additional solve (500 → 450 → … → 50 minimum). The live site for those challenges was deployed separately at cyberfest2026-roan.vercel.app — participants had to find flags hidden inside a real React app’s source, console, service workers, WebAssembly comments, and JSON-LD metadata.
The APK challenges required the most tooling — I built generate_apk_challenges.py specifically to embed flags inside compiled Android bytecode and resource files at varying obfuscation levels.
The OSINT challenges were my favourite to design. No file downloads. Just a question, your brain, and the open internet. Twenty questions across geospatial intelligence, social media forensics, corporate recon, and historical data archeology.
The Backend: Node.js + Express + MongoDB
I chose Node.js with Express for speed of development and Mongoose for MongoDB ODM. The schema design was the most critical part.
The Team Model
// models/Team.js
const TeamSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true, trim: true },
token: { type: String }, // JWT stored for session continuity
solves: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Submission' }],
score: { type: Number, default: 0 },
createdAt: { type: Date, default: Date.now }
});
No passwords. A team name is your identity. Admin key gates creation.
The Submission Model
// models/Submission.js
const SubmissionSchema = new mongoose.Schema({
team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team', required: true },
challenge: { type: mongoose.Schema.Types.ObjectId, ref: 'Challenge', required: true },
submittedAt: { type: Date, default: Date.now },
correct: { type: Boolean, required: true }
});
Tracking both correct and incorrect submissions gives you rich audit logs for post-event analysis.
Auth Flow
Registration requires a team name and the admin secret. On success, a JWT (24h expiry) is issued. On subsequent visits, teams log in with just their team name—the server verifies the JWT from localStorage. This keeps the UX dead simple while maintaining server-side authority.
// routes/auth.js (simplified)
router.post('/register', async (req, res) => {
const { teamName, adminKey } = req.body;
if (adminKey !== process.env.ADMIN_SECRET) {
return res.status(403).json({ error: 'Invalid admin key' });
}
const team = await Team.create({ name: teamName });
const token = jwt.sign({ teamId: team._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.json({ token });
});
Security I Actually Cared About
CTF platforms are a delicious irony: a security-focused event running on infrastructure that gets attacked constantly by the very people you invited. I had to think defensively.
Rate Limiting: Flag submissions are rate-limited to 10 per minute per team. Auth endpoints allow 20 requests per 15 minutes. This prevents brute-force flag guessing cold.
Input Sanitization: All flag and team name inputs are trimmed, length-checked, and passed through Mongoose’s schema validation before touching the database.
CORS Hardening: Only the explicit Render frontend URL is whitelisted. No wildcards.
JWT + No Passwords: There’s nothing to leak. Team auth tokens expire in 24 hours and are re-issued on login.
The flag format was locked to cyberfest{...} to make flag parsing deterministic and prevent ambiguity.
The Frontend: Cobalt Blue Aesthetic
The frontend is intentionally vanilla HTML/CSS/JS—no React, no build step, no bundler. This was a deliberate choice: simplicity of deployment. A static site on Render deploys in under 60 seconds.
The visual theme is Cobalt Blue on Black with White accents. It reads as “hacker aesthetic” without being a tired green-on-black terminal cliché.
Three views:
index.html— Main CTF platform (challenges, scoreboard, team management)admin.html— Admin panel (create/edit/delete challenges, view teams)- Real-time scoreboard — Updates live via polling, showing ranked teams with solve counts, scores, and timestamps of last correct submission.
The Seed Script Problem
121 challenges. Each with a title, description, difficulty, category, point value, flag, and optional file URL. Entering this manually via the admin panel would have taken days and been catastrophically error-prone.
I wrote backend/seed.js. One npm run seed command populates the entire database from a structured JSON definition. Idempotent — can be re-run safely. This turned what would have been a multi-day data entry job into a two-second database operation.
For a national-level event where challenge integrity is non-negotiable, this was not optional — it was the only sane path.
Deployment Architecture on Render
Render (Free Tier)
├── Web Service ← Node.js backend (root: /backend, start: npm start)
└── Static Site ← HTML frontend (root: /frontend, no build step)
MongoDB Atlas
└── Free Cluster ← Mongoose models (Team, Challenge, Submission)
Environment variables: MONGODB_URI, JWT_SECRET, ADMIN_SECRET, FRONTEND_URL. That’s it. The entire secret surface area of the platform is four env vars.
36 Hours: Monitoring the Live Event
Building the platform was one thing. Being on-site as the sole monitor for a 36-hour live CTF was a completely different kind of pressure.
My responsibilities during the event:
- Flag dispute resolution — when a team claimed their exact flag was correct but the system rejected it (usually a trailing space or encoding issue), I had to investigate and adjudicate in real time
- Infrastructure babysitting — watching Render logs for abnormal submission spikes, keeping an eye on MongoDB Atlas cluster metrics
- Rate limit tuning — I had to tighten the flag submission rate limit mid-event because one team was hitting it hard with scripted brute-force attempts
- Challenge clarifications — fielding questions from teams on Discord about ambiguous challenge descriptions, issuing clarification edits via the admin panel live
- Scoreboard integrity — manually verifying the top-5 scoreboard before announcing winners to ensure no submission anomalies
The platform held for all 36 hours. Zero downtime. Zero flag leaks. The Render free tier managed the load fine because I’d been careful with connection pooling and had index coverage on every query the scoreboard used.
The hardest part wasn’t technical — it was staying sharp on hour 30. That’s when the most intense scoreboard drama happened.
What I’d Do Differently
First-blood tracking. The first team to solve a challenge deserves recognition on the UI—a small badge or highlight. I didn’t build this and I wish I had.
WebSocket scoreboard. I used polling for the live scoreboard. With a bit more time, a proper WebSocket connection (socket.io) would eliminate even the 5-second lag.
The Challenge Generation Scripts
One thing I built that saved me enormous time: two Python scripts that live in the repo root.
generate_challenges.py — takes a structured definition of each challenge (title, description, difficulty, flag, category) and programmatically builds out the challenge objects ready to pipe into the seed script. It enforces validation on every challenge: no duplicate flags, no missing fields, no flag format violations. Running it gives you instant feedback if any challenge is malformed.
generate_apk_challenges.py — specifically for generating the Android APK reverse-engineering challenges. It wraps real Android APK manipulation steps, embeds flags inside compiled bytecode or resource files, and packages the result. Reverse-engineering 5 different APKs at varying levels of obfuscation was one of the most technically demanding parts of the challenge set to build.
Both scripts were the answer to: how do you maintain 121 challenges without losing your mind? The answer is: you treat challenges as data, not as manual entries.
What the Teams Actually Found Hard
Post-event solve rate analysis across all 121 challenges:
| Category | Solve Rate | Notes |
|---|---|---|
| Easy (warmup 1–3) | 95%+ | Designed accessible — most teams cleared these first |
| Medium Forensics/Stego | 40–60% | Required tools (Autopsy, Steghide, ExifTool) |
| Hard Reversing | 15–25% | Ghidra/Binary Ninja players progressed; others skipped |
| Insane Crypto | <10% | Two teams solved all. Everyone else didn’t touch them |
| APK Reversing | 10–20% | More teams had Android skills than I expected |
| AI / Prompt Injection | 25–35% | Novel category — teams genuinely enjoyed it |
| Web Exploitation | 20–35% | Classic SQLi/XSS/IDOR — solid mid-tier solves |
| Master Web (MW1–MW11) | <5% | One team solved MW1. Nobody got past MW4 |
| OSINT | 30–40% | Teams without a dedicated OSINT player skipped it entirely |
The Master Web challenges were the talk of the event. Flags were hidden inside a live deployed React site — inside service worker pre-cache configs, invisible CSS-styled console output, JSON-LD schema blocks, and WebAssembly block comments. Nobody expected flags to live there.
Running a national-level cybersecurity conclave as VP of DDC — coordinating workshops during the day, then switching into platform builder and live monitor mode for a 36-hour CTF — was the most operationally intense thing I’ve done as a student.
Building the platform solo, watching 100+ teams hammer it live, and seeing the scoreboard flip in real time across 121 challenges — no downtime, zero flag leaks, zero auth bypasses — that’s the kind of stress-test that makes you quietly proud.
CyberFest 2026 was a statement: CBIT students can run national-level security events end-to-end. The Digital Defence Club will be back.
— Vasanth