Nantes, France
Enzo Peigné — Portfolio · Ingénieur Cybersécurité —
Projet · Application web

PwnForge

Plateforme collaborative de gestion CTF et challenges d'entraînement — suivi des challenges, writeups partagés et statistiques d'équipe en temps réel.

Les équipes CTF manquent d’outillage adapté à la réalité d’une compétition. Pendant un CTF, les informations s’éparpillent : challenges suivis sur un Google Sheets partagé, notes personnelles dans des éditeurs locaux, writeups perdus dans des fils Discord. Quand une équipe de six personnes attaque simultanément une trentaine de challenges sur 48 heures, l’absence de plateforme centrale coûte du temps.

Les solutions existantes sont soit trop lourdes pour un usage ponctuel, soit trop légères pour une équipe structurée. PwnForge part de ce constat.

Contexte

La pratique régulière de CTF et de challenges d’entraînement (Hack The Box, TryHackMe, Root-Me) révèle un problème récurrent : la coordination d’équipe repose sur des outils génériques qui ne comprennent pas le contexte d’une compétition.

Un tableur ne sait pas qu’un challenge “en cours” depuis 6 heures sans mise à jour mérite peut-être un coup d’œil collectif. Un canal Discord ne permet pas de retrouver les notes du writeup rédigées il y a trois semaines. Un outil de prise de notes générique ne propose pas de filtrer les challenges par catégorie, par plateforme, ou par membre assigné.

PwnForge est une réponse directe à ces frictions, construite par quelqu’un qui les subit régulièrement.

Objectifs

Concevoir une plateforme web collaborative, disponible en SaaS ou auto-hébergée, open-source, pensée pour les équipes CTF qui veulent travailler sérieusement sans dépendre d’outils génériques.

Trois axes structurent le projet :

  • Centralisation : un espace de travail par compétition, accessible à toute l’équipe, où chaque challenge a son état, ses notes et son writeup associé.
  • Collaboration temps réel : les mises à jour sont propagées instantanément via WebSockets : quand un membre pose un flag, l’équipe le sait sans recharger la page.
  • Capitalisation : les writeups rédigés pendant la compétition restent consultables après, construisant une base de connaissances collective exploitable pour les éditions suivantes.
  • Archivage et statistiques : chaque workspace peut être archivé en lecture seule pour conserver une trace de la compétition, avec des statistiques détaillées par membre, par catégorie de challenge, et par plateforme. Cette archive peut être consultée à tout moment, même après la fin de la compétition afin d’être réutilisée pour les éditions suivantes.

Un objectif technique structurant accompagne ces axes : écrire le backend en Rust. Ce projet est aussi une opportunité d’apprendre le langage sérieusement, dans un contexte applicatif réel avec des contraintes de concurrence (WebSockets, accès concurrent à la base de données).

Architecture

Le projet est organisé en monorepo avec deux workspaces distincts : backend/ (Rust/Axum) et frontend/ (React/TypeScript/Vite). Docker Compose orchestre l’ensemble pour le développement et la production.

Backend — Rust/Axum

Rust a été choisi pour sa performance, sa sûreté mémoire et son écosystème de bibliothèques modernes. Il offre la possibilité d’écrire un backend web robuste et performant, tout en me permettant d’apprendre le langage de manière approfondie.

Axum a été retenu pour sa philosophie proche du framework web : routeur composable, extracteurs typés, intégration native avec le runtime tokio. La persistance est assurée par PostgreSQL, accédé via SeaORM (migrations, modèles typés et requêtes construites avec le query builder). Le backend expose une API RESTful pour les opérations CRUD sur les équipes, workspaces, challenges et writeups. Un système de rôles par équipe (owner, admin, membre) contrôle les permissions à chaque niveau.

