fix(workspace-server): default-bind to 127.0.0.1 in dev mode + close S-8 LAN fail-open #7
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The workspace-server fails open on
*:8080for any caller on the same network as the founder's Mac during local development. Same-LAN peer can create / list / delete workspaces with no token. Verified viacurl -X POST http://<mac-LAN-ip>:8080/workspaces -d '{"name":"unauth-probe"}'returning201 Created.This is finding S-8 from the security posture audit RFC v1 (
molecule-ai/internal#28, merged 2026-05-07).Root cause — both the bind AND the auth chain are permissive
The fail-open requires BOTH conditions, but on a typical local-dev Mac BOTH hold simultaneously:
Bind on
*:cmd/server/main.go:341-344doesAddr: fmt.Sprintf(":%s", port). Go'snet.Listen("tcp", ":8080")binds every interface, not just loopback. Same-LAN peers can reach the listener.AdminAuthfail-open:internal/middleware/wsauth_middleware.go:155-186. Two branches let unauthenticated requests through:ADMIN_TOKEN=""AND no live workspace tokens in DB →c.Next(). Path documented as "fresh install before first token mint."isDevModeFailOpen()→ADMIN_TOKEN=""ANDMOLECULE_ENV=dev|development→c.Next(). Documented ininternal/middleware/devmode.goas "local-dev escape hatch so canvas at :3000 keeps working without ADMIN_TOKEN friction."In production SaaS (per saved memory
reference_saas_workspace_server_auth_chain):ADMIN_TOKENis always setMOLECULE_ENV=production*is fine because the only network reachability is from the tenant's own EC2 + the controlplane's reverse proxy — no random LAN peers.Local dev hits both fail-open conditions:
ADMIN_TOKEN(the canvas dev workflow doesn't require it)MOLECULE_ENV=devfor the dev-mode escape hatch*exposes to coffee shop / conference / home guest-wifi peersAffected surfaces
workspace-server/cmd/server/main.go:341-344:8080becomes*:8080workspace-server/internal/middleware/wsauth_middleware.go:155-186workspace-server/internal/middleware/devmode.go:49-56isDevModeFailOpen()predicate (also called byWorkspaceAuth)~/.molecule-ai/handbook.mdPrior art reviewed
reference_saas_workspace_server_auth_chain(saved memory): documents the production auth chain — tenant subdomain + per-tenantADMIN_TOKEN+X-Molecule-Org-Id. None of this applies for local dev → no auth on the wire.feedback_local_must_mimic_production(saved memory): "when local-dev skips a prod-only path, bugs there are structurally invisible until they hit a paying tenant." A bind change should NOT break the local-dev mimic; binding to127.0.0.1is more restrictive than prod, not less, so it doesn't hide a prod bug — it just shrinks the local attack surface.#166(referenced in router.go:106 + 134-136): wrapped/workspacesPOST/DELETE in AdminAuth; closed C1+C20 against attackers WITH valid network reach. Did not change the bind.#623(referenced in wsauth_middleware.go:150-154): closed an Origin-header bypass onCanvasOrBearer. Same authorial intent: don't trust forgeable headers.#684(referenced in devmode.go:43): added theADMIN_TOKENopt-in. Half of the eventual closure.External prior art:
http://localhost:8888/?token=...). Adopting: shows the pattern is well-trodden. Rejecting for THIS PR: complicates the canvas integration (canvas would need to read the log or the URL fragment); larger change than needed today.listen_addresses+pg_hba.conf: separate the network-binding decision from the auth-method decision. Adopting: same shape — bind narrowness AND auth strength are independent levers; this fix touches only the bind axis.-H tcp://.... Adopting: localhost-by-default is the safe shape.protected-mode yes: when binding to all interfaces AND no auth set, Redis refuses external connections by default. Adopting: same coupling — auth-or-bind, never neither. Rejecting at full strength: "refuse all external" is too aggressive for our self-hosted shape.Proposed approach
Smallest correct fix: introduce
BIND_ADDRenv var. Default the bind to127.0.0.1ONLY whenisDevModeFailOpen()returns true (=ADMIN_TOKEN=""ANDMOLECULE_ENVis a dev value). Production and non-dev self-hosted keep binding on*(existing behavior).This couples the bind-narrowness to the SAME signal the auth fail-open uses. Two safety levers move together; no new state.
Alternatives considered
Always require
ADMIN_TOKEN(remove dev-mode fail-open entirely). REJECTED: contradicts the documented design goal (devmode.go:8-21) of zero-friction local smoke tests. Would force every Mac dev to set + propagateADMIN_TOKENto canvas viaNEXT_PUBLIC_ADMIN_TOKEN, then rebuild canvas bundle. Friction the platform team already chose to avoid.Auto-mint per-process random token + log to stderr (Jupyter pattern). REJECTED for this PR: bigger change, would require canvas to read either the log file or a startup file to pick up the token. Solving a local-dev UX problem at the cost of integration complexity. Worth doing as a follow-up if the bind narrowing alone proves insufficient.
Bind always
*regardless of mode + tighten auth instead. REJECTED: doesn't help operators on shared wifi. Network surface narrowing is a strictly defensive layer in addition to auth, not a replacement. Defense-in-depth.SSOT decision
The bind address is set in exactly one place:
cmd/server/main.go:341-344. This PR keeps that as SSOT — the dispatch logic for "should I narrow to loopback?" lives inline at that one site, callingmiddleware.IsDevModeFailOpen()(already exposed as a public helper atdevmode.go:63). No new file, no new abstraction.Security-aware design check (the 4 questions, per SOP)
BIND_ADDR) at startup. The HTTP request flow is unchanged.BIND_ADDR=0.0.0.0explicitly (and pair it withADMIN_TOKENif they want safety).Versioning + backwards compatibility
BIND_ADDRis a NEW env var; absence preserves the prior behavior except in dev-mode (where the default narrows to127.0.0.1). This is a behavior change for operators running withMOLECULE_ENV=dev|developmentandADMIN_TOKENunset.http://<machine-LAN-ip>:8080from a different machine on their LAN will need to either setBIND_ADDR=0.0.0.0(back to today's shape) OR setADMIN_TOKEN(which makesisDevModeFailOpen()false and restores the*default).Acceptance criteria for this issue
cmd/server/main.gobind logic uses the conditionalbind_test.go(BIND_ADDR explicit, BIND_ADDR unset + dev → 127.0.0.1, BIND_ADDR unset + non-dev → empty)if middleware.IsDevModeFailOpen()line makes the dev-mode test failMOLECULE_ENV=dev ADMIN_TOKEN= go run ./cmd/server→lsof -iTCP:8080 -sTCP:LISTENshows127.0.0.1:8080not*:8080.curl http://<mac-LAN-ip>:8080/healthfrom a same-LAN device returns connection refused.Out of scope (parked as follow-ups)
*:3000): Next.js dev server. Same fix shape (next dev -H 127.0.0.1). Separate PR.*:5432): Docker Desktop config. Operator one-line fix. Separate PR.