How to Configure Capability-Based Security with workerd Bindings

Workderd implements a default-deny capability-based security model where Workers possess no privileges by default and gain access to external resources only through explicitly declared bindings in the Cap'n Proto configuration file.

Configuring capability-based security with workerd bindings enforces the principle of least privilege across your edge computing deployment. In the cloudflare/workerd repository, the runtime guarantees that Workers start with zero access to external systems. You grant specific capabilities by declaring bindings in the Cap'n Proto schema, enabling precise security controls without modifying application code.

Understanding the Default-Deny Security Model

The foundation of workerd's security architecture is explicit capability declaration. As documented in the schema comments at lines 23-30 of src/workerd/server/workerd.capnp, the runtime avoids giving Workers the ability to access external resources by name. Instead, the default stance is that a Worker has access to no privileged resources at all, and you must explicitly declare bindings to grant access.

This design allows operators to audit and restrict Worker privileges by editing configuration files rather than source code. Because the runtime creates JavaScript wrapper objects only for declared bindings, the Worker code cannot reach any resource it has not been explicitly granted.

The Binding Schema in workerd.capnp

The Binding struct defined at lines 35-84 of src/workerd/server/workerd.capnp enumerates every supported capability type. Each binding maps a binding name to a concrete resource or parameter.

Key fields in the Binding struct include:

  • name @0 :Text – The identifier used to access the binding in JavaScript (e.g., env.MY_KV).
  • optional @3 :Bool – Marks whether the binding can be omitted in derived Worker configurations.
  • parameter @4 :Bool – Indicates the binding must be supplied by a derived Worker, enabling composition while enforcing capability requirements.

These options support complex deployment scenarios where base Workers declare required capabilities that concrete implementations must provide.

Runtime Injection of Bindings

When a Worker starts, the runtime injects declared bindings into the JavaScript execution context. For ES modules, bindings appear as properties on the env object passed to the module's top-level export default function. In Service-Worker syntax, bindings become global variables.

The runtime creates thin JavaScript wrappers that forward calls to the underlying system. For example, a KV namespace binding becomes an instance of KVNamespace. Because these wrappers are instantiated only for explicitly declared bindings, the capability-based security model is enforced at the runtime level.

Practical Configuration Examples

KV Namespace Binding

The kvNamespace binding type (lines 94-98 of src/workerd/server/workerd.capnp) connects a Worker to a KV storage service.

using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "kv-service", kvNamespace = "MY_KV")
  ],

  sockets = [ (name = "http", address = "*:8080", http = (), service = "my-worker") ]
);

const myWorker :Workerd.Worker = (
  compatibilityDate = "2024-01-01",
  modules = [ (name = "index.js", esModule = embed "index.js") ],

  bindings = [
    (name = "MY_KV", kvNamespace = "kv-service")
  ],
);

// index.js
export default {
  async fetch(request, env) {
    await env.MY_KV.put("counter", "1");
    const value = await env.MY_KV.get("counter");
    return new Response(`Counter = ${value}`);
  },
};

Durable Object Class Binding

The durableObjectClass binding (lines 82-84 of src/workerd/server/workerd.capnp) exposes a Durable Object namespace to the Worker.

using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "chat-do", durableObjectClass = "ChatDO")
  ],

  sockets = [ (name = "http", address = "*:8080", http = (), service = "chat-worker") ]
);

const chatWorker :Workerd.Worker = (
  compatibilityDate = "2024-01-01",
  modules = [ (name = "index.js", esModule = embed "index.js") ],

  bindings = [
    (name = "CHAT", durableObjectClass = "chat-do")
  ],
);

// index.js
export default {
  async fetch(request, env) {
    const id = env.CHAT.idFromName("room1");
    const stub = env.CHAT.get(id);
    const response = await stub.fetch(request);
    return response;
  },
};

Wrapped Bindings for Capability Composition

The wrapped binding type (lines 104-106 of src/workerd/server/workerd.capnp) groups multiple capabilities into a single binding, enabling complex permission structures.

using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "inner-kv", kvNamespace = "INNER_KV"),
    (name = "inner-r2", r2Bucket = "INNER_R2")
  ],

  sockets = [ (name = "http", address = "*:8080", http = (), service = "wrapper-worker") ]
);

const wrapperWorker :Workerd.Worker = (
  compatibilityDate = "2024-01-01",
  modules = [ (name = "index.js", esModule = embed "index.js") ],

  bindings = [
    (name = "store",
      wrapped = (
        innerBindings = [
          (name = "kv", kvNamespace = "inner-kv"),
          (name = "bucket", r2Bucket = "inner-r2")
        ]
      )
    )
  ],
);

// index.js
export default {
  async fetch(request, env) {
    await env.store.kv.put("msg", "hello");
    const obj = await env.store.bucket.put("msg.txt", "hello world");
    return new Response("Stored");
  },
};

Summary

  • Default-deny posture: Workers begin execution with zero privileges, requiring explicit capability grants via bindings.
  • Schema-driven security: The Binding struct in src/workerd/server/workerd.capnp defines all available capability types, from KV namespaces to Durable Objects.
  • Runtime enforcement: Bindings appear as the env object in ES modules or global variables in Service Workers, with the runtime creating wrappers only for declared capabilities.
  • Compositional design: Optional bindings, parameters, and wrapped bindings enable complex inheritance patterns while maintaining strict capability boundaries.
  • Configuration-only changes: Security policies can be audited and modified by editing Cap'n Proto config files without touching application source code.

Frequently Asked Questions

What is the default security stance of a workerd Worker?

Workderd adopts a strict default-deny approach. According to the schema comments in src/workerd/server/workerd.capnp (lines 23-30), a Worker starts with access to no privileged resources at all. You must explicitly declare bindings in the configuration file to grant access to external services like KV namespaces, Durable Objects, or R2 buckets.

How do bindings appear in JavaScript code?

Bindings are injected into the JavaScript runtime at startup. For ES modules, they appear as properties on the env object passed to the export default function. In Service-Worker syntax, bindings become global variables. The runtime creates thin wrapper objects (such as KVNamespace instances) only for the specific capabilities declared in the configuration, enforcing the capability-based security boundary.

Can I make bindings optional or require them in derived Workers?

Yes. The Binding struct in src/workerd/server/workerd.capnp supports both optional and parameter fields. Setting optional = true allows a binding to be omitted in specific configurations. Setting parameter = true marks the binding as a required capability that must be supplied by any derived Worker, enabling secure composition and inheritance patterns while ensuring all concrete implementations provide necessary resources.

What is a wrapped binding and when should I use it?

A wrapped binding, defined at lines 104-106 of src/workerd/server/workerd.capnp, groups multiple inner bindings into a single named capability. This allows you to bundle related resources—such as a KV namespace and an R2 bucket—into one logical unit that can be passed to a Worker as a single parameter. Use wrapped bindings when you need to compose complex capability sets or when building reusable Worker libraries that require multiple coordinated resources.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →