Architecture overview
A high-level tour of Invictus Academy: a cloud-native cyber range built as a modular Go monolith with isolated run-modes, an honest-asymmetric trust model, and EU-sovereign forensics.
What the platform is
Invictus Academy is a cloud-native cyber range for incident response and forensics. SOC and IR analysts practise detecting and investigating cloud-compromise incidents on the platforms where those attacks actually happen: Azure, Entra ID and Microsoft 365, with AWS included as a second-provider proof. Every lab is derived from a real incident, anonymised, and made replayable.
The product is three integrated pieces working as one. A native web application owns learning, scoring and the CTF experience over the Go backend. A cloud-native range backend provisions and isolates a shared range. An integrated forensic toolkit handles evidence acquisition, timeline reconstruction and chain of custody.
Capturing live cloud telemetry from a running range is the piece that is not finished. The collector, its normalisation and its storage are built and carry the captured datasets the frozen labs replay; no cloud adapter pulls from a live environment yet, and one that is asked says so rather than reporting a quiet hour.
One point worth setting straight early: interactive live labs are graded on the end state you leave behind, not on your keystrokes. The platform reads the resulting cloud state and evidence you produce, it does not score the path you took to get there.
The modular monolith with run-modes
The backend, academy-api, is a single Go codebase built and shipped as one image (see ADR-001). It is not a fleet of microservices. Domains live as bounded contexts inside one binary and talk to each other only through in-process interfaces, with no cross-module database access.
That same image deploys as five separate Kubernetes workloads, each selected by a run-mode flag and each given its own workload identity, node placement and least-privilege scope. This is how the design reconciles two pressures at once: a small team cannot afford the operational tax of many codebases, but the security model demands that high-privilege provisioning code cannot be reached by compromising the internet-facing surface.
So privilege separation comes from identity and deployment topology rather than from splitting the code. The internet-facing pod holds no cloud-provisioning credentials, while the component that writes into the range runs isolated and acts only on signed, authorised work. The MVP deliberately ships no gRPC, Kafka, NATS or service mesh.
- api: internet-facing HTTP and streaming endpoints; enqueues signed intents but never writes to the range.
- provisioner: consumes authorised intents, runs Terraform apply/reset/detonate; the sole writer into the range.
- collector: pulls provider telemetry, normalises it to OCSF, preserves the raw copy and fans out. Built, and pulling from a live cloud range is not: an adapter with no collector reports that rather than an empty window.
- acquisition: assembles the evidence a run already captured, hashes it, and appends to the hash-chained custody ledger, signing the chain head when a signing key is configured. It holds no range credential at all, so it reads nothing out of the range itself.
- gateway: the interactive-shell broker for live labs (ADR-031); a non-internet workload that dials a pseudo-terminal into only the ticketed container. Reached by nobody unless the api's `LAB_SHELL` is on: the api mints no ticket while it is off (ADR-222).
Containers and how they relate
Everything deploys to Azure Container Apps in an EU region, in the one subscription the consortium has, beside the lab estates (ADR-188). The Azure PaaS pieces sit behind interfaces, so portability is bounded by abstraction rather than shipped as a second deployment (see ADR-004).
The system splits into three planes. The control plane is trusted infrastructure and holds the front end, Keycloak, the four academy-api workloads and their data stores. The range plane is a pool of standing Azure, Entra and M365 environments (plus partial AWS) that are reset between runs and treated as hostile. The evidence plane holds the evidence store and the hash-chained custody ledger.
The flow between control and range is deliberately narrow. The provisioner is the only writer into the range. The range emits telemetry outbound only, with no inbound path to the control-plane data. There is no network peering between the two planes.
Short-lived, read-only, per-run tokens for the readers are the design (ADR-012) and are not built. The collector federates as the deployment rather than per run, and acquisition holds no range credential at all: it assembles evidence a run already captured. So the inbound crossing is bounded today by which run-mode carries a range credential, not by the scope of a token.
- Front end: Next.js App Router (React and TypeScript) run as a Node service with a backend-for-frontend OIDC posture (ADR-003).
- Learning and scoring: native to the platform, delivered by the front end over the Go backend rather than a separate LMS container. ADR-053 supersedes the earlier CTFd design, so there is no CTFd container, plugin or second datastore.
- Keycloak: the OIDC identity provider for both surfaces (ADR-005). One realm ships, `tenants`. The separate staff realm that decision describes is not built, and no organisation's own IdP is federated today.
- Data stores: PostgreSQL as the system of record, Valkey for cache and queueing, and two separate Blob accounts that keep evidence apart from operational artefacts.
Core technology choices
The stack is Go, PostgreSQL, Valkey and Terraform. Python is reserved as an escape hatch for the occasional acquisition adapter where a Go SDK falls short (ADR-027); the CTFd plugin that decision also cited no longer applies, since learning and scoring are native (ADR-053). This keeps the dependency surface small enough for a two-to-four engineer team to carry.
Each data store has exactly one owner and no container reaches across into another's database. PostgreSQL uses a schema-per-module layout with Row-Level Security keyed on tenant, applied as defence-in-depth behind the primary application-layer authorisation (ADR-006). Keycloak keeps its own separate PostgreSQL backend. There is no second learning-platform datastore: learning, scoring and the CTF all live in the platform's own PostgreSQL (ADR-053).
Internal messaging is intentionally simple: a single Valkey-backed durable job queue plus a transactional outbox drain (ADR-007). Azure Event Hubs is used only as inbound transport for provider-native cloud logs, not as an internal bus. Lab content ships as declarative, checksum-verified bundles; publishers sign them with ed25519 that is verified against a public-key trust store, so provenance travels with the content itself rather than a registry (ADR-046, ADR-057).
The trust model and what we do not claim
The range is hostile by assumption and everything it emits is untrusted input: schema-validated, size- and rate-capped, sanitised on ingest, output-encoded in the front end and sanitised again before any egress to a customer SIEM (ADR-014).
The trust model is honestly asymmetric, not zero trust into the range (ADR-013). Forensic acquisition and live scoring genuinely read into the range, so a one-way barrier would be a false claim. Instead the two crossings are named and bounded: telemetry outbound only in one direction, and the isolated sole-writer provisioner in the other. The narrow per-run token broker ADR-012 describes for the inbound crossing is not built; the provisioner federates as the deployment, not per run, so today that crossing is bounded by which run-mode holds a range credential at all rather than by a token minted for one run.
Forensic integrity is treated as a product feature, built from hash-on-capture and a signed, hash-chained custody ledger. Write-once (WORM) evidence retention and external time-anchoring are further controls on the production deployment path, not present-tense guarantees. It is tamper-evident documentation for training and investigation, not court-admissible evidence, and trainee PII is kept out of the evidence store (ADR-022, ADR-023, ADR-060).
The headline effectiveness figure, a 25 percent or better improvement in mean time to detect, is reported as a measured programme KPI with a stated pre and post methodology across a pilot cohort of roughly 40 trainees. It is an operational measurement, not a statistically powered or proven result (ADR-026). All processing of trainee PII and incident-derived content stays in EU regions under the EU Data Boundary.