A local-first platform for building, running and grading containerised security labs — on your own machine.
CyberForge is the tooling layer you run yourself to define, provision, isolate, snapshot, monitor and tear down container-based lab environments — plus everything needed to turn a lab into an exercise: a written brief, a self-check that grades it, a catalog of well-known vulnerable targets, capture-the-flag mechanics, an in-browser attacker terminal, and a request workbench for poking at a target.
It is not a hosted training site and not a clone of one. Everything runs locally. Nothing is sent anywhere — no telemetry, no remote update check, not even to validate a form field.
git clone <your-repo> cyberforge && cd cyberforge
cp .env.example .env
docker compose up -dOpen http://localhost:3000 and sign in with the bootstrap administrator from
your .env (admin / ChangeMe!2024 by default).
Change that password immediately. The default is published in
.env.example, so it is public knowledge.
Add the observability stack when you want it:
docker compose --profile monitoring up -d # Prometheus, Grafana, Loki, Promtail| Service | URL | Notes |
|---|---|---|
| Web UI | http://localhost:3000 | The application |
| API docs | http://localhost:8000/docs | Swagger UI (hidden in production) |
| MinIO console | http://localhost:9001 | Object-storage browser |
| Grafana | http://localhost:3001 | monitoring profile only |
| Prometheus | http://localhost:9090 | monitoring profile only |
Prefer a terminal? The bundled CLI is standard-library-only Python:
pip install ./sdk/python
cyberforge login --username admin
cyberforge template ls --tag dvwa
cyberforge lab new web/dvwa --name "My DVWA" --start
cyberforge lab verify "My DVWA"A lab is a named, versioned set of containers on one or more isolated
networks, with a resource budget and a lifecycle. Create one from scratch, from a
built-in template, from the vulnerable-target catalog, by cloning, or by
importing a portable .cyberforge.json export from another machine.
Lifecycle verbs are enforced by a server-side state machine, and the API tells the client which ones are currently legal so the UI never offers an action that would fail:
| Action | What it does | What it frees |
|---|---|---|
| Start | Provisions networks and containers, starts services | — |
| Sleep | Pauses containers | CPU. Memory and in-container state kept |
| Resume | Unpauses containers | — |
| Stop | Stops and removes containers | Memory. Volumes are kept |
| Archive | Releases all runtime resources, makes lab read-only | Everything |
That sleep/stop distinction is the point of the platform: sleeping a lab costs nothing in CPU and gives it back instantly, exactly as you left it.
- Vulnerable-target catalog — one-click DVWA, Juice Shop, WebGoat, bWAPP and
Metasploitable 2, plus a one-click Kali attacker box, all under stable slugs
that external content can link. Every target ships on an
internalnetwork with no published port; exposing one is a decision you make, never one the catalog makes for you. - Guided labs — a lab can carry a Markdown brief and a declarative
success check (
http,tcp,flag, orcontainer).POST /labs/{id}/verifyruns the check from inside the lab's own network, so an isolated target is still gradeable, and returns a three-way verdict: passed, failed, or unverifiable — because "the check is malformed" is not the same as "you got it wrong". - ATT&CK coverage — templates are tagged with MITRE ATT&CK technique ids (never names — names drift between releases, ids do not), with a coverage endpoint that reports how many templates carry no mapping, so the number is honest rather than flattering.
- Challenges, flags & scoreboard — a separate
Challengeentity (a flag never lives on a Lab or Template, where it would leak), per-player flag instances injected as files, constant-time comparison, a two-layer submission rate limit, and a scoreboard whose freeze is a read-time filter and whose dynamic scores are computed, never stored. - Payload workbench — craft an HTTP request against a lab service, fire it from inside the lab network, see the response, tweak and replay, and save named variants beside a seeded, editable payload library (SQLi, XSS, command injection, traversal, SSTI).
- Image build — write a Dockerfile in the browser and build an image, with a
byte-offset streaming log. Off by default and gated three ways, because a
RUNexecutes as root on the daemon host — the most privileged thing the platform can be asked to do. - Interactive terminal — a real pty into a lab container (xterm.js), with optional session recording and replay (output only by default; input recording is opt-in with a warning, since it captures typed passwords). Authorised by a single-use, short-lived, HTTP-minted ticket.
- Exposure — publish a single service on a routed network for a time-boxed window, audited and reaped automatically. The backend itself never joins a lab network.
- Live updates — the UI stays current over Server-Sent Events that carry invalidation hints, not data: the client is told what changed and refetches through the already-authorised REST endpoint, so nothing can leak over the stream that the endpoint would not have returned anyway.
- Networking — virtual networks with real isolation.
internalnetworks have no route to the host or the internet (the default). Subnet overlap is rejected. Firewall rules are stored declaratively and labelled honestly as either enforced or advisory. A topology view shows what is attached to what. - Containers — a read-through view of the Docker engine annotated with which lab owns what, plus logs, stats and lifecycle control.
- Snapshots — capture a lab's definition and restore it later, with an automatic safety snapshot taken first. Volume contents are captured only when an operator has enabled it and the individual snapshot asks for it — and the snapshot's manifest always states exactly what it captured.
- Storage — MinIO-backed object store with a filesystem fallback, indexed in PostgreSQL so listing and quota accounting never require a bucket scan.
- Reports — Markdown, HTML and JSON rendered from one document model so they
can never disagree; HTML output is self-contained and opens offline, and is
linkable at a stable
.../report.htmlURL. - Plugins — a real SDK. Plugins contribute API routes, dashboard widgets, frontend pages, themes and lab templates.
- Enterprise — MFA, SSO, teams, per-field quotas, custom roles, webhooks, a PostgreSQL-backed job queue, image scanning and engagement tracking.
- Monitoring — Prometheus metrics, a provisioned Grafana dashboard, and container logs shipped to Loki.
- Audit — every security-relevant action recorded in a hash-chained, append-only trail.
These are the constraints the codebase is built around, not aspirations.
Runs on one machine, offline. No telemetry, no remote update check, no
external call to validate a form field. GET /system/updates reports
check_performed: false and says so.
Low idle footprint. The backend idles at roughly 120 MB RSS. There is no background poller: resource samples are computed on demand and memoised for a few seconds, so an idle server does no work at all. Historical recording is opt-in.
Only PostgreSQL is required. Redis falls back to an in-process cache, MinIO
falls back to filesystem storage, and Docker being unavailable is a normal state —
reads return empty collections and writes return 503 with an actionable
message. The platform boots and the UI renders honestly in all of those cases.
No AI, deliberately. CyberForge ships zero AI functionality: no LLM
client, no embeddings, no vector store, no RAG, no agents, no speech or vision.
What exists is the seam — Protocols in app/services/ai/interfaces.py and a
capability-discovery endpoint that reports enabled: false. Adding a provider
later means writing one class; no existing call site changes.
Honest about limits. Where something is scaffolded rather than finished, the
API and the UI say so. A snapshot's manifest records exactly which of the
definition, the volume contents, and the container filesystem it captured. A
verification whose check is malformed returns unverifiable, never a false
failure. Disabling a plugin returns restart_required: true, because mounted
routes genuinely cannot be unmounted at runtime.
The dangerous operations are gated and audited. Container exec, image build, and volume capture each default to off, require an explicit permission, and write an audit record. The hardened Docker-socket-proxy profile denies image build unless the same flag is set, so the two layers cannot silently disagree.
Browser ──► nginx ──► FastAPI ──► PostgreSQL (required)
├────► Redis (optional, falls back to memory)
├────► MinIO (optional, falls back to filesystem)
└────► Docker engine (optional, degrades gracefully)
The backend is layered strictly inward — api → services → repositories → models
— with adapters (Docker, storage, cache) behind Protocols so every external
dependency has a working fallback. Nothing above app/services/docker/ imports
docker; the engine, the null adapter and the Protocol change together.
apps/backend/app/
core/ config, security, RBAC, errors, logging, metrics, middleware
db/ engine, base, migrations, seed data, the vulnerable-target catalog
models/ SQLAlchemy 2.0 models
schemas/ Pydantic request/response contracts
repositories/ all SQL lives here
services/ business logic, lifecycle orchestration, the Docker adapter
api/v1/ thin HTTP endpoints
plugins/ plugin loader
Every non-obvious decision is written down: see the 64 architecture decision records in docs/DECISIONS.md. Full detail in docs/ARCHITECTURE.md.
# Backend
python -m venv .venv && .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e "apps/backend[dev]" -e sdk/python
docker compose up -d postgres redis minio
cd apps/backend && alembic upgrade head
uvicorn app.main:app --reload
# Frontend
npm install
npm run dev # http://localhost:5173# Tests
cd apps/backend && pytest # 874 tests
npm run test --workspace @cyberforge/frontend # 151 testsThe backend suite runs entirely on in-memory SQLite with fake Docker and storage
adapters, so it needs no running services and works offline. The gates are
ruff, ruff format, mypy --strict, pytest, tsc, eslint, and vitest.
More in docs/DEVELOPMENT.md.
| Document | Contents |
|---|---|
| ARCHITECTURE.md | Layering, request lifecycle, adapters, degradation |
| DATABASE.md | Schema, relationships, indexes, migrations |
| API.md | Auth flow, error contract, endpoint reference |
| ENTERPRISE.md | MFA, SSO, teams, quotas, roles, webhooks, jobs, scanning, engagements |
| DEVELOPMENT.md | Local setup, workflows, debugging |
| DEPLOYMENT.md | Compose, Kubernetes/Helm, air-gapped, backup, upgrades |
| PLUGINS.md | Writing a plugin, the SDK, the manifest |
| SECURITY.md | Threat model, controls, and what is not protected |
| TESTING.md | Strategy, fixtures, running the suites |
| STRUCTURE.md | Every directory, explained |
| CODING_STANDARDS.md | Conventions and their rationale |
| CONTRIBUTING.md | How to work on this |
| DECISIONS.md | 64 architecture decision records |
| ROADMAP.md | What is next, and what is deliberately not |
CyberForge manages Docker. Read docs/SECURITY.md before exposing it to anything but localhost. Three things matter most:
- The Docker socket is mounted into the backend. That is equivalent to root
on the host. Anyone who can reach the API with sufficient permissions can
influence the Docker daemon. A socket-proxy
hardenedprofile narrows, but does not eliminate, that surface. - Image build runs as root on the daemon host. It is off by default, restricted to instructors and above, and audited — but when enabled it is the single most powerful capability the platform offers.
- Plugins are trusted code. An enabled plugin runs in the backend process with its full privileges. There is no sandbox, and the documentation does not claim one.
All three are inherent to what the platform does. They are stated plainly rather than buried.
CyberForge is free software, licensed under the GNU General Public License
v3.0 (GPL-3.0-only). See LICENSE for the full text.
You may use, study, share and modify it under those terms; if you distribute a modified version, it must be released under the same licence. Every source file carries an SPDX header to make the licence unambiguous per file.
Copyright (C) 2026 Nithees Narendra S.