Les WebSockets sont gérés par Axum en natif. Un système de canaux tokio::broadcast diffuse les événements (flag posé, challenge mis à jour, nouveau writeup) à tous les clients connectés à un workspace donné. Pour éviter d’exposer le token JWT dans les query strings WebSocket, l’authentification passe par un ticket à usage unique généré côté serveur et stocké brièvement dans Redis.

Redis assure également le cache des données fréquemment lues (workspaces, rôles) avec une stratégie d’invalidation par versioning de clé, et sert de backend à la rotation des refresh tokens.

L’authentification est basée sur des tokens JWT à courte durée de vie, avec refresh token persisté en base. Aucune dépendance à un service externe d’identité pour rester auto-hébergeable simplement.

Frontend — React/TypeScript

Interface React construite avec React Router v7 pour la navigation, TanStack Query pour la gestion du cache serveur, et un hook useWebSocket maison qui gère la connexion, la reconnexion automatique, et la distribution des événements aux composants abonnés.

Le design est fonctionnel et dense : l’interface d’un outil de travail, pas d’un produit grand public. Priorité à la lisibilité de l’information et à la densité utile : tableaux filtrables, vue kanban par statut de challenge, éditeur de writeup Markdown avec aperçu côte à côte (CodeMirror 6).

Schéma de données

Le modèle distingue deux entités structurantes :

  • Team : groupe permanent de personnes avec des rôles (owner, admin, membre). Vit dans le temps.
  • Workspace : espace de travail pour une compétition ou une plateforme d’entraînement, en mode solo ou équipe. Peut être archivé en lecture seule après une compétition.

Les challenges (catégorie, points, statut, assignés, flag) et les writeups (Markdown, auteur, tags, visibilité, commentaires) sont rattachés à un workspace. Les identifiants utilisent UUID v7 pour la localité B-tree en base. Le statut de résolution d’un challenge est calculé via une VIEW PostgreSQL plutôt que stocké en colonne redondante.

Réalisations

Projet en phase de conception. Les réalisations seront documentées au fil du développement.

La spécification fonctionnelle est finalisée. Le schéma de base de données est défini.

Le démarrage du développement est prévu pour le second semestre 2026, avec une première version fonctionnelle ciblée pour fin 2026 - début 2027, en fonction de l’avancement et des imprévus.

Stack technique détaillée

Rust / Axum — backend. Framework web asynchrone, routeur composable, WebSockets natifs. Cible : performances, sûreté mémoire, et apprentissage sérieux du langage.

PostgreSQL 16 — persistance. Base relationnelle pour les équipes, workspaces, challenges et writeups. Accès via SeaORM : migrations versionnées, modèles typés, query builder.

Redis — cache, sessions et tickets WebSocket. Invalidation par versioning de clé, rotation des refresh tokens, tickets d’authentification à usage unique pour les connexions WebSocket.

React / TypeScript — frontend. Interface dense et fonctionnelle, optimisée pour l’usage en compétition. TanStack Query pour le cache serveur. CodeMirror 6 pour l’éditeur Markdown split-pane.

WebSockets — collaboration temps réel. Diffusion des événements via tokio::broadcast, abonnement par workspace. Authentification via ticket Redis à usage unique. Reconnexion automatique côté client.

Docker Compose — déploiement. Un compose.yml à la racine du monorepo : backend, frontend, PostgreSQL, Redis, et un reverse proxy Caddy.

JWT — authentification. Tokens à courte durée de vie, refresh token persisté, sans dépendance à un service d’identité externe.

Ce que j’en retire

Section à compléter en cours de développement.

PwnForge est aussi un exercice d’architecture : définir des frontières claires entre les modules, choisir des abstractions qui facilitent l’évolution plutôt que la compliquer, documenter les décisions techniques au fur et à mesure plutôt qu’après coup.

Le choix de Rust pour le backend est délibérément inconfortable. Un projet annexe, aux contraintes réelles mais sans pression de livraison externe, est le contexte idéal pour apprendre un nouveau langage sérieusement.