How Cloudflare OS Implements a Capability-Based Security Framework
Cloudflare OS (the "Workshop") enforces security through object capabilities using Cap'n Web RPC, where possessing a stub equals possessing authority—eliminating the need for per-call permission checks.
This architecture, implemented in the cloudflare/cloudflare-os repository, moves access control from ambient permission checks to explicit capability objects that encode exactly which operations a caller may invoke. The system guarantees "no capability = no authority" at both compile time and runtime through immutable, type-safe RPC stubs.
Core Architecture of the Capability Model
Cap'n Web RPC as the Foundation
The framework builds upon Cap'n Web RPC, which transforms ordinary objects into stubs (client-side proxies) and targets (server-side implementations). Unlike traditional ACL-based systems where every endpoint checks permissions independently, these stubs carry their authority intrinsically. When a client holds a stub, it holds the capability to invoke only the methods that stub exposes. The protocol pipelines these stubs across RPC boundaries so the remote side receives a reference rather than a concrete object, preserving the capability boundary across network calls.
The Overseer as Central Authority
At the heart of the system lies the Overseer, a Durable Object defined in packages/workshop-backend/src/overseer.ts that acts as the sole issuer of capabilities. It mints stubs for workspaces, gadgets, gatekeepers, and admin APIs based on the caller's resolved role. When a session initializes, the Overseer.open() method examines the user's permission graph via getEffectiveRole() and returns either the full OverseerClientInterface (for owners) or a restricted variant (for collaborators). As documented in docs/sharing.md lines 149-156, once minted, these capabilities remain immutable for the session duration—even if the user's role changes later.
How Capabilities Are Issued and Propagated
Authentication to Authorization Flow
The security flow begins when a user authenticates through the sign-in flow defined in auth/login-flow.ts. The resulting session stub carries the user's identity to the Overseer, which performs the authorization decision at the boundary. This centralizes trust decisions in one location rather than scattering permission checks across every method. The capability handed back—whether full or restricted—determines the entire permission set available to that connection.
Minting Capability Objects
When the Overseer creates a workspace, it mints a workspace capability (a Durable Object stub) that serves as the authority token for that resource. Similarly, gatekeeper accounts are minted in packages/workshop-backend/src/user.ts via User.newGatekeeper(), where a resource URL becomes a capability encapsulated inside the user's Durable Object. This account capability is never exposed directly; instead, it remains stored within the DO and is only accessible through specific stubs like GatekeeperClient defined in packages/workshop-shared/src/api.ts.
Sub-capabilities and Delegation
The system supports sub-capabilities—restricted views created by an owning capability and handed to specific callers. For example, GatekeeperClient.ui and GadgetClient.getUiBundle expose only a safe subset of the full API. This implements the principle that callers only know the methods explicitly granted to them. A notable pattern is the "restore-forger" mechanism implemented in overseer.ts lines 94-102, where a transient stub passed to CODE_MODE_HARNESS allows executed code to env.<binding>[restore](…) and create persistent stubs without gaining broader authority.
Runtime Enforcement Mechanisms
Restricted Interfaces and Method-Level Guards
Restricted capabilities are thin wrappers that implement the full interface but throw Unauthorized for any method not on the allow-list. As detailed in docs/sharing.md lines 28-33, a "use" session receives UseOverseerInterface, which exposes only read and execute methods while hiding administrative functions. This defense-in-depth approach ensures that even if new methods are added to the underlying interface, existing restricted sessions cannot invoke them accidentally.
Session Immutability
Capabilities are immutable for the lifetime of a session. If an administrator revokes a user's access or upgrades their role, existing sessions continue with their original capability set. Only new sessions established after the permission change receive the updated capability. This prevents privilege escalation mid-session and eliminates complex revocation tracking logic, as documented in the sharing documentation lines 149-156.
Resource Disposal and Lifecycle Management
To prevent resource leaks on the server side, RPC stubs implement [Symbol.dispose](). The system explicitly disposes capabilities when sessions end, particularly visible in packages/workshop-frontend/src/rateLimitedCapability.ts where wrapped capabilities add rate-limiting while preserving the original authority. The AgentSpawnerBinding and related components ensure that server-side resources are released when client-side stubs are garbage collected or explicitly closed.
Code Example: Opening a Session
The following TypeScript demonstrates how front-end code requests a capability and the runtime enforcement that follows:
// Front-end code (React) – request a session capability from the backend
const overseer = await fetchRpcStub<OverseerClientInterface>('OVERSEER');
const session = await overseer.open(); // Returns either full or restricted capability
// The session object is a capability; calling any method enforces the granted rights
await session.getWorkspaceSummary(); // Allowed for both build and use
await session.updateWorkspaceConfig(config) // Throws Unauthorized for a "use" session
The open() call in packages/workshop-backend/src/overseer.ts resolves the user's effective role and returns the appropriate capability object, ensuring that subsequent calls require no additional permission checks.
Summary
- Centralized issuance: The
OverseerDurable Object inoverseer.tsis the sole authority for minting capabilities based ongetEffectiveRole()results. - Type-safe restrictions:
UseOverseerInterfaceand similar restricted interfaces enforce boundaries at compile time and throwUnauthorizedat runtime for disallowed methods. - Immutable sessions: Capabilities do not change during a session; role changes only affect new sessions, preventing mid-session privilege escalation.
- Explicit disposal: Stubs implement
[Symbol.dispose]()to prevent server-side resource leaks, with patterns demonstrated inrateLimitedCapability.ts. - Sub-capability delegation: Fine-grained authority is achieved through stubs like
GatekeeperClient.uithat expose only specific functionality to specific callers.
Frequently Asked Questions
What protocol enables the capability-based security in Cloudflare OS?
Cap'n Web RPC provides the underlying transport that converts objects into capability stubs. This protocol allows stubs to be passed across RPC calls as references rather than copied values, maintaining the capability boundary across the client-server divide according to the cloudflare/cloudflare-os source code.
How does Cloudflare OS prevent privilege escalation during an active session?
The system enforces session immutability: once the Overseer.open() method mints a capability for a connection, that capability never changes. If a user's role is upgraded or revoked in the permission graph, existing sessions retain their original capability set, and only new connections receive the updated permissions, as documented in docs/sharing.md.
Where are gatekeeper account capabilities stored and how are they accessed?
Gatekeeper capabilities are minted in packages/workshop-backend/src/user.ts via the User.newGatekeeper() method and stored inside the user's Durable Object. The capability is encapsulated within the DO and never exposed directly to clients; instead, it is accessed through the GatekeeperClient interface defined in packages/workshop-shared/src/api.ts.
What prevents restricted capabilities from accessing new methods added to an interface?
Restricted capabilities use interface wrapping that explicitly throws Unauthorized for any method not on the pre-defined allow-list. This means even if the underlying Overseer interface gains new administrative methods, existing "use" sessions holding UseOverseerInterface cannot invoke them because their restricted wrapper lacks those method implementations, providing defense-in-depth against future interface expansion.
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 →