DreamLake

Living dev note. Iterate freely.

Running list of security tightening we've consciously deferred, with the reasoning. Each item is a "good enough today, here's the upgrade path".

Open items

Daemon → controlplane exec-result auth (per-exec token)

Today: POST /v1/daemon/exec/:exec_id/result is un-namespaced and authorised implicitly by the unguessable exec_id (ULID). The /hello response session_token isn't validated on the callback.

Risk: anyone who guesses or intercepts an exec_id can submit a forged result for that exec. Mitigated by: (a) ULIDs aren't guessable; (b) the exec row is read-once at completion; (c) all CLI ↔ CP traffic is TLS.

Upgrade: at exec creation time, mint a short-lived per-exec MAC secret bound to the exec row. Server returns it in the POST /v1/namespaces/.../exec response (encrypted to the requesting daemon via the daemon's session pubkey, or — simpler — passed verbatim to the daemon as part of the exec command body via the existing poll-response path). Daemon HMACs the result body and sends it as a header on the callback; server validates before persisting. Adds ~one ed25519 / HMAC verify per result POST.

Session token persistence

Today: /hello mints a session_token (bare ULID) and returns it. The daemon round-trips it on subsequent polls but the server does not persist or validate it — the token is effectively cosmetic.

Risk: any caller can claim to be any daemon on the namespaced endpoints (worker setup, mount mounts) if they know a worker_id. The auth-hook exemption for the legacy exec-result URL widens this.

Upgrade: store sha256(session_token + per-request salt) in the Token table (same shape the CLI uses), bind to the worker row's namespaceId + workerId. /v1/daemon/{poll,ack,event} start validating it. Aligns daemon auth with CLI auth — one model.

LAKESHORE_ADMIN_TOKEN as a long-lived bearer

Today: LAKESHORE_ADMIN_TOKEN is a long-lived secret that mints namespace tokens on lakeshore auth login. Same secret on Heroku config, every developer machine, and any CI.

Risk: rotation is awkward; leak surface is large.

Upgrade: short-lived signed-JWT model for admin operations, with per-developer issuer keys. Out of scope for the inner-loop work.

Open mode (local dev)

Today: When LAKESHORE_ADMIN_TOKEN is unset, the controlplane runs in OPEN_MODE — /v1/namespaces/:ns/* accepts any caller. Loud warn on boot. Required for the local-laptop dev loop in RUNBOOK Phase 1.

Risk: if a dev's local controlplane is exposed (ngrok, port forward), it's a full-access endpoint.

Upgrade: bind to 127.0.0.1 in OPEN_MODE so it physically can't be reached from off-host. Trivial change; do it before adding any "bring a fresh laptop into the loop" workflow.

TLS pinning

Today: daemon trusts whatever cert the system trust store accepts on the controlplane URL. Standard browser-style PKI.

Risk: a compromised CA or MITM with a valid cert can intercept. Real but small.

Upgrade: pin the controlplane's cert fingerprint in the daemon config. Operationally annoying — every cert rotation is a deploy. Defer until a real threat model warrants it.

Closed (do not re-litigate)

Legacy namespaced exec-result auth bypass

The auth hook now exempts /v1/namespaces/:ns/workers/:id/exec/:exec_id/result from token validation. This is a transitional shim while old daemons still POST to that URL. Plan: remove the exemption once every deployed daemon is past nymph v0.1.1 (which switched to the un-namespaced peer /v1/daemon/exec/:exec_id/result). Track in lakeshore-controlplane/src/auth.ts.