Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/workerd/llms.txt
Use this file to discover all available pages before exploring further.
workerd implements a capability-based security model where workers can only access resources explicitly granted to them through bindings. This approach provides strong security guarantees and makes applications more composable.
What are capabilities?
A capability is an unforgeable reference to a resource that grants the holder permission to access that resource. In workerd, capabilities are expressed as bindings in the worker configuration.
Traditional access control uses:
- Global namespaces: Access resources by name (URLs, file paths, database names)
- Ambient authority: Programs have implicit access to many resources
- Separate authorization: Permission checks happen at access time
Capability-based security uses:
- Explicit bindings: Resources must be explicitly granted
- Least privilege: Workers start with zero access
- Unforgeable references: Cannot fabricate access to resources
How capabilities work in workerd
Configuration defines capabilities
Capabilities are granted in the worker configuration:
const myWorker :Workerd.Worker = (
serviceWorkerScript = embed "worker.js",
compatibilityDate = "2024-01-01",
# These bindings are capabilities
bindings = [
(name = "DATABASE", service = "postgres"),
(name = "KV", kvNamespace = "my-kv"),
(name = "API_KEY", text = "secret")
],
# Global fetch capability
globalOutbound = "internet"
);
Workers receive capabilities as bindings
The worker receives capabilities through the env parameter:
export default {
async fetch(request, env, ctx) {
// env.DATABASE is a capability to access the database service
// env.KV is a capability to access the KV namespace
// env.API_KEY is a capability (just a string in this case)
const response = await env.DATABASE.fetch(request);
return response;
}
}
The worker cannot access any resource not present in env (except through fetch() if globalOutbound is configured).
Preventing SSRF attacks
Server-Side Request Forgery (SSRF) is a common vulnerability where an attacker tricks a server into making requests to unintended destinations.
Traditional approach (vulnerable)
// VULNERABLE: Worker can fetch ANY URL
export default {
async fetch(request) {
const url = new URL(request.url).searchParams.get('url');
// Attacker can pass internal URLs:
// ?url=http://internal.company.com/admin
// ?url=http://169.254.169.254/metadata (cloud metadata)
const response = await fetch(url);
return response;
}
}
Capability-based approach (secure)
// SECURE: Worker can only fetch from bound services
export default {
async fetch(request, env) {
const service = new URL(request.url).searchParams.get('service');
// Only allowed services are accessible
if (service === 'api') {
return await env.API_SERVICE.fetch(request);
} else if (service === 'data') {
return await env.DATA_SERVICE.fetch(request);
}
return new Response('Service not found', { status: 404 });
}
}
Configuration:
bindings = [
(name = "API_SERVICE", service = "public-api"),
(name = "DATA_SERVICE", service = "data-backend")
]
# No globalOutbound = worker cannot fetch arbitrary URLs
Controlled internet access
If a worker needs internet access, grant it explicitly with restrictions:
services = [
(name = "internet", network = (
allow = ["public"], # Only public IPs
deny = ["10.0.0.0/8", "192.168.0.0/16"], # Block private ranges
tlsOptions = (trustBrowserCas = true)
))
]
const myWorker :Workerd.Worker = (
serviceWorkerScript = embed "worker.js",
compatibilityDate = "2024-01-01",
globalOutbound = "internet" # Controlled access
);
The network service configuration defines exactly which addresses are reachable:
allow = ["public"]: Only publicly-routable IP addresses
deny = [...]: Explicitly block specific ranges
- Private networks are blocked by default
Service isolation
Capabilities enable strong isolation between services.
Example: Multi-tenant architecture
services = [
(name = "customer-a", worker = .customerWorker),
(name = "customer-b", worker = .customerWorker),
(name = "database-a", external = (
address = "db-a.internal:5432",
http = ()
)),
(name = "database-b", external = (
address = "db-b.internal:5432",
http = ()
))
]
const customerWorker :Workerd.Worker = (
serviceWorkerScript = embed "customer.js",
compatibilityDate = "2024-01-01",
bindings = [(name = "DB", parameter = (type = (service = void)))]
);
const customerA :Workerd.Worker = (
inherit = "customerWorker",
bindings = [(name = "DB", service = "database-a")]
);
const customerB :Workerd.Worker = (
inherit = "customerWorker",
bindings = [(name = "DB", service = "database-b")]
);
The same worker code runs for both customers, but each has access only to their own database. There’s no way for customer A’s worker to access customer B’s database, even if the code is compromised.
Composability through capabilities
Capabilities make workers highly composable.
Parameter bindings
Define workers that require specific capabilities without specifying what they are:
const authWorker :Workerd.Worker = (
serviceWorkerScript = embed "auth.js",
compatibilityDate = "2024-01-01",
bindings = [
(name = "USER_DB", parameter = (type = (service = void))),
(name = "SESSION_STORE", parameter = (type = (kvNamespace = void)))
]
);
Different deployments can fulfill these parameters differently:
# Development environment
const authDev :Workerd.Worker = (
inherit = "authWorker",
bindings = [
(name = "USER_DB", service = "dev-user-db"),
(name = "SESSION_STORE", kvNamespace = "dev-sessions")
]
);
# Production environment
const authProd :Workerd.Worker = (
inherit = "authWorker",
bindings = [
(name = "USER_DB", service = "prod-user-db"),
(name = "SESSION_STORE", kvNamespace = "prod-sessions")
]
);
The same code works in both environments with different backing resources.
Testing with mock services
Easily inject mock services for testing:
const mockDatabase :Workerd.Worker = (
serviceWorkerScript = embed "mock-db.js",
compatibilityDate = "2024-01-01"
);
const myWorkerTest :Workerd.Worker = (
inherit = "myWorker",
bindings = [
(name = "DATABASE", service = "mock-database")
]
);
The test configuration replaces the real database with a mock, without changing the worker code.
Capability types
workerd supports various types of capabilities:
Service capabilities
Access to another worker or service:
(name = "BACKEND", service = "backend-service")
Storage capabilities
Access to persistent storage:
(name = "KV", kvNamespace = "namespace-id")
(name = "R2", r2Bucket = "bucket-service")
(name = "DO", durableObjectNamespace = "MyClass")
Network capabilities
Access to network resources:
globalOutbound = "internet"
Data capabilities
Configuration and secrets:
(name = "API_KEY", text = "secret")
(name = "CONFIG", json = "{\"key\": \"value\"}")
(name = "CERT", data = embed "cert.pem")
Cryptographic capabilities
Cryptographic keys:
(name = "SIGNING_KEY", cryptoKey = (
raw = embed "key.bin",
algorithm = (name = "HMAC"),
extractable = false,
usages = ["sign", "verify"]
))
With extractable = false, the worker can use the key but cannot extract the raw key material - the key never leaves the runtime in plaintext.
Security properties
The capability model provides strong security properties:
Least privilege by default
Workers start with zero access. They can only access resources explicitly granted:
export default {
async fetch(request, env) {
// This works: env.ALLOWED_SERVICE is bound
await env.ALLOWED_SERVICE.fetch(request);
// This fails: env.OTHER_SERVICE is not bound
await env.OTHER_SERVICE.fetch(request); // undefined
}
}
Unforgeable references
Workers cannot create or guess valid capability references. They must receive them through bindings.
Audit trail
Configuration explicitly documents all resource access:
# Looking at this config, you can see exactly what the worker can access:
bindings = [
(name = "USER_DB", service = "user-database"),
(name = "CACHE", kvNamespace = "user-cache")
]
globalOutbound = "internet"
Delegation is explicit
For one service to give another service access to a resource, it must explicitly pass the capability:
// worker-a.js
export default {
async fetch(request, env) {
// Explicitly passing the DB capability to worker B
return await env.WORKER_B.fetch(request, {
headers: { 'X-DB-Binding': JSON.stringify(env.DB) }
});
}
}
However, most commonly you’d configure delegation in the config file by binding the same resource to both workers.
Best practices
Grant minimal capabilities
Only grant the capabilities a worker actually needs:
# Bad: Worker can access everything
bindings = [
(name = "ALL_SERVICES", service = "service-registry"),
(name = "ROOT_DB", service = "root-database")
]
# Good: Worker can access only what it needs
bindings = [
(name = "USER_API", service = "user-service"),
(name = "USER_CACHE", kvNamespace = "users")
]
Use parameter bindings for reusable workers
Make workers generic by using parameter bindings:
const cacheWorker :Workerd.Worker = (
serviceWorkerScript = embed "cache.js",
compatibilityDate = "2024-01-01",
bindings = [
(name = "UPSTREAM", parameter = (type = (service = void))),
(name = "CACHE", parameter = (type = (kvNamespace = void)))
]
);
Document capability requirements
Document what capabilities your worker expects:
/**
* Image processing worker
*
* Required bindings:
* - IMAGE_STORAGE: R2 bucket for storing processed images
* - CACHE: KV namespace for caching thumbnails
* - METRICS: Service for reporting processing metrics
*/
export default {
async fetch(request, env) {
// ...
}
}
Test with restricted capabilities
Write tests that verify your worker handles missing capabilities gracefully:
export default {
async fetch(request, env) {
if (!env.OPTIONAL_FEATURE) {
return new Response('Feature not available', { status: 503 });
}
return await env.OPTIONAL_FEATURE.fetch(request);
}
}