# How Cloudflare OS Implements a Capability-Based Security Framework

> Discover how Cloudflare OS implements object capabilities with Cap n Proto. Learn how possessing a stub equates to authority, removing the need for per-call permission checks.

- Repository: [Cloudflare/cloudflare-os](https://github.com/cloudflare/cloudflare-os)
- Tags: internals
- Published: 2026-09-04

---

**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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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:

```typescript
// 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`](https://github.com/cloudflare/cloudflare-os/blob/main/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 `Overseer` Durable Object in [`overseer.ts`](https://github.com/cloudflare/cloudflare-os/blob/main/overseer.ts) is the sole authority for minting capabilities based on `getEffectiveRole()` results.
- **Type-safe restrictions**: `UseOverseerInterface` and similar restricted interfaces enforce boundaries at compile time and throw `Unauthorized` at 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 in [`rateLimitedCapability.ts`](https://github.com/cloudflare/cloudflare-os/blob/main/rateLimitedCapability.ts).
- **Sub-capability delegation**: Fine-grained authority is achieved through stubs like `GatekeeperClient.ui` that 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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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`](https://github.com/cloudflare/cloudflare-os/blob/main/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.