Skip to content

Make stacktape dev safe on a shared machine and network - #317

Merged
matuscongrady merged 2 commits into
mainfrom
fix/dev-mode-security
Oct 23, 2026
Merged

matuscongrady merged 2 commits into
mainfrom
fix/dev-mode-security

Conversation

@matuscongrady

@matuscongrady matuscongrady commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Dev mode exposed more than the developer's own machine. The agent HTTP API listened on every interface with no authentication, and a web page could send it simple POSTs (SQL with confirm: true, /stop, rebuilds). /env returned raw secrets. Local databases were published on all interfaces with the fixed password localdevpassword and relayed through the public, unauthenticated bore.pub. Two worktrees of one project on one stage shared and stopped each other's database containers, and the agent reported ready while a workload had failed.

What changed:

  • Agent: listens on 127.0.0.1 only. Every request needs a per-session token from ~/.stacktape/dev-agent-tokens/<port>.token (directory 0700, file 0600, removed on stop; only the path is printed). Requests with a foreign Host, any Origin header, or a non-JSON POST are refused. dev:stop, the MCP stacktape_dev tool, the startup/compact docs and the live dev canary send the token.
  • /env masks values read from $Secret/$SsmParam, values under secret-like names (including container AWS credentials) and passwords in connection strings, using the shared scrubSensitiveText.
  • No public relay: Lambda tunnels run only when STACKTAPE_TUNNEL_SERVER names the user's own relay. Otherwise dev mode warns which Lambda can't reach which local database and points to --remote-resources. No new flags.
  • Local databases without a configured password get a random password stored at .stacktape/dev-data/<stage>/<resource>/password. Data created with the old fixed password keeps it, with a warning; --freshDb replaces data and password. All database ports are published on 127.0.0.1.
  • Container names include a hash of the working directory. Found while testing: a port-bind retry failed with "container name in use" because Docker had already created the container, Postgres readiness passed against the image's temporary init server, and --freshDb could not delete Linux data owned by the container user. All three are fixed.
  • Readiness: ready requires the session started and every workload running. Failed workloads appear in /status and the AGENT_READY payload, and the daemon parent stops the session and exits non-zero with the workload errors. A hung daemon start now kills the whole process tree.

How it was checked:

  • New scenarios: the real agent server is probed with no token, a wrong token, a rebound Host, an Origin header and a text/plain stop, and checked for masked /env, LAN unreachability and token-file cleanup. Readiness is checked with failed, starting and running workloads. A Docker test starts two worktrees on one stage through the real local-resource code, plus an upgraded checkout whose old-format container holds legacy data, plus --freshDb. dev:stop is tested with and without the token. The MCP fixture daemons now require the token.
  • Each new security check, the directory hash and the loopback publish were reverted one at a time and the tests failed.
  • Passed: local-dev (70), local-dev-containers, local-dev-ministack, cli source tests (160 files), terminal ownership, operation rules, llm-docs/mcp-docs/corpus, generate:check, typecheck, lint, fmt. check:public fails only on packaging-harness.spec.ts, which also fails on main. test:tools now fails on main's unregistered dependency-installer.spec.ts from Reuse the runner's dependency install on Git push deploys [skip ci] #316.
  • Not run: the live local-dev-live and local-dev-lambda-live canaries (no API key in this worktree). Container-to-database reachability through host.docker.internal on Docker Desktop is not verified; native Linux host networking is.

Follow-up fixes (4281dc3): The live canary showed that dev:stop --agentPort failed when run outside the directory dev mode was started in, because the token lived in that directory. The token now lives in the user's state directory under the agent's port, so dev:stop, the MCP tool and the lock-file health check find it from any directory; a starting agent removes token files whose port nothing listens on. Separately, dev --agent returned no result when it found an agent already running, so the MCP stacktape_dev tool built a session without a token and reported the agent unreachable on every retry. It now returns the same ready payload as a new agent, and the tool no longer guesses the port when the CLI reports none. New tests cover dev:stop from another directory and from another user, the reuse result through the real CLI, the MCP reuse path, the token directory's permissions and stale-file pruning. The live dev-mode-local-loop canary now passes the dev:stop and leftover checks; three checks in the interactive, terminal-SIGTERM and occupied-ports scenarios fail identically on origin/main on this machine, because native Linux Docker runs the API container with host networking, which publishes no port for the canary to find and cannot move the API off an occupied port.

Risks:

  • Breaking for Lambda + local database users: functions no longer reach local databases unless they run their own relay. The getting-started guide now says to use --remote-resources. The local-dev-lambda-live release lane now needs STACKTAPE_TUNNEL_SERVER set to a relay we control.
  • Scripts that called the agent without a token now get 401.
  • Running dev sessions from an older CLI keep their old container names; a new session removes an old container only when it mounts the same data directory.

matuscongrady and others added 2 commits October 24, 2026 00:51
The dev agent listened on every interface without authentication, local
databases were published on all interfaces with a fixed password and
relayed to the public bore.pub, worktrees of one project shared database
containers, and the agent reported ready while a workload had failed.

- The agent listens on 127.0.0.1 and requires a per-session token from
  .stacktape/dev-agent/<port>.token (mode 0600) on every request. It
  refuses foreign Host headers, any Origin header and non-JSON POSTs.
  dev:stop, the MCP dev tool and the live canary send the token.
- /env masks secret-sourced and secret-named values and passwords in
  connection strings, using the shared scrubber.
- Lambda tunnels run only through a relay named in
  STACKTAPE_TUNNEL_SERVER; otherwise dev mode warns which functions cannot
  reach which local databases and points to --remote-resources.
- Local databases without a configured password get a generated one,
  stored next to their data. Data created with the old fixed password
  keeps it, with a warning; --freshDb replaces both.
- Database ports are published on 127.0.0.1 only. Container names include
  a hash of the working directory. A port-bind retry removes the
  half-created container, Postgres readiness is checked over TCP, and
  --freshDb can delete data the database container owns on Linux.
- Readiness requires every workload running; failed workloads appear in
  /status and AGENT_READY, and the daemon parent stops the session and
  exits non-zero. A hung daemon start kills the whole process tree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…an agent [skip ci]

The token moves from <workingDir>/.stacktape/dev-agent/<port>.token to the user's
~/.stacktape/dev-agent-tokens/<port>.token (directory 0700, file 0600). dev:stop, the
MCP dev tool and getRunningAgent read it by port, so `dev:stop --agentPort` works from
any directory. A starting agent removes token files whose port nothing listens on.

`dev --agent` now returns the ready payload when it finds an agent already running,
so the MCP tool gets the port instead of building a session that the agent refuses.
The tool no longer falls back to the requested port when the CLI reports none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@matuscongrady
matuscongrady merged commit 20950b0 into main Oct 23, 2026
@matuscongrady
matuscongrady deleted the fix/dev-mode-security branch October 23, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant