Skip to content

harden: bind to 127.0.0.1 by default; honor CAMOFOX_HOST#4730

Closed
robertbentleyhq wants to merge 6 commits into
jo-inc:masterfrom
robertbentleyhq:harden/access-key-and-loopback-bind
Closed

harden: bind to 127.0.0.1 by default; honor CAMOFOX_HOST#4730
robertbentleyhq wants to merge 6 commits into
jo-inc:masterfrom
robertbentleyhq:harden/access-key-and-loopback-bind

Conversation

@robertbentleyhq

Copy link
Copy Markdown

Summary

server.js previously called app.listen(PORT) with no host argument, which makes Node bind to the unspecified address (0.0.0.0 on most kernels). The launchd wrapper this server ships with on macOS exports CAMOFOX_HOST=localhost, but nothing in the code reads it -- so the service is reachable on every interface the host has configured. This PR closes that gap and adds documentation + tests for the new env var.

The companion global access-key gate (CAMOFOX_ACCESS_KEY + accessKeyMiddleware) is already in place and unchanged here. Binding to loopback by default gives operators a defense-in-depth layer underneath that auth so the network surface is also reduced.

Changes

  • lib/config.js — add host to the loaded config, defaulting to 127.0.0.1. Trim whitespace, treat empty string as unset. Forward CAMOFOX_HOST through serverEnv so subprocesses see the same value.
  • server.js — pass HOST to app.listen(PORT, HOST, ...), log it on server:started and EADDRINUSE for operator visibility.
  • README.md — document CAMOFOX_HOST in the env-var table with the 127.0.0.1 default and the Docker/Fly escape hatch.
  • tests/unit/config.test.js — lock the loopback default, explicit-override, whitespace-trim, empty-string fallback, and serverEnv-forwarding contracts (5 new tests).

Compatibility

Docker / Fly.io / Railway deployments that already want public ingress set CAMOFOX_HOST=0.0.0.0 (or just HOST if their orchestrator does that) -- they continue to work unchanged. Only npm start on a developer laptop changes behavior: it stops listening on 0.0.0.0 and listens on loopback instead.

Test plan

  • NODE_OPTIONS='--experimental-vm-modules' npx jest tests/unit/config.test.js -- 7 pass
  • NODE_OPTIONS='--experimental-vm-modules' npx jest tests/unit/auth.test.js tests/unit/accessKey.test.js -- 70 pass (no regressions on the existing access-key middleware)
  • Manual smoke: restart service, confirm lsof -nP -iTCP:9377 -sTCP:LISTEN shows TCP 127.0.0.1:9377 (LISTEN) (was TCP *:9377 before)
  • Manual smoke: curl http://127.0.0.1:9377/health returns 200; curl -H "Authorization: Bearer ..." http://127.0.0.1:9377/tabs?userId=test returns 200; unauthenticated /tabs returns 401

robertbentleyhq and others added 3 commits July 8, 2026 08:23
server.js previously called `app.listen(PORT)` with no host argument, which
makes Node bind to the unspecified address (0.0.0.0 on most kernels). The
launchd wrapper on macOS exported CAMOFOX_HOST=localhost but nothing read it,
so the service was reachable on every interface the host had configured -- a
silent regression of the host-bind config.

Changes:

- lib/config.js: add `host` to the loaded config, defaulting to `127.0.0.1`
  (loopback-only). Trim whitespace and treat empty-string as unset. Forward
  CAMOFOX_HOST through `serverEnv` so subprocesses see the same value.
- server.js: pass HOST to `app.listen(PORT, HOST, ...)`, log it on
  server:started / EADDRINUSE for operator visibility.
- README.md: document CAMOFOX_HOST in the env-var table with the
  127.0.0.1 default and the Docker/Fly escape hatch.
- tests/unit/config.test.js: lock the loopback default, explicit-override,
  whitespace trim, empty-string fallback, and serverEnv-forwarding contracts.

Why default to loopback: the global CAMOFOX_ACCESS_KEY gate added earlier
already enforces auth, but defense-in-depth says the network surface should
not be reachable beyond the local machine unless the operator opts in. The
Fly.io / Docker deployment flow already sets CAMOFOX_HOST=0.0.0.0 explicitly
when it wants public ingress, so the new default doesn't break those.

