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.