Demo control page
A web page to drive the demo without a terminal: start k3s, deploy, check and remove a mode, see its links and accounts, run the chaos testing and read its verdicts.
demo/k3s/demo.sh ui # starts cfg-demo-ui, prints the URL with a fresh token
demo/k3s/demo.sh ui stop # stops it when no action runs; --force: anyway
ui prints http://127.0.0.1:32001/#token=... and opens nothing: paste
the URL into the browser. The token serves once; run again, ui draws a
new one for the running page (another tab, another browser).
What it shows
| Tab | What |
|---|---|
| Control | k3s’s state, cluster-up and cluster-down; per mode: deploy (preset, boxes s3, oidc, proxy, otel), check, remove; per deployed mode: links (console, explorer, Keycloak, RustFS console, Grafana), the sftp command to copy, credentials on a click |
| Chaos | each pod’s live state (ready, /readyz, peers, backends, detectors, bans), one card per experiment: hypothesis, mechanism and rules, injection, signal timeline, verdict and delays, a link to Grafana over the run’s time range with --with-otel |
The running action’s log streams live under the tabs. One action at a
time per mode; cluster-up and cluster-down exclude any other. Secrets
stay out of it: a deploy started from the page prints
(hidden: in the page) in their place, and the page masks any secret
value of an env.sh that shows up in a line. The “Show credentials”
button reads them from demo/k3s/.work/<mode>/env.sh, on demand.
The security model
The container cfg-demo-ui holds the host’s Docker socket: whoever drives
the page drives Docker, the equivalent of root on the machine. The page
is therefore never started on its own, and every request goes through these
guards:
| Guard | Effect |
|---|---|
| listening | CFG_DEMO_BIND when it is a loopback address (127.0.0.1 by default, 127.x, ::1), no other; port CFG_DEMO_UI_PORT (CFG_DEMO_PORT_BASE + 1, 32001) |
| token | 32 random bytes, drawn at each ui, in demo/k3s/.work/ui/token (mode 600); in the URL’s fragment (#token=), which the browser sends to no server; the page trades it once (POST /api/session) for a session secret, and takes it out of the address bar; the token is then spent |
| session | the secret, kept in the tab’s sessionStorage, sent as Authorization: Bearer with every API request; no cookie, so nothing the browser would attach on its own from another page or port; without it, 401 (the page’s files, holding no secret, are served without) |
Host header | 127.0.0.1, localhost, [::1] or the listening address, with the port: a DNS name rebound to 127.0.0.1 is refused (403) |
| origin | every action is a JSON POST (Content-Type: application/json, 4096 bytes at most) from the page itself: the page’s Origin (or Sec-Fetch-Site); otherwise 403 |
| actions | a fixed list of demo.sh subcommands; modes read from modes/, presets from presets/, options among the four --with-*, experiments from chaos.json; no free text, demo.sh run without a shell |
| container | the host’s user and group (plus the socket’s group), read-only file system, no capability; the kit mounted read-only at its own path, .work writable |
| headers | Content-Security-Policy with no outside source nor inline script, X-Frame-Options: DENY, Referrer-Policy: no-referrer, Cache-Control: no-store |
The image, cfg-demo-ui:<fingerprint>, is built at the first ui from
demo/k3s/ui/Dockerfile: the kit’s Python image (python:3.13.9-alpine3.22),
plus bash, the Docker CLI and coreutils (Alpine packages). The server
(demo/k3s/ui/server.py) uses Python’s standard library alone; the page
(index.html, app.js, style.css) loads nothing from elsewhere.
An action’s lock holds within the page: a demo.sh started in a terminal
at the same time is not held back. cluster-down leaves the page running,
and demo/k3s/.work/ui/ with it; ui stop removes them. ui stop refuses
while an action of the page runs; --force stops the page, which first
stops each action, an experiment’s restore included (300 s at most).
chaos undoes at its start what an interrupted experiment would have left.
The browser test
demo/k3s/demo.sh cluster-up
demo/k3s/ui/test/run.sh # starts the page if need be, draws a fresh token
run.sh runs demo/k3s/ui/test/ui.mjs in the Playwright image of the
console’s tests (mcr.microsoft.com/playwright:v1.63.0-noble), headless
Chromium, on the host’s network. It checks, in raw HTTP: 401 without a
session, with a forged one or with the startup token in its place, 401
for a wrong token, 403 under another Host or from another origin, the
token refused once traded, 415 for no JSON, 400 for a body over 4096
bytes, an option or an action out of the list, no cookie at all. Then, in
the page: the token out of the address,
deploy single started and its log followed to the end, the passwords
neither in the log nor in the page, the credentials on a click, the tabs
from the keyboard, the pod’s state, chaos single auth-burst and its card
(verdict, timeline, hypothesis, rules), remove single. A browser console
error fails it. The mode single must be free; log and screenshots:
demo/k3s/.work/ui-test/.