This change pairs with rotating CAMOFOX_ACCESS_KEY on the local launchd
wrapper to give browser-control routes both a network-layer and an
application-layer gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Agent-browser sessions need to inspect localhost while the service still uses DataImpulse for external browsing. Add a loopback proxy bypass to normalized Playwright proxy options so local app testing stays local.\n\nAlso quarantine persisted storage-state files when Playwright rejects them during context creation, then retry without the stale state. This prevents one bad profile from blocking tab creation for the default agent identity.
@robertbentleyhq
robertbentleyhq force-pushed the harden/access-key-and-loopback-bind branch from 7a48fd3 to 689cbe5 Compare July 8, 2026 12:38
robertbentleyhq and others added 3 commits July 8, 2026 08:41
… Firefox 135 tab creation

Why: every brain MCP browser_* tab-create had 500'd since ~Jul 8. Root cause
established from ground truth, not guesses: commit 080fcd8 ("update browser
runtime packages") bumped playwright-core 1.59.1 -> 1.61.1, and playwright-core
1.61 sends a new viewport.isMobile field in the Browser.setDefaultViewport
protocol call. The installed Camoufox Firefox binary (135.0.1/beta.24,
~/Library/Caches/camoufox/Camoufox.app) runs an older Juggler build that
rejects the unknown field, so the browser LAUNCHES fine but every
tab-create fails. camoufox-js declares playwright-core:"*" (no upper bound),
which is how the breaking minor slipped in silently with the dep sweep.

What was considered: upgrading the Camoufox binary instead (forward fix) was
rejected for tonight — larger surface, unattended overnight run; the exact
downgrade pin is surgical, needs no native rebuild (better-sqlite3 untouched),
and was verified live end-to-end after a launchctl kickstart -k of
com.bentleyhq.camofox: browser_health browserConnected:true, browser_open_tab
returns a tabId (previously 500), browser_screenshot renders the live
dashboard, browser_close_tab ok, no setDefaultViewport errors in the logs.

UNPIN CONDITION: when the Camoufox Firefox binary is upgraded past 135.0.1,
playwright-core can move forward again — re-test tab creation (the
setDefaultViewport call) before and after unpinning. Until then, any
"helpful" playwright-core upgrade re-breaks all brain browser_* tools.

Context: this working tree IS production (launchd runs it directly, no deploy
step, same model as brain-mcp-server) — committed so a stray git checkout
cannot silently revert the running fix. Applied during night-shift jo-inc#16
(overnight 2026-07-18 -> 07-19) on the branch that was already checked out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght-core pin

Why: upstream (jo-inc/camofox-browser) moved 1.11.2 -> 1.12.0 (38 commits) and
independently fixed the root cause of the Jul-8 breakage this branch had pinned
around: afd7638 "drop fixed viewport for camoufox compatibility" stops sending
the Browser.setDefaultViewport call that Camoufox Firefox 135's Juggler
rejects. With the call gone, the exact playwright-core=1.59.1 pin (b183706) is
no longer load-bearing, so package.json returns to upstream's ^1.58.0 range.
NOTE: upstream's own package-lock still resolves playwright-core to 1.59.1, so
the running version is unchanged today - the pin's protection now lives in the
upstream lockfile + the removed viewport call, not in our manifest.

Conflicts resolved (3): server.js import block - union of our
quarantinePersistedStorageState import and upstream's new browser-errors /
process-ownership imports; tests/unit/config.test.js - kept our host-binding
describe block (upstream has no equivalent); package-lock.json - took
upstream's, regenerated via npm install against the merged manifest.

Fork-only patches retained through the merge (still absent upstream as of
1.12.0): 0821b1e loopback bind by default + CAMOFOX_HOST; 12040cc agent-session
hardening (localhost proxy bypass + storage-state quarantine); 689cbe5 idle
health-probe skip. Upstream DID adopt access-key auth (jo-inc#586) - that came in
with the 1.11.2 baseline already, nothing to reconcile here.

Verification at merge time: 53/53 unit+plugin suites green (744 tests); the 13
red suites are tests/e2e/* which require a live SERVER_URL env and fail
identically pre-merge. Live service round-trip verified after launchd restart
(recorded in the session log).

Pre-merge state pushed to fork as b183706 - revert path is
git reset --hard b183706 && npm install && kickstart.
@skyfallsin

Copy link
Copy Markdown
Contributor

Superseded by #8096, which added the configurable bind host on current master with focused configuration coverage and was released in v1.13.0. This branch now conflicts and also carries unrelated persistence/proxy changes, so we are closing it rather than merging the duplicate implementation. Thank you for raising the default-bind hardening motivation.

@skyfallsin skyfallsin closed this Jul 20, 2026
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.

2 participants