Egress enforcement is a firewall-layer control, not an application-layer convention (correction B). HTTP_PROXY/HTTPS_PROXY configure containerd for a clean pull path; they enforce nothing on their own. - src/egress.js — per-environment dedicated iptables chain (ROOTNODE-ENV-<short-id>), built with -A (append, order-preserving) rather than the -I (insert-at-head) pattern an earlier design draft used, which would have installed rules in reverse order and let the default-deny shadow the proxy/DNS accepts. Required infrastructure (egress proxy, DNS) is explicitly permitted first, then RFC1918 ranges are dropped unconditionally, then a final default-deny catches the public internet. One jump rule per environment hooks its subnet into its chain from DOCKER-USER (the chain Docker guarantees it won't silently rewrite). Destroy is deterministic: unhook, flush, delete — no comment-grep matching. The ordering fix is empirically verified, not just reasoned about: the egress proxy's own address is itself an RFC1918 address (it's Platform-side infrastructure), and testing confirms it's reachable while the broader RFC1918 range is still blocked — exactly the scenario the original ordering bug would have broken. - platform/egress-proxy/ — shared Squid instance (one per deployment, not per-student), destination-allowlisted (dstdomain ACL against a plain-text allowlist file), starting set covers docker.io/ghcr.io/gcr.io/quay.io/k8s.io. Proxy performs domain allowlisting; the firewall chain above is what makes it unbypassable, not the other way around. The egress proxy's address is resolved per-student-network (each network's Docker gateway IP, since the proxy runs with host networking and is present on every bridge gateway) rather than a fixed hostname — a static address wouldn't be reachable from more than one isolated student network at a time. Found while testing with two simultaneous environments. Verified: student-to-student isolation (A cannot reach B's internal IP), RFC1918 blocking (A cannot reach a simulated trusted-infra address), and the proxy allowlist itself (CONNECT tunnel to an allowlisted host succeeds, non-allowlisted gets a clean 403 from Squid). 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. RootNode Academy runs it across three delivery modes. Never publish the raw privileged container — front it with isolation.
| Mode | Provisioning | Isolation |
|---|---|---|
| In-person camp | Pre-spun container per seat, torn down after | Lab VLAN, no public ingress |
| Live cohort | Container per enrolled student for the session window | Per-student auth at the proxy |
| Self-paced | On-demand spin-up per session, idle-timeout teardown | Per-user volume + auth + resource limits |
Hardening checklist for shared infra:
- One container per student — never share a cluster between learners.
- Apply CPU/memory limits so one runaway pod can't starve a host.
- Put authentication in front (proxy-level), never expose
:7554to the internet. - Persist each student's
/datato a dedicated volume. - Idle-timeout + auto-teardown for self-paced sessions to control cost.
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).