fix(workspace-server): default-bind to 127.0.0.1 in dev-mode fail-open (closes #7) #8
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/s8-bind-loopback-dev"
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?
Closes #7. Fix S-8 from the security posture audit RFC v1 (
molecule-ai/internal#28).What changed
In dev-mode (
MOLECULE_ENV=dev|developmentANDADMIN_TOKENunset) the AdminAuth chain fails open by design — canvas at:3000calls workspace-server at:8080with no bearer token. Pairing that with the existing wildcard bind on:8080exposed unauthenticatedPOST /workspacesto any same-LAN peer.This PR couples the bind narrowness to the same signal that gates the auth fail-open. New
resolveBindHost()incmd/server/main.gowith this precedence:BIND_ADDRenv var (explicit operator override, any value)middleware.IsDevModeFailOpen()true →127.0.0.1""(Go binds every interface — existing prod/self-host shape)ADMIN_TOKENMOLECULE_ENVBIND_ADDRdev/development127.0.0.1(NEW)*(unchanged from today)production/ unset / other*(unchanged from today)<host><host>(operator override)Startup log now includes the resolved bind + dev-mode-fail-open state so operators can audit the listener shape from logs alone.
Why this approach
Two safety levers — bind narrowness and auth strength — move together. A production deploy (
ADMIN_TOKENset) keeps binding to all interfaces because the auth chain is doing its job; a dev Mac (noADMIN_TOKEN,MOLECULE_ENV=dev) is reachable only via loopback because the auth chain is fail-open. No new state, no new env-var configuration the operator has to remember to set.Alternatives rejected
ADMIN_TOKEN— would force every Mac dev to set + propagate it to canvas viaNEXT_PUBLIC_ADMIN_TOKEN, then rebuild canvas bundle. Friction the platform team chose to avoid; preserved here.*+ tighten auth — 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).Tests
workspace-server/cmd/server/bind_test.go— 8t.Setenvtable cases:Mutation test: deleting the
if middleware.IsDevModeFailOpen()branch (soresolveBindHostalways returns"") makes bothno_bindaddr_devmode_*cases FAIL — confirms the tests aren't tautologies.go vet ./cmd/server/...clean.go build ./...clean (the newmiddlewareimport incmd/server/main.gois the only new dep edge —internal/middlewarewas already a transitive dep viarouter).Manual verification (post-deploy)
After this lands and operator restarts workspace-server:
Production smoke (run on staging tenant, where
ADMIN_TOKENis set):Security review (Phase 2 questions)
BIND_ADDR) at startup. HTTP request flow unchanged.BIND_ADDR=0.0.0.0(and ideally pair withADMIN_TOKEN).Versioning + backwards compatibility
BIND_ADDRis a new additive env var. Absence preserves prior behavior except in dev-mode (where the default narrows to127.0.0.1). Affected operators:MOLECULE_ENV=dev. This is the goal.http://<lan-ip>:8080from a separate machine on their LAN: must setBIND_ADDR=0.0.0.0(back to today's shape) OR setADMIN_TOKEN(which makesIsDevModeFailOpen()false → restores*). Documented in handbook §5 (added in this PR's companion edit, see below).ADMIN_TOKENis always set in tenant env, soIsDevModeFailOpen()returns false, so default bind stays""(all interfaces). CI smoke and tenant runtime are unaffected.No semver bump (internal service). No schema, no API version. No migration.
Documentation
~/.molecule-ai/handbook.md§5 gets a new "Local-dev exposure model — workspace-server" subsection (committed locally; not in this monorepo since handbook lives on the operator host).cmd/server/main.goreferencingmolecule-core#7.Rollout / rollback
git revertthe merge OR setBIND_ADDR=0.0.0.0to immediately restore today's shape without a code revert.Out of scope (parked as separate issues)
*:3000) — Next.js dev server. Same fix shape (next dev -H 127.0.0.1). Needs a separate PR in the canvas repo.*:5432) — Docker Desktop config. Operator one-line fix.Five-axis self-review (hostile)
Three weakest spots:
resolveBindHost()is a pure function and tested as such; thehttp.Server.Addr = fmt.Sprintf("%s:%s", bindHost, port)glue is not exercised by a test. Risk: low — the formatting is straightforward and the http.Server contract is a stdlib guarantee. Mitigated by the manual verification step above.MOLECULE_ENV=devis the OS-environment substring match, not a structural type. A typo likeMOLECULE_ENV=Devwould still trigger loopback (devmode.go lowercases) butMOLECULE_ENV=develwould not (allowlisted set is{"development","dev"}). Pre-existing behavior ofIsDevModeFailOpen(); not regressed here.docs/directory in molecule-core. If the handbook drifts (e.g., operator host rebuilds), the table is the only authoritative source. Acceptable for now since the handbook is the documented SSOT for ops; longer-term we should consider adocs/operations/local-dev.mdin this repo and amol_handbook_syncjob.🤖 Generated with Claude Code
Hongming-approved (chat 2026-05-07 'knock out core#8 + core#12'). S-8 SECURITY fix: workspace-server default-bind to 127.0.0.1 in dev paths (was 0.0.0.0 fail-open).