Krill needed to run as a public, internet-facing demo (live.krillswarm.com on
EC2, port 8442 only). A security audit found the server was not safe to
expose, independent of any “demo” feature:
GET /krill-token handed the cluster’s master bearer token to any
unauthenticated caller (Routes.kt), nullifying all auth on a public host./nodes, /node/{id}, /sse) serialized full node meta,
including SMTP token, webhook auth headers, and LLM host — despite the SDK’s
own KDoc claiming those are “never sent to read clients.” Nothing enforced it.anyHost() + allowCredentials=true, so any website could script a
token-holding box.GET /incoming/** fires node
invocations and GET /shutdown kills the process.The auth model is a single cluster-wide PIN-derived bearer token shared by
clients and peers, handed out freely by /krill-token on the assumption the
WASM app is same-origin — false on the public internet. There was no notion of
“read-only,” no server-side write chokepoint, and no read-time redaction.
Added a server-level demoMode flag (ServerConfig.demoMode, default false,
backward-compatible via fastJson lenient parsing). When enabled:
DemoMode.kt::isBlockedInDemoMode, wired as an
ApplicationCallPipeline.Plugins interceptor in Plugins.kt) rejects every
mutation with 403 regardless of token. Policy is default-deny for writes
— any non-GET is blocked, plus the explicit side-effect/secret GETs
(/incoming, /shutdown, /krill-token) — so a future mutating route is
covered automatically.DemoMode.kt::redactSecrets) blanks credential-named
meta fields at any JSON depth, applied to /nodes, /node/{id}, and the
/sse serialize chokepoint.anyHost()/allowCredentials in demo mode (WASM UI is
same-origin).GET /demo advertises the flag so clients can skip FTUE
before the PIN gate (the client learns demo status pre-connect).Client + SDK (same feature, this PR + krill-oss#209):
ServerMetaData.demoMode was added to krill-sdk (0.0.60) and is populated by
ServerIdentity.serverMetaData(), so /health and /sse carry the flag.ClientNodeManager no-ops all six user mutators (submit/delete/execute/
edited/postSnapshot/updateMetaData) when the connected server is a demo,
escalating the flag whenever a demo Server node streams in. Inbound SSE
writers are untouched, so live data keeps flowing./health before the FTUE/PIN gate and skips onboarding
for a demo host (the “read health at launch, then skip” flow). A
LocalDemoMode CompositionLocal hides Save / Add-child / Delete affordances.The two policy functions are pure and unit-tested (DemoModeGuardTest) with no
filesystem/port/PIN dependency — the route handlers and plugin delegate to them,
per the repo’s “call the helper the route delegates to” test rule.
authenticate blocks or keying on
HTTP method (which misses state-changing GETs)./krill-token’s same-origin assumption is false on the public internet; the
audit is what surfaced it. When exposing anything publicly, enumerate every
route by (method, path, auth, side-effect) first.ClientNodeManager
no-op’ing every write (and the server 403). Gate the one place all user writes
funnel through, then hide affordances on top./health (open in demo mode) at launch, before the gate — not to let FTUE
render and dismiss it afterward (which flashes)./krill-token disclosure (#916), read
secret-leak (#917), lambda sandbox (#918), CORS (#919), /shutdown (#920),
unauth project reads (#921).