How Cloudflare OS Implements Capability-Based Security with Gatekeepers
Cloudflare OS isolates external services behind lightweight Gatekeeper Workers that expose only typed RPC stubs to applications, enforcing least-privilege boundaries through URL-granular permissions and Durable Object-backed accounts.
Cloudflare OS (codenamed "Workshop") implements a rigorous capability-based security model using Gatekeepers to isolate external integrations. In the cloudflare/cloudflare-os repository, each gatekeeper encapsulates sensitive credentials within Durable Objects and exposes only minimal, typed capabilities to agent workspaces. This architecture ensures that applications receive exactly the access they need—nothing more—through a combination of provisioned accounts, strict resource URL validation, and ambient capability injection.
Gatekeeper Architecture: Three Layers of Enforcement
Cloudflare OS enforces capability boundaries at three distinct architectural layers. Each layer acts as a checkpoint to prevent unauthorized access to external service credentials.
The Binding Layer
Service bindings determine which capabilities are ambient to a workspace. In packages/workshop-backend/src/overseer.ts, the system creates binding objects through GatekeeperLoopback and GatekeeperHookLoopback classes. These bindings register the gatekeeper's suggestedBindingName as a named chat binding during the prepareChatBindings startup sequence.
The Account Layer
A gatekeeper-provided GatekeeperUser instance owns the actual capability (such as an OAuth token or read-only API key). Each gatekeeper implements Vendor and User classes—for example, GatekeeperVendor and GatekeeperUserImpl in packages/gatekeeper-context/src/library-gatekeeper.ts. The account's sensitive state persists in a Durable Object, ensuring secrets never leave the gatekeeper's isolation boundary.
The Resource-URL Granularity Layer
The getGatekeeperClassFor(url) method maps URLs to concrete capability classes, ensuring agents can only reach declared resources. Central logic lives in packages/workshop-shared/src/gatekeeper.ts (lines 590-620), consumed by the backend at packages/workshop-backend/src/user.ts (line 1666) to resolve resource URLs against the gatekeeper's whitelist.
The Capability Flow: From Provisioning to Enforcement
Cloudflare OS manages capabilities through a four-stage lifecycle that keeps credentials encapsulated while making functionality available to agents.
1. Account Provisioning
When a gatekeeper is auto-provisioned, its GatekeeperVendor.createAccount() method instantiates a new GatekeeperUser without requiring user identity. The capability (such as an OAuth token) stores within a Durable Object (GatekeeperUserImpl). This pattern appears in packages/gatekeeper-context/src/library-gatekeeper.ts:
export class GatekeeperVendor extends WorkerEntrypoint<Cloudflare.Env, GatekeeperVendorProps> {
// auto-provisions an account that provides a read-only session
async createAccount() {
// Creates GatekeeperUserImpl instance with embedded credentials
}
}
2. Binding Registration
The workshop's AdminConfig enumerates enabled gatekeepers. During startup, prepareChatBindings in packages/workshop-backend/src/overseer.ts registers each gatekeeper's binding name as a named chat binding, making the capability ambient to eligible workspaces.
3. Capability Injection
When an agent executes code via executeCode, the system calls getSession or getAgentCatalog RPC methods on the gatekeeper's GatekeeperUser. These return a stub that the agent embeds in its workspace. Because the stub represents a capability rather than a credential, it can traverse RPC boundaries without exposing raw secrets.
// The singleton stub exposed to agents
export class GatekeeperUserImpl extends WorkerEntrypoint<Cloudflare.Env, GatekeeperUserImplProps> {
// Returns a read-only session for the agent
async getSession() {
// Returns capability stub, not raw token
}
}
4. Runtime Enforcement
All external service calls must route through the gatekeeper's RPC interface. The user.ts:getGatekeeperClassFor method validates URLs against the gatekeeper's whitelist, throwing if the resource is not explicitly allowed.
Security Boundaries and Provisioning Policies
Cloudflare OS implements multiple mechanisms to prevent accidental capability escalation.
Ambient vs. Explicit Capabilities
Only gatekeepers marked as auto-provisioned become ambient capabilities automatically. All others require explicit addition through the admin UI. This distinction prevents privileged capabilities from inadvertently attaching to workspaces.
Three-State Provisioning Mode
The provisioning-policy.ts module defines three states: enabled, optional, and disabled.
- Enabled: The gatekeeper auto-creates an account for every user, guaranteeing capability availability while hiding it from the UI
- Optional: Users must opt-in via the Connectors UI
- Disabled: No capability is created or available
Resource-URL Grammar
The resources.ts module defines a strict grammar for capability URLs. When the backend calls getGatekeeperClassFor, it parses the URL and returns a typed class. For example, a private collection returns a capability class restricted to the owning account, while a public collection returns a shared capability class:
// packages/workshop-backend/src/user.ts
const { class: cls, resource } = await account.account.getGatekeeperClassFor(url);
Backend Enforcement with getGatekeeperClassFor
The core enforcement mechanism resides in packages/workshop-shared/src/gatekeeper.ts. This utility classifies URLs and returns the appropriate capability constructor. The implementation (around lines 590-620) applies the resource grammar rules to ensure that a capability for context://public/docs cannot access context://private/secrets.
When a workspace requests the Context capability, the resolution flow works as follows:
// Front-end: request the Context capability
const ctx = await rpc.getGatekeeperClassFor('context://my-collection');
const session = await ctx.getSession(); // Returns a read-only stub
const docs = await session.listDocuments(); // RPC call uses the capability internally
The backend validates the gatekeeper's provisioning policy before returning the capability:
// Backend: enforce capability when a user attempts to add a new gatekeeper
import { AdminConfig } from 'packages/workshop-backend/src/admin-config';
if (gatekeeper.autoProvisionsAccount && AdminConfig.provisioningPolicy === 'disabled') {
throw new Error('Auto-provisioned gatekeepers cannot be disabled');
}
Summary
- Capability Isolation: Each external service runs in its own Gatekeeper Worker with Durable Object-backed accounts, ensuring secrets never leak to agent workspaces.
- Typed RPC Stubs: Gatekeepers expose only
getSessionorgetAgentCatalogRPC methods that return capability stubs, not raw credentials. - Deterministic Binding: The
suggestedBindingNamecreates a predictable naming scheme for capability injection viaGatekeeperLoopbackinoverseer.ts. - URL-Granular Enforcement: The
getGatekeeperClassFormethod ingatekeeper.ts(lines 590-620) anduser.ts(line 1666) validates resource URLs against strict grammars to prevent capability misuse. - Provisioning Controls: Three-state provisioning policies (
enabled|optional|disabled) allow administrators to control whether capabilities are ambient, opt-in, or unavailable.
Frequently Asked Questions
What is a Gatekeeper in Cloudflare OS?
A Gatekeeper is a lightweight Cloudflare Worker that encapsulates an external service integration. It owns a Durable Object account (GatekeeperUserImpl) that stores credentials such as OAuth tokens or API keys. Rather than exposing these secrets directly, the Gatekeeper exposes typed RPC methods like getSession() that return capability stubs, allowing agents to interact with external services without ever handling raw authentication material.
How does Cloudflare OS prevent capability leakage between workspaces?
The system enforces isolation at the binding and account layers. In packages/workshop-backend/src/overseer.ts, the GatekeeperLoopback class creates service bindings that are only injected into workspaces where the administrator has explicitly enabled them. Additionally, the getGatekeeperClassFor method in packages/workshop-backend/src/user.ts validates that the requesting workspace's account owns the requested resource URL, throwing an error if a workspace attempts to access another's private capabilities.
What is the difference between auto-provisioned and optional gatekeepers?
According to packages/workshop-shared/src/provisioning-policy.ts, auto-provisioned gatekeepers automatically create a GatekeeperUser account for every user when set to enabled mode, making the capability ambient and hidden from the UI. Optional gatekeepers require users to explicitly connect the service through the Connectors UI. Disabled gatekeepers cannot be instantiated or used by any workspace, providing a hard kill-switch for vulnerable integrations.
How does resource URL granularity enforce security boundaries?
The resources.ts module and getGatekeeperClassFor implementation parse resource URLs to determine capability scope. For example, a URL like context://private/user-123/data returns a capability class restricted to that specific user, while context://public/shared returns a shared capability class. This grammar ensures that a capability granted for public resources cannot be reused to access private resources, even if both are served by the same Gatekeeper backend.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →