Skip to content

docs(enterprise): document outbound network access and guard it in CI - #3131

Merged
benjaminshafii merged 2 commits into
devfrom
docs/outbound-access-allowlist
Jul 25, 2026
Merged

docs(enterprise): document outbound network access and guard it in CI#3131
benjaminshafii merged 2 commits into
devfrom
docs/outbound-access-allowlist

Conversation

@benjaminshafii

Copy link
Copy Markdown
Member

Why

Enterprise customers run OpenWork on networks where all outbound connections are blocked by default and IT approves each destination individually. Nothing in this repo listed what OpenWork needs to reach — so every deployment rediscovered it by hitting failures.

Two destinations were named nowhere in the repo, and both produce confusing symptoms:

Host Why it is needed Symptom when only the obvious host is approved
objects.githubusercontent.com Redirect target of every GitHub release-asset download (redirect: "follow") IT approves github.com; the download starts and then dies partway through
registry.npmjs.org npx -y openwork-ui-mcp on the packaged desktop path UI-control MCP is silently unavailable while the app otherwise opens normally

What this adds

docs/enterprise/outbound-access.md — written for customer IT and non-technical admins. Deliberately avoids the word "egress". Leads with a copy-pasteable minimum allowlist, then tables with purpose / required-or-optional / what breaks when blocked.

Desktop client access is documented separately from self-hosted Den access, because different teams approve them on different networks. It also records that self-hosted Den replaces the hosted origins and that the desktop needs only one origin (API and MCP are derived as <baseUrl>/api/den/...), and that Den's only hard requirements are its MySQL endpoint plus ghcr.io at image-pull time — everything else is per-feature.

docs/enterprise/outbound-access.json — machine-readable, 34 hosts:

kind count requirement count
fetched 23 opt-in 16
link-only 7 optional 11
subprocess 2 required-in-practice 4
redirect-target 1 required-for-cloud 2
schema-string 1 required 1

The kind field is what lets indirectly-reached destinations be documented at all — a redirect target and an npx subprocess have no URL literal to find in the code.

scripts/check-outbound-access.mjs + a ci-tests.yml step — scans the shipped client surfaces (apps/desktop/electron, apps/installer/src, apps/server/src, apps/app/src, packages/openwork-bootstrap/bin) and fails when a hostname appears in code but not in the manifest. So a feature PR that introduces a new destination must document its enterprise impact. It also flags fetched entries whose host no longer exists in code, so the list cannot rot in either direction. RFC 2606 documentation names, loopback, and standards/schema hosts are ignored with a documented reason in the script.

Classifications corrected while verifying

Every row was checked against a real call site rather than assumed. Corrections:

  • api.githubcopilot.com — not a provider call path; an example custom MCP URL placeholder (apps/app/src/i18n/locales/en.ts:803).
  • ollama.com — link-only help text (ollama-config.tsx:312); actual Ollama traffic is loopback http://localhost:11434/v1.
  • code.claude.com — source comment only (apps/server/src/claude-plugin-bundle.ts:11), no runtime call.
  • linear.app — demo transcript sample link (session-surface.tsx:168), not a fetch.
  • mail.google.com — link-only Gmail draft/thread URLs emitted as output (google-workspace.ts:925,929).
  • schemas.agentskills.io — schema identifier string, never fetched (connect-skill-catalog.ts:11,15).
  • registry.npmjs.org — also covers the dev-only npx -y @openwork/handsfree mcp fallback (computer-use.mjs:50).
  • Added ghcr.io for the self-hosted image/chart pull requirement (values.yaml:184,212,241).

Compatibility

No breaking changes for self-hosted deployments. No product code, configuration, environment variable, API, Helm chart, or database surface is touched. This is documentation, one check script, one CI step, one package.json script.

One operational note for maintainers: because the check now runs in ci-tests.yml, an open PR that introduces a new outbound destination will fail until it adds a manifest line. That is the intended forcing function.

Tests run

