Skip to content

[bug] host-cloudflare: Access service tokens can never be admins, so machine identities permanently lose all workspace writes #1959

Description

@BobzTH

Executor version: v1.6.8
Surface: self-hosted on Cloudflare Workers (apps/host-cloudflare), behind Cloudflare Access
Related: #1958 (the console-side half of this)

What happened

v1.6.8 (#1919) introduced the organization role model. On host-cloudflare, roles are derived from the Cloudflare Access JWT:

const email = typeof claims.email === "string" ? claims.email : "";
const commonName = typeof claims.common_name === "string" ? claims.common_name : "";
const isAdmin = email.length > 0 && config.adminEmails.includes(email.toLowerCase());
orgRole: isAdmin ? "admin" : "member",

A Cloudflare Access service token authenticates with common_name and carries no email claim — this is how Access works and cannot be configured otherwise. isAdmin is therefore unreachable for any machine identity. The existing unit test states it as intended behaviour:

// a token is a member, not an admin
expect(p.roles).toEqual(["member"]);

The consequence on an Access-gated deployment is that every non-browser identity permanently loses all workspace writes. In our case that is four machines that reach the gateway with the documented service-token headers. They can still read and execute tools, but from inside execute they can no longer:

  • executor.mcp.addServer (register an MCP server)
  • executor.coreTools.oauth.clients.registerDynamic
  • executor.coreTools.oauth.start for an owner: "org" connection
  • executor.coreTools.connections.refresh / remove on a workspace connection

All of these now return OrgWriteDeniedError. Since owner: "org" is the only ownership that machine identities can see at all (a user-owned connection belongs to the browser principal and is invisible to a service token), the practical effect is that automated setup and repair of the gateway is no longer possible from any machine — every one of those operations now requires a human in a browser.

What I expected

An Access-gated single-tenant deployment should be able to designate trusted machine identities as administrators, in the same way ADMIN_EMAILS designates trusted humans.

Suggested fix

An ADMIN_COMMON_NAMES environment variable alongside ADMIN_EMAILS, checked when the JWT carries common_name instead of email:

const isAdmin =
  (email.length > 0 && config.adminEmails.includes(email.toLowerCase())) ||
  (commonName.length > 0 && config.adminCommonNames.includes(commonName.toLowerCase()));

This keeps the default closed — a service token is still a member unless explicitly listed — while letting an operator opt a known automation identity in. It mirrors the existing ADMIN_EMAILS shape, needs no schema change, and does not affect cloud or self-host.

Steps to reproduce

  1. Deploy apps/host-cloudflare at v1.6.8 behind Cloudflare Access with a service-token policy.
  2. Call the gateway with CF-Access-Client-Id / CF-Access-Client-Secret.
  3. GET /api/account/me"email": "", identity is the token's common_name.
  4. From inside execute, call any workspace write, e.g. executor.coreTools.connections.refresh({ owner: "org", ... })org_write_denied.

Note

We hit this while trying to repair an expired workspace OAuth connection. Combined with #1958 — where the console hides Reconnect/Remove from admins because me never returns orgRole — there was no path to repair the connection at all: not from the machines (member), and not from the browser (buttons hidden). We had to roll the Worker back to the pre-1.6.8 version to recover.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions