Skip to content

[miniflare] Authenticate remote bindings with Cloudflare Access service tokens#14198

Open
krys-cf wants to merge 2 commits into
cloudflare:mainfrom
krys-cf:fix/remote-bindings-access-service-token
Open

[miniflare] Authenticate remote bindings with Cloudflare Access service tokens#14198
krys-cf wants to merge 2 commits into
cloudflare:mainfrom
krys-cf:fix/remote-bindings-access-service-token

Conversation

@krys-cf
Copy link
Copy Markdown

@krys-cf krys-cf commented Jun 5, 2026

What

When wrangler dev (or the Vite plugin) uses remote bindings against a Worker whose *.workers.dev domain is protected by Cloudflare Access, requests from the local remote-bindings proxy client to the remote proxy server are rejected with a 401/403. This breaks every remote binding behind Access — Workers AI, AI Gateway, Vectorize, Images, Artifacts, etc.

This builds on #14008 / #14011 (which fixed Access service-token auth for the realish-preview HTTP path and added a block warning) by also authenticating the binding proxy traffic itself. There are two distinct code paths and both needed fixing:

Path Used by Fix
HTTP makeFetch wrapped-fetcher bindings: AI, Vectorize, Images Attach CF-Access-Client-Id / CF-Access-Client-Secret headers to the request to the proxy server
capnweb WebSocket makeRemoteProxyStub RPC bindings: Artifacts, service bindings Establish the WebSocket via a fetch() upgrade (Upgrade: websocket) so the Access headers ride the handshake — new WebSocket(url) cannot set request headers in the Workers runtime

Credentials are read from CLOUDFLARE_ACCESS_CLIENT_ID / CLOUDFLARE_ACCESS_CLIENT_SECRET — the same Service Token env vars that getAccessHeaders() already uses for the realish-preview HTTP path (packages/wrangler/src/user/access.ts), so this is consistent with existing wrangler conventions. They're forwarded to the proxy client worker as text bindings (accessClientId / accessClientSecret).

When the env vars are unset, behaviour is unchanged.

Why the WebSocket change

capnweb's newWebSocketRpcSession accepts either a URL string (which it upgrades with new WebSocket(url) — no header support) or a pre-connected WebSocket. When Access credentials are present we do the upgrade ourselves via fetch(httpUrl, { headers: { Upgrade: "websocket", ...accessHeaders } }) and hand the resulting response.webSocket to capnweb. RPC methods are async over the network, so awaiting the authenticated upgrade is transparent to callers (e.g. await env.ARTIFACTS.<method>()).

Testing

Verified end-to-end against a real internal app (D1/Vectorize/AI/Artifacts, all remote: true) on a workers.dev account protected by an Access policy:

  • Workers AI + AI Gateway (course generation) — was failing with InferenceUpstreamError: invalid_token, now succeeds

  • Vectorize query + AI embeddings (vector search) — returns ranked results

  • Artifacts git read/write over capnweb RPC — commits succeed and content reads back

  • ✅ Confirmed by elimination: patch + creds → works; creds only → invalid_token; patch only → invalid_token

  • pnpm --filter miniflare build passes with 0 type errors

  • Tests included/updated — not added (see note below)

  • Public documentation — the existing Service Token docs (added in [Workers] Document Access Service Auth setup for remote bindings cloudflare-docs#31021) already cover CLOUDFLARE_ACCESS_CLIENT_ID / CLOUDFLARE_ACCESS_CLIENT_SECRET; this extends the same mechanism to all remote bindings.

Note: I'm happy to add automated coverage if a maintainer can point me at the right harness — the existing remote-bindings paths are largely exercised in staging/manual since they require a real Access-protected workers.dev origin. Verified manually as above.


Open in Devin Review

…ce tokens

When the workers.dev domain is behind Cloudflare Access, remote bindings (AI, Vectorize, Images, Artifacts, etc.) failed with 401/403 because the proxy client never sent Access credentials.

Attach CF-Access-Client-Id / CF-Access-Client-Secret (from CLOUDFLARE_ACCESS_CLIENT_ID / CLOUDFLARE_ACCESS_CLIENT_SECRET env vars) to both:
- the HTTP makeFetch path (wrapped-fetcher bindings: AI, Vectorize, Images)
- the capnweb WebSocket path (RPC bindings: Artifacts) via a fetch()-based Upgrade so headers ride the handshake (new WebSocket(url) cannot set headers)

Consistent with getAccessHeaders() which already reads these env vars for the realish-preview HTTP path. No behavior change when the env vars are unset.
@krys-cf krys-cf requested a review from workers-devprod as a code owner June 5, 2026 14:10
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 5, 2026

🦋 Changeset detected

Latest commit: 4e7793c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
miniflare Patch
@cloudflare/pages-shared Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
wrangler Patch
@cloudflare/wrangler-bundler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@workers-devprod workers-devprod requested review from a team and james-elicx and removed request for a team June 5, 2026 14:10
@workers-devprod
Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/remote-bindings-access-service-token.md: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/shared/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace-proxy.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/shared/remote-bindings-utils.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

- Return undefined for 'then' while the capnweb WebSocket is connecting so the proxy is not treated as a thenable (an await would otherwise dispatch a bogus remote 'then' RPC).
- Attach a no-op .catch() to stubPromise so a failed WS upgrade isn't an unhandled rejection when only the .fetch() path is used; the error still surfaces on RPC access via the awaited stubPromise.
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