```
node scripts/check-outbound-access.mjs

Outbound access manifest covers 34 hosts. Scanned 31 external hosts from

shipped client surfaces. No stale fetched entries.

pnpm check:outbound-access # pass
node scripts/check-outbound-access.mjs --json # valid JSON, ok: true
```

Negative controls, run in three different files so the scan surface is genuinely covered:

Injected https://collector.acme-telemetry.io/v1 into apps/app/src/app/lib/desktop.ts:
```
Outbound access manifest check failed.

  • Host collector.acme-telemetry.io is used in apps/app/src/app/lib/desktop.ts:601
    but is missing from docs/enterprise/outbound-access.json. Add it to
    docs/enterprise/outbound-access.json (see docs/enterprise/outbound-access.md).
    exit=1
    ```

Injected a fake host into apps/installer/src/release-asset.ts → caught. Deleted the models.openworklabs.com manifest entry → caught at apps/server/src/opencode-models-url.ts:2. Added a fake kind: "fetched" entry with no code literal → correctly reported as stale:
```

Fraimz

Skipped, stated explicitly per AGENTS.md: documentation plus a CI check script, no runtime path changed. The executable proof is the four controls above.

Enterprise networks block outbound connections by default and IT must approve
each destination, but nothing in this repo listed what OpenWork needs to reach.
Two destinations were named nowhere at all:

- objects.githubusercontent.com, the redirect target of every release-asset
  download. Approving only github.com lets a download start and then fail
  partway through.
- registry.npmjs.org, used by `npx -y openwork-ui-mcp` in packaged builds.
  Blocked, the UI-control MCP is silently unavailable while the app still opens.

Add docs/enterprise/outbound-access.md for customer IT, in plain language, with
a copy-pasteable minimum allowlist and a what-breaks-when-blocked column. Desktop
client access is documented separately from self-hosted Den access because
different teams approve them on different networks.

Back it with docs/enterprise/outbound-access.json and
scripts/check-outbound-access.mjs, wired into ci-tests.yml, so a feature that
introduces a new destination must document its enterprise impact. The check also
reports manifest entries that no longer exist in code, so the list cannot rot.
Hosts reached indirectly (redirect targets, subprocesses, links, schema strings)
are classified by kind and exempt from the staleness rule.

Docs, one check script, one CI step, one package script. No product behavior,
configuration, API, or chart surface changes.
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment Jul 25, 2026 6:04pm
openwork-den Ready Ready Preview, Comment Jul 25, 2026 6:04pm
openwork-den-worker-proxy Ready Ready Preview, Comment Jul 25, 2026 6:04pm
openwork-diagnostics Ready Ready Preview, Comment Jul 25, 2026 6:04pm
openwork-landing Ready Ready Preview, Comment, Open in v0 Jul 25, 2026 6:04pm

Verified against a live OpenWork release asset: github.com now redirects
release downloads to release-assets.githubusercontent.com, not
objects.githubusercontent.com. An allowlist naming only the old host would
still let downloads start and then fail partway through, which is the exact
symptom this page exists to prevent.

List both. release-assets.githubusercontent.com is what GitHub serves today;
objects.githubusercontent.com stays documented for older clients and any
staged rollback.
@benjaminshafii

Copy link
Copy Markdown
Member Author

Correction pushed: the GitHub release-asset host has changed

While writing the companion private-network deployment guide I verified the redirect chain against a live OpenWork release asset instead of trusting the repo's own prose. The current target is not objects.githubusercontent.com:

$ curl -sIL -o /dev/null -w '%{url_effective}' \
    https://github.com/different-ai/openwork/releases/latest/download/latest-mac.yml

https://release-assets.githubusercontent.com/github-production-release-asset/1133911335/...

Full chain:

  1. github.com/.../releases/latest/download/latest-mac.yml → 302
  2. github.com/.../releases/download/v0.18.1/latest-mac.yml → 302
  3. release-assets.githubusercontent.com/...

GitHub migrated the release-asset CDN. This PR originally named only objects.githubusercontent.com, so it would have told customer IT to open the wrong host — producing exactly the start-then-die download failure the page exists to prevent. That would have been worse than no doc at all.

Both are now listed: release-assets.githubusercontent.com as the host GitHub serves today, and objects.githubusercontent.com retained for older clients and any staged rollback. Manifest is now 35 hosts and the check still passes.

Worth noting for reviewers: the only reason this surfaced is that the repo describes this destination generically as "GitHub's redirected release-asset host" and never names it, so no code or doc anywhere pinned the value and nothing broke visibly when it changed.

@benjaminshafii
benjaminshafii merged commit cb4c139 into dev Jul 25, 2026
17 checks passed
benjaminshafii added a commit that referenced this pull request Jul 25, 2026
…ce (#3139)

* docs(enterprise): document outbound network access and guard it in CI

Enterprise networks block outbound connections by default and IT must approve
each destination, but nothing in this repo listed what OpenWork needs to reach.
Two destinations were named nowhere at all:

- objects.githubusercontent.com, the redirect target of every release-asset
  download. Approving only github.com lets a download start and then fail
  partway through.
- registry.npmjs.org, used by `npx -y openwork-ui-mcp` in packaged builds.
  Blocked, the UI-control MCP is silently unavailable while the app still opens.

Add docs/enterprise/outbound-access.md for customer IT, in plain language, with
a copy-pasteable minimum allowlist and a what-breaks-when-blocked column. Desktop
client access is documented separately from self-hosted Den access because
different teams approve them on different networks.

Back it with docs/enterprise/outbound-access.json and
scripts/check-outbound-access.mjs, wired into ci-tests.yml, so a feature that
introduces a new destination must document its enterprise impact. The check also
reports manifest entries that no longer exist in code, so the list cannot rot.
Hosts reached indirectly (redirect targets, subprocesses, links, schema strings)
are classified by kind and exempt from the staleness rule.

Docs, one check script, one CI step, one package script. No product behavior,
configuration, API, or chart surface changes.

* docs(self-host): make the cloud catalog diagnostic actionable on-prem

Settings > Debug runs a bounded initialize -> tools/list check against the
managed openwork-cloud MCP entry. It carries an organization MCP bearer token,
so the probe refuses to send that credential to an origin outside its trust
list. DEFAULT_TRUSTED_ORIGINS holds only the hosted origins, so on every
self-hosted deployment the check returns untrusted_endpoint and never runs.

The escape hatch, OPENWORK_AGENT_DIAGNOSTICS_TRUSTED_ORIGINS, appeared in
exactly two places in the repo: its own source file and its own test. The
guidance an operator actually saw was "have the server administrator explicitly
trust the development origin" -- which calls a production on-prem Den a
development origin and never names the variable, so nobody could act on it.

Name the variable in the operator action, and document it in the self-hosting
guide with the real format rules enforced by configuredTrustedOrigins: bare
origins, comma-separated, https except loopback, invalid entries ignored rather
than failing open, additive to the hosted defaults. The docs state explicitly
that this enables the diagnostic only and that Cloud MCP itself works without
it, so operators do not read a skipped probe as a broken deployment.

The trust model is deliberately unchanged: agent-context-cloud-probe.ts is
untouched, so which origins are trusted cannot differ from before. Trusting the
endpoint's own origin would be circular, since the probe exists to validate the
very config that names it. Tests pin that an unlisted self-hosted origin still
performs no fetch, and that the action keeps naming the variable.

* docs(helm): expose the private-network MCP opt-out to operators

Den's SSRF guard refuses to fetch private/reserved addresses on behalf of
users, which is correct on hosted multi-tenant deployments. But a common
enterprise topology puts Den inside the customer's private network with their
MCP servers on private addresses, and there the guard blocks every internal
server. The operator sees MCP_URL_BLOCKED and is told to "use a public HTTPS
MCP URL or change the deployment's private-network policy through security
review", with no indication that a supported opt-out exists.

env.ts has provided DEN_ALLOW_PRIVATE_MCP_URLS=1 all along, but it appeared
only in source, unit tests, and one eval flow: not in .env.example, not in the
chart, not in any doc. Surface it in all three places so the fix is reachable
without reading the source.

Scoped to den-api only. den-api, den-web, and inference all envFrom the same
ConfigMap, so adding the key there would inject an SSRF-bypass flag into two
workloads that never read it; the value is emitted directly on the den-api
container instead. Only den-api consumes allowPrivateMcpUrls (mcp-connections,
plugin-system/store, enterprise-mcp-client-adapter, external-mcp-client).

Nothing renders unless an operator opts in, so helm template with default
values is byte-identical to the previous chart: no ConfigMap checksum change
and no pod restart on upgrade. Blank, 0, and false render nothing; only "1"
enables it, matching the trimmed string comparison in env.ts; any other value
fails the render rather than guessing. url-guard.ts and env.ts are untouched.

* docs(enterprise): name the current GitHub release-asset host

Verified against a live OpenWork release asset: github.com now redirects
release downloads to release-assets.githubusercontent.com, not
objects.githubusercontent.com. An allowlist naming only the old host would
still let downloads start and then fail partway through, which is the exact
symptom this page exists to prevent.

List both. release-assets.githubusercontent.com is what GitHub serves today;
objects.githubusercontent.com stays documented for older clients and any
staged rollback.

* docs(self-host): document the private-network (semi air-gapped) deployment

Most enterprise installs are not fully air-gapped. The common shape is laptops
with internet plus VPN, and Den web and Den API inside the customer's private
network. That split was undocumented, so every customer rediscovered its
gotchas by hitting failures.

Add a published page covering the two network paths separately, because
different teams approve them: what the desktop must reach, what Den must reach
(only MySQL and ghcr.io at pull time are hard requirements), and the three
things that surprise operators, each with symptom, cause, and fix:

- Internal MCP servers are refused by the SSRF guard until
  DEN_ALLOW_PRIVATE_MCP_URLS=1, with the security tradeoff stated.
- The Settings > Debug cloud catalog probe is skipped because only hosted
  origins are trusted. It states explicitly that this affects the diagnostic
  only and Cloud MCP still works, so a skipped probe is not read as an outage,
  and that the variable must be set before launch because the in-app
  environment store strips OPENWORK_* keys.
- Den's outbound self-check targets a public host by default and can be
  self-hosted with DEN_DIAGNOSTICS_ORIGIN.

Also resolve a contradiction that decided whether IT opens one hostname or
two. docs/org-install-links.md claimed "the desktop must be able to reach both
origins"; resolveDenBaseUrls ignores an explicit API origin when a base URL is
present and derives <baseUrl>/api/den, proven by the existing den-mcp-url test.
Steady-state desktop traffic needs one origin. A separate API origin is only
reachable-critical for the one-time install-link exchange, which builds its
endpoint from the apiBaseUrl carried in the link, and for external MCP clients.
Both docs now state their context instead of disagreeing.

* docs(self-host): consolidate network, air-gap, and certificate guidance

Promote Self-host from a small Start here group to a dedicated docs tab while
preserving every existing start-here URL. Consolidate scattered network
material into five focused published pages: air-gapped deployment, outbound
network access, certificate trust and proxies, installer delivery, and network
diagnostics. Keep the private-network topology page as the high-level entry.

Make boundaries explicit instead of conflating them:

- mounted installers solve installer delivery, not full product isolation;
- desktop Chromium, spawned sidecars, Den containers, and MySQL use different
  trust surfaces;
- Linux Electron may require NSS/user trust even when /etc/ssl is correct;
- sslmode=require encrypts but is not certificate identity verification;
- a skipped Cloud catalog diagnostic does not mean Cloud MCP is broken.

Promote the machine-guarded outbound manifest into customer-facing docs,
retain repository operator pages and their existing anchors, and slim the
self-host/private-network overviews so there is one canonical owner per detail.
The Helm and diagnostics READMEs remain authoritative for low-level mechanics.

This branch is stacked on PRs #3131, #3132, #3133, and #3135. It changes docs
and navigation only; no product, Helm template, environment, skill, or eval
behavior changes.
@benjaminshafii
benjaminshafii deleted the docs/outbound-access-allowlist branch July 25, 2026 20:26
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