New internal component (platform/gateway/) — the only student-facing path to
disposable Lab environments. Browsers never reach a Lab container, the
Provisioner, or Postgres directly.
Routes:
- /api/environment/{status,launch,reset} — student-facing glue (X-Student-Id),
calls the Provisioner internally (shared secret never reaches the browser).
Reuses an existing non-terminal environment on launch rather than always
provisioning a new one; reset destroys+recreates the same environment row.
- /lab/<token>/* — HTTP+WS proxy into that one student's Lab container,
resolved by opaque routing token via a new Provisioner endpoint
(GET /environments/by-token/:token). No raw container IP/port ever reaches
the browser. Rejects invalid tokens (404), unready environments (202/503),
and cross-student access via an optional ?sid= ownership check (403) — see
docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md "Security" for why token-alone,
student-id-alone, and env-id-alone are each individually insufficient and
this is explicitly pilot-grade, not production auth.
- everything else — proxied to the persistent identity/curriculum backend
(roster, login, curriculum, progress, platform-shell frontend), or to the
student's own Lab if a valid lab-session cookie is present.
Routing design note: the existing Lab UI makes root-relative calls
(fetch('/api/scenarios'), the /shell-ws WebSocket) with no idea it might be
served under a /lab/<token>/ prefix — preserving that UI unmodified was a
hard requirement. Solved with an HttpOnly, SameSite=Lax cookie set on the
first /lab/<token>/ response: every later request from that browser carries
it, so the unmodified Lab UI's own unprefixed calls resolve correctly once a
student has navigated into a lab session, with zero changes to its code.
/api/environment/* is explicitly exempted from cookie-based routing so it
never gets hijacked by an active lab session.
Also added platform/docker-compose.dev.yml (the full local stack: Postgres +
egress-proxy + Provisioner + a persistent "identity" instance + Gateway) and
platform/README.md tying the components together. The "identity" instance
required an explicit `network_mode: bridge` — under Compose's own
auto-created bridge network, k3s failed at startup with "no default routes
found in /proc/net/route"; documented as a known limitation, not fully
root-caused.
Found and fixed during testing: express.json() applied globally broke
http-proxy's re-streaming of proxied POST bodies (e.g. POST /api/roster hung
forever, since the body had already been consumed by the parser before
proxy.web() tried to forward it). Removed — none of the Gateway's own routes
actually need a parsed body.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
KubeKosh — RootNode Academy Edition
A browser-based Kubernetes lab for hands-on learners.
Real K3s. Real kubectl. Instant validation. No cloud account, no local cluster.
Attribution. This is a branded, modified fork of the excellent
zeborg/kubekosh, licensed under Apache 2.0. RootNode Academy adds custom scenarios, branding, and multi-student deployment tooling on top of the upstream project. Original copyright and license notices are preserved inLICENSE. Modified files are marked per Apache 2.0 §4. Full credit to the original author for the core platform.
What It Is
KubeKosh runs a real single-node K3s cluster inside one Docker
container and pairs it with a browser terminal and automated scenario validation.
A learner opens a tab, gets a live cluster, runs real kubectl, and clicks Validate
to check their work against actual cluster state.
In the RootNode Academy curriculum this is the capstone rung — the Kubernetes & DevOps track that sits on top of the Linux / Git / Docker foundations. It is aimed at the advanced learner (roughly 14+), not the absolute beginner.
Quick Start
Prerequisite: Docker
docker run -itd --name kubekosh --privileged -p 7554:80 ced4568/kubekosh:latest
Open http://localhost:7554 and wait ~30s for the Cluster Ready indicator to turn green.
--privilegedis required — K3s needs kernel namespaces and cgroups. Do not expose this container directly to the public internet. It is a teaching sandbox. Multi-student exposure is handled by the deployment layer below, not by publishing the raw container.
Persist Progress
docker run -itd --name kubekosh --privileged -p 7554:80 \
-v <host_dir>:/data ced4568/kubekosh:latest
Progress is stored in SQLite at /data/progress.db. Mount a host directory to /data
to keep progress across restarts — one volume per student.
Build From Source
docker build -t kubekosh .
# multi-platform
docker buildx build --platform linux/amd64,linux/arm64 -t kubekosh .
What's Inside
| Bundle | Focus | Exam Mode |
|---|---|---|
| 🌱 Kubernetes Basics | Core concepts | 60 min |
| 🧑✈️ Kubernetes Administrator | CKA | 120 min |
| 🛠️ Kubernetes Developer | CKAD | 120 min |
| 🛡️ Kubernetes Security | CKS | 120 min |
Scenario types
- Task — hands-on challenge in the live terminal; Validate auto-checks cluster state.
- MCQ — multiple choice with a detailed explanation on submission.
Custom RootNode Academy scenarios live alongside upstream ones in scenarios/data/ and are
grouped into bundles in scenarios/bundles/. See scenarios/SCHEMA.md.
Architecture
flowchart TD
subgraph Container["Single Docker Image (--privileged)"]
NGINX["nginx :80 — reverse proxy"]
FE["Frontend<br/>React + Vite + xterm.js"]
BE["Backend<br/>Node / Express + node-pty (WebSocket PTY)"]
K3S["K3s<br/>single-node in-container cluster"]
DB[("SQLite<br/>/data/progress.db")]
NGINX --> FE
NGINX --> BE
BE -->|kubectl / PTY| K3S
BE --> DB
end
Student["Learner browser"] -->|:7554| NGINX
| Component | Technology |
|---|---|
| Frontend | React + Vite, xterm.js |
| Backend | Node.js / Express, node-pty WebSocket PTY |
| Cluster | K3s (single-node, in-container) |
| Proxy | nginx on port 80, mapped to host 7554 |
| Storage | SQLite (better-sqlite3) at /data/progress.db |
Everything runs inside one image managed by scripts/entrypoint.sh.
Multi-Student Deployment (RootNode Academy addition)
The upstream project assumes one learner, one container — this image is still exactly that,
unmodified. RootNode Academy multiplies it into a real platform via a separate provisioning
layer rather than changing the image itself: see docs/LAB_ARCHITECTURE.md for the
architecture and platform/provisioner/ for the Phase 1 implementation (Postgres-backed
environment registry, per-student Docker networking, firewall-enforced egress, idle reaper —
see platform/provisioner/README.md for how to run it).
Never publish the raw privileged container — front it with isolation. The one-container
docker run --privileged -p 7554:80 ... command above stays supported for local development
and scenario authoring; it is not how this gets deployed to real students.
Key properties of the Phase 1 platform (not this image):
- One container per student, its own Docker network, its own firewall chain — never a shared cluster between learners.
- Progress/identity live outside the disposable container (currently this image's own SQLite, moving to the Platform's PostgreSQL in a later phase) — destroying a student's environment never loses their academic record.
- CPU/memory/PID/storage limits are enforced per container, config-driven.
- Egress is restricted to an allowlisted proxy at the firewall layer, not left open.
- Idle/abandoned environments are automatically cleaned up.
Production topology and what's built vs. not yet: docs/LAB_ARCHITECTURE.md and
docs/PHASE1_PLATFORM_FOUNDATION.md.
Repository Layout
scenarios/
├── data/ # one JSON file per scenario -> <scenario-id>.json
├── bundles/ # one JSON file per bundle -> <bundle-id>.json
└── SCHEMA.md # full schema reference
backend/ server.js # Express API + WebSocket PTY
frontend/ src/ # React + Vite SPA
scripts/ entrypoint.sh # container startup (k3s -> API -> nginx)
nginx.conf # reverse-proxy config
Authoring Scenarios
Each scenario is one JSON file in scenarios/data/; each bundle is one JSON file in
scenarios/bundles/. See scenarios/SCHEMA.md for the full schema.
Task checklist
validation.commands— idempotentkubectlonlysetup_commands/teardown_commands—kubectlor native Ubuntu only
MCQ checklist
correct_optionmust match anoptions[].id- always include an
explanation
Hot-reload while authoring (no rebuild):
docker run --rm -itd --privileged -p 7554:80 --name kubekosh \
-v $PWD/scenarios:/app/scenarios ced4568/kubekosh:latest
curl -X POST http://localhost:7554/api/cache/reload
License
Apache 2.0 — see LICENSE. Original work © the zeborg/kubekosh authors.
Modifications © RootNode Academy / Chase Dumphord (ced4568).