ORDeC Hub
ORDeC Hub runs ORDeC for workshop participants: everyone gets their own ephemeral, isolated instance in the browser, with nothing to install.
The reason it exists is that ORDeC executes arbitrary code by design — an ORD
design is a program, and running it is the whole point. A single shared
server is therefore not an option: one participant could read another’s work,
or the host. Each participant needs a sandbox of their own, and something has
to hand those out, log people in, and clean up afterwards. That is what the hub
does. Only integrated mode is available behind it; local mode (-m) is
disabled, so no participant code touches a filesystem that outlives the
session.
How it works
The hub is JupyterHub, which despite the name has nothing to do with Jupyter:
it is a generic “authenticate a user, spawn a single-user web service, proxy to
it” platform. It was built for exactly this threat model — Jupyter, like ORDeC,
runs untrusted code as a feature — and brings a login page, a spawner, a
reverse proxy, an admin panel and an idle culler. Participants see it only at
login; afterwards they are redirected straight into the ORDeC UI at
/user/<name>/.
Each instance is a Docker container run under the Kata Containers runtime, which starts it inside a lightweight
KVM virtual machine with its own guest kernel. That is the point: plain
containers share the host kernel, so a kernel bug is a host compromise, whereas
Kata puts a hardware boundary in the way while still being an ordinary OCI
container to build and spawn. The host must therefore expose /dev/kvm,
which dedicated servers do and most budget cloud VMs do not.
The containers sit on an internal: true Docker network, so they have no
NAT, no DNS and no egress — the hub proxy reaching the ORDeC port is the only
path in or out. Per-user CPU and memory caps are enforced by the kernel, so a
runaway simulation burns only its owner’s allowance, and an out-of-memory kill
lands inside that user’s own VM. Nothing is mounted and nothing persists:
stopping an instance deletes it. The idle culler stops instances after 90
minutes by default, long enough to survive a lunch break.
Rough sizing: about 1.5–2 GB of RAM per participant, and CPU that is bursty enough to oversubscribe safely. RAM is the binding resource — 80 participants want something in the region of 256 GB and 32 cores, which is one rented dedicated machine rather than a cluster.
What ORDeC does differently behind the hub
ordec/hub.py holds the integration; it uses only the standard library, so
the user image does not depend on JupyterHub. It activates automatically when
the JUPYTERHUB_SERVICE_PREFIX and JUPYTERHUB_API_TOKEN environment
variables are present.
- Serving under a path prefix
JupyterHub’s proxy forwards
/user/<name>/...without stripping the prefix, so the server strips it before matching routes and answers 404 outside it. The frontend keeps every URL relative — websocket, fetches, links, and Vite’sbase: './'— so the built assets contain no absolute paths. This is also available standalone viaordec --base-url /pfx/.- Authenticating against the hub
A full OAuth flow: authorize redirect with a state cookie, code exchange, and a check that the user is the one this instance was spawned for. The resulting session cookie is scoped to the prefix. Everything is gated, including the websocket handshake; unauthenticated API calls get 401 while page navigations get the redirect. The frontend then fetches ORDeC’s own auth token from the cookie-gated
api/tokenendpoint instead of reading it from the URL fragment, so the per-session token auth stays intact underneath.- Reporting activity
Every websocket message updates a last-activity timestamp, which a reporter thread POSTs to the hub every five minutes. Without it the culler would see a long-lived idle websocket as activity and keep dead sessions alive; with it, an open-but-forgotten tab is culled while real use never is.
- Surviving a cull
When the websocket cannot connect and ORDeC knows it is hub-hosted, the frontend offers to restart the session: it stashes the editor source in
sessionStorage, reloads through the hub — which respawns the instance — and restores the source.
Deployment
Needs a KVM-capable Linux host (check with ls /dev/kvm) and a DNS record
for the workshop hostname; Caddy fetches Let’s Encrypt certificates by itself.
# 1. Host: Docker + Kata Containers + smoke test (review this first!)
sudo hub/deploy/host-setup.sh
# 2. Images (from the repository root)
docker build -t ordec .
docker build -t ordec-hub-user -f hub/Dockerfile hub/
# 3. Configuration
cp hub/example.env hub/.env
$EDITOR hub/.env # domain, workshop key, admin key, limits
# 4. Start hub + TLS proxy
cd hub/
docker compose up -d --build
Participants then browse to https://<domain>/, enter the workshop key (no
username), and land in ORDeC.
The pieces live in hub/: jupyterhub_config.py (authenticator, spawner,
limits, culler — all tunable through ORDEC_HUB_* variables),
templates/login.html (the key-only login form), Dockerfile (the user
image, the regular ordec image with a hub-suitable start command),
hub.Dockerfile (the hub itself), docker-compose.yml (hub plus Caddy, and
the internal-only user network) and deploy/ (Caddyfile,
host-setup.sh).
Login and sessions
There are two ways in, both from the same login page (a small custom
Authenticator in jupyterhub_config.py):
- Participants
Enter only the shared workshop key — no username. Each login mints a fresh
guest-<random>identity, and JupyterHub’s own session cookie then binds that browser to its guest container until logout or culling. So every browser gets its own distinct, ephemeral session; a second browser with the same key gets a separate container.- Admins
Follow the “Admin login” link (
/hub/login?admin) to a separate page and enter an allowlisted username (ORDEC_HUB_ADMINS) plus the separate admin key (ORDEC_HUB_ADMIN_KEY). Admins land directly on the JupyterHub admin panel at/hub/admin(no ORDeC container is spawned for them), which lists every session, shows activity, and can stop, access or delete servers. For live CPU/RAM usedocker statson the host. LeavingORDEC_HUB_ADMIN_KEYempty disables admin login.The username is not a credential.
authenticatenever lets a caller pick an existing guest identity — an empty username always mints a fresh random one, and a submitted username only reaches the admin path (which requires the admin key) — so knowing a participant’s URL-visibleguest-<random>name does not grant access to their session. Access is gated by the signed hub session cookie and per-server OAuth, not the username.- Ending a session
The ORDeC toolbar shows an End session button in hub mode. It navigates to
/hub/logout; the hub runs withshutdown_on_logoutso logging out also stops the container (which, with the spawner’sremove=True, deletes it). The backend hands the hub logout URL to the frontend via theapi/tokenresponse, so it is correct under any hub base URL.Guest accounts are genuinely ephemeral. A custom logout handler deletes the
guest-<random>account on logout, once its server has stopped, so an active “End session” leaves nothing behind. Sessions that are merely abandoned (tab closed, no logout) are cleaned up by the idle-culler: afterORDEC_HUB_IDLE_TIMEOUTit stops the idle server and--cull-usersdeletes the now-inactive account. Admin accounts are never auto-deleted.
Moving to institutional or OAuth login is a config change of
c.JupyterHub.authenticator_class — nothing in ORDeC or the spawner setup may
assume the shared-key model.
Security checklist
Worth verifying on the deployed host, since most of this cannot be tested anywhere else:
Kata is really active:
docker run --rm --runtime io.containerd.kata.v2 alpine uname -rmust report a different kernel than the host.host-setup.shchecks this.No egress from user containers: from inside a spawned container, external connections and DNS lookups must fail.
Cross-user access is denied: another user’s
/user/<name>/api/versionmust answer 401 without their session cookie.tests/test_hub.pycovers this, but re-check it deployed.Idle culling works: leave an instance idle past
ORDEC_HUB_IDLE_TIMEOUTand watch the container disappear.
Local testing without KVM
Setting ORDEC_HUB_RUNTIME=runc in .env runs the whole flow with plain
containers, which is useful on a laptop and must never be used for a real
workshop: it is the shared-kernel isolation that Kata exists to avoid. For
testing without TLS, publish port 8000 of the jupyterhub service and use
http://localhost:8000.
To exercise the full path through Caddy on a development host that has no
domain and no certificate, point ORDEC_HUB_DOMAIN at an http:// address
instead: Caddy only fetches a certificate when the site address is a bare
hostname, and serves plain HTTP when the scheme is explicit.
ORDEC_HUB_DOMAIN=http://devhost.local # or ':80' to match any host / a bare IP
Participants — and the OAuth redirects — then use http://<host>/. ORDeC’s
session cookies drop their Secure flag automatically in this case, since
they follow the X-Forwarded-Proto that Caddy sends. This is for development
only: without TLS the workshop key and every session cookie cross the network
in the clear.