This document describes WDL's current security boundaries. It is not a formal threat model for every deployment, but it records the assumptions the code and infrastructure rely on today. Current code, tests, and deployed network policy remain authoritative.
- Tenant worker code is untrusted.
- Platform services inside the private service mesh are trusted platform components.
Internal endpoints require the shared
WDL_INTERNAL_AUTH_TOKENapplication header in addition to socket, network, service, and task-placement boundaries. Health and metrics endpoints remain unauthenticated so orchestrators and scrapers can probe service liveness. - Public ingress and admin ingress are different planes. Gateway routes public tenant traffic; control/auth authorize control-plane operations.
- Internal network safety is still a deployment contract. The shared internal token is defense in depth and caller-shape authentication for in-tree platform services; it is not a substitute for keeping private ports off public ingress.
- A runtime escape must not automatically become cloud credential theft. Tenant-running tasks must use least-privilege task roles and must not receive broad infrastructure credentials.
| Zone | Examples | Trust level | Main boundary |
|---|---|---|---|
| Public data plane | Gateway public socket, tenant Worker URLs | Untrusted clients | Gateway routing plus runtime worker isolation |
| Admin/control plane | Control URL/admin host, control worker, auth worker, CLI token API | Authenticated operators/tenants | x-admin-token, auth role table, namespace scope |
| Tenant runtime | user-runtime loaded workers | Untrusted tenant code | workerd isolate, wrapper-shaped env, public-only outbound |
| System runtime | system-runtime, control/auth/tail, __system__ workers |
Platform code | Reserved namespace and private+public outbound |
| Private service mesh | d1-runtime, do-runtime, workflows, scheduler, redis-proxy sidecars | Trusted platform components | Service Connect/security groups/internal sockets plus WDL_INTERNAL_AUTH_TOKEN |
| State stores | Valkey DBs, EFS localDisk, S3-compatible storage | Platform-owned data services | Writer ownership, Redis DB split, secret envelopes, storage credentials |
| Host/infra | ECS Fargate tasks, IAM roles, task metadata | Operator-controlled | IAM least privilege, ECS Exec policy |
Gateway is not an authorization layer for tenant applications. It resolves a public host/path to an immutable worker version and forwards the request to the runtime loader socket. Tenant applications must implement their own public application auth when they need it.
Admin-host requests are different. Gateway's ADMIN_HOST branch only forwards to
control; it does not authorize. Control parses the route into an action and asks auth
to verify x-admin-token against shared/auth-roles.js.
Reserved namespaces are exact literals, not a broad __* convention. The current set is
__system__, __platform__, and __community__. __system__ is the system-runtime
namespace and is the only reserved namespace with a narrow public system-route allowance
documented in the gateway module. __platform__ is the current platform-tier namespace
for platform bindings and platform-scoped roles. __community__ is reserved for future
community platform-tier use, but it is not currently a platform-tier role namespace.
Reserved namespaces are blocked in public subdomain routing before Redis route lookup.
Reserved namespaces are not tenant namespaces: tenant-scoped ns tokens must bind
ordinary tenant namespaces, while reserved namespace access requires ops or the
platform role shapes allowed by shared/auth-roles.js. Tenant namespaces use
DNS-label-compatible grammar: 1-63 lowercase alphanumeric/hyphen characters, with
alphanumeric first and last characters.
WDL uses stock workerd and does not patch it. Runtime isolation therefore starts with workerd isolate boundaries and continues with WDL-specific wrapper and network rules:
- Tenant bundles are loaded as immutable worker versions through
workerLoader. - Runtime wrapper generation constructs the tenant-visible
env. Generic authenticated backend Fetchers stay inside runtime and never enter loaded-worker env; module evaluation may observe only declaration-scoped host adapters whose immutable props limit them to the corresponding declared binding. The DO host adapter owns direct owner forwarding through runtime's internal network; the AI host adapter exposes only its binding-scopedfetch()surface before wrapper invocation. - user-runtime loaded workers receive public-only outbound. Tenant
fetch()andcloudflare:socketsmust not reach platform-private addresses. - system-runtime
__system__workers intentionally have private+public outbound because they are platform code, not tenant code. - Transferring a connected socket over JSRPC delegates that existing connection; it does not bypass outbound checks when opening new connections. Privileged workers must not hand private connections to untrusted code.
- Privileged runtime events use the private
:8088internal socket. Gateway must not reserve tenant-visible paths like/_scheduled; the socket boundary is the security boundary.
Tenant execution context is not a host authenticity boundary. This applies even when a property name, symbol, or storage key is unavailable to ordinary tenant code:
- Imported env, nested
withEnv(),AsyncLocalStorage, and captured async frames are tenant-controlled ambient state. Tenant code can replace an env and can restore a previously captured frame. None of these mechanisms may own authorization, fencing, deduplication, host-failure attribution, or another durable platform decision. - A private
Symbolprevents accidental naming collisions, but a tenantProxytrap can observe the property key and return a forged value.WeakMaporWeakSetidentity can prove that the wrapper previously saw the exact same object; it cannot prove which invocation originally produced or later relayed that object. - JSRPC wildcard methods are also tenant-realm property lookups: an inherited property
can suppress wildcard resolution. Platform wrappers must bind reviewed callables before
exposing the corresponding object to tenant code and must not use a raw RPC object as a
facade target whose descriptors or
dup()reveal the underlying capability. - Host-only context must stay in the host isolate, a binding with immutable host props, or an authoritative backend state machine. Hidden entrypoint props must be consumed before the tenant constructor runs, and persisted metadata that controls wrapper generation must fail closed before tenant execution.
- Error provenance is intentionally narrow. A generated facade may privately brand the
exact Error rejected by a direct host call together with that call's bound capability.
A reviewed
run()or callback Promise boundary may act only when the same object escapes and the recorded capability belongs to the current Workflow env. Catching the Error and returning a fallback prevents that boundary from reporting and permits the fallback to commit; detaching its Promise likewise removes it only from that boundary's settlement. Neither action erases the private Error-to-capability association. A replacement, wrapper,cause,AggregateError, or other object does not inherit that provenance. Rethrowing the exact Error may report under the same capability-membership check, including from another invocation using the same binding identity. It can only make that tenant retry its own Workflow and grants no additional capability. Making a caught Error affect a boundary it does not escape requires moving the decision into a tenant-inaccessible host execution realm or explicit host protocol, not adding ambient-context heuristics. - Dynamic Worker env and props may carry only capability forms supported by the pinned
workerd configuration. Compatibility flags are valid design inputs: platform-owned
static workers should actively consider a flag when it replaces WDL machinery or
gives a simpler native boundary. A flag that changes tenant Dynamic Workers needs a
stricter product, security, compatibility, and rollout review because it expands
tenant-visible behavior. The current reporter design does not enable
allow_irrevocable_stub_storage: using it here would require extending irrevocable stub persistence into the tenant worker, which is broader than the report-only capability. Use a host-ownedctx.exportsloopback/service stub for this boundary. A reporter transport rejection may carry the active report nonce back only in the generated boundary Error's standard message; the host requires an exact active-registry match before changing the failure latch. Verify workerd-owned serialization, constructor ordering, and reverse JSRPC in real workerd before treating any flag or capability as a boundary. Once that transport has a real-workerd gate and no stale capability crosses a product boundary, host-local registry concurrency and terminal cleanup may remain unit-tested.
Many internal endpoints are private platform protocols, not public APIs:
- runtime
:8088scheduled, queue, and Workflow run/notify dispatch - d1-runtime owner, SQL, drain, and renew paths
- do-runtime invoke/connect/alarm/storage cleanup/drain/renew paths
- do-runtime diagnostic probe path
- workflows internal lifecycle, step, tick, and DO alarm mutation/cleanup paths
- redis-proxy sidecar APIs for cold-load, KV, queue, logs, and runtime support
These endpoints require the x-wdl-internal-auth header whose value comes from the
shared WDL_INTERNAL_AUTH_TOKEN secret injected into every platform service task.
During rotation, receivers also accept optional WDL_INTERNAL_AUTH_PREVIOUS_TOKEN;
callers still send only the current token. The header is stripped from
tenant-originated forwarding paths. The token stays in host-owned Durable Object
proxies and host-side backend capabilities; it is not embedded in generated
tenant facade code or tenant-visible env.
Rust clients for private service and loopback HTTP calls disable ambient proxy discovery
and automatic redirects. Internal-auth headers must travel directly to the configured
private endpoint, not through an operator-shell or container HTTP_PROXY/ALL_PROXY
value or a response-selected redirect target.
Do not expose these endpoints through gateway or an internet-facing load balancer. The shared internal token authenticates in-tree platform callers on the private mesh; it does not authorize arbitrary external callers. If a new caller is outside the trusted mesh, add an explicit authentication and authorization design instead of reusing the internal protocol directly.
This "private mesh is trusted" rule is not permission to trust tenant input. Runtime and stateful services still validate worker ids, namespace/worker grammar, owner headers, generation fences, content types, request sizes, and metadata shapes at the protocol boundary.
Auth tokens are bearer tokens. Auth stores token records and token hashes in Redis;
plaintext tokens are shown only at issue time. Role evaluation is centralized in
shared/auth-roles.js.
Important role boundaries:
opsis full-plane and bootstrap-managed.ops-observeris cross-namespace read-only but intentionally lacks secret value, workflow payload, arbitrary SQL, R2 object body/head, token list, and write capabilities.nsroles are tenant-namespace scoped.platformandplatform-observerroles are pinned to platform-tier reserved namespaces.token-issueris namespace-unbound; aside from/whoamiself-introspection, its only non-diagnostic action isauth.delegated_token.issue. It cannot issue direct tokens, list/revoke tokens, or access tenant resources. Delegated issue returns short-lived credentials; namespace resource lifecycle remains outside auth token lifecycle.- Delegated namespace safety assumes routine namespace-scoped writes use namespace-bound credentials. Full-plane unbound credentials can still perform namespace-scoped writes today, and those writes may leave no auth-visible namespace fact once the active worker gate is cleared; this is an accepted residual risk until a persistent namespace fact index exists.
- Platform cross-namespace visibility requires the role kind and bound namespace to match the platform-tier rule; do not replace this with route-name checks.
Control handlers should not infer permission from URL prefixes. They should use
parseControlRoute() action classification and auth verification.
Secret PUT handlers see plaintext only long enough to validate and encrypt it. The Redis
secret stores contain WDL-ENC: envelope values; redis-proxy decrypts them only while
serving /runtime/load. There is no steady-state plaintext fallback on the runtime
cold-load path, so a missing or wrong secret-envelope provider key fails closed for
workers that need secrets.
AI credential PUT follows the same envelope owner but a different read path. A write
commits only against the exact current provider revision. The ciphertext AAD binds the
namespace credential hash key, provider field, and envelope version; it does not bind
the provider revision, so same-kind metadata updates preserve the existing ciphertext.
Authenticated redis-proxy /ai/resolve atomically reads metadata plus ciphertext and
decrypts only for the local host binding. Provider list/get and model-list responses
never contain plaintext.
Bindings are the main tenant capability surface:
- KV and queue producers use redis-proxy sidecars and runtime-owned caps.
- D1 and DO facades route through stateful runtimes that own single-writer leases and generation fences.
- Workflows facade calls workflows; workflows owns DB 2 state and does not trust tenant-provided identity fields.
- R2 uses platform S3-compatible credentials in runtime; tenant code receives an R2 binding, not raw credentials.
- AI exposes a generated facade while the credential remains host-only. redis-proxy
generates one of three exact official destinations; runtime revalidates it, discards
tenant authorization/endpoint headers, rejects redirects, and sends provider traffic
only through the public-only
AI_NETWORKservice. - ASSETS only exposes
env.ASSETS.url(path)for tokenized CDN URLs; runtime does not expose S3 credentials or bytes for assets. - Service and platform bindings are resolved from control metadata and ACLs; cross-namespace service calls require target-side authorization.
- Secrets are materialized as plaintext only at runtime
envconstruction. At rest,secrets:<ns>andsecrets:<ns>:<worker>hash values are envelope ciphertext; Redis snapshots and debug reads should not reveal tenant secret plaintext.
Redis key families have explicit owners in module docs. Indexes are usually repairable projections, not authority. Adding a second writer or fallback SCAN path is a security and correctness change because it can bypass lifecycle and delete fences.
Terraform runs platform services on ECS Fargate, including tenant-executing runtime tasks. Cloud credential exposure for tenant-running tasks is bounded by least-privilege task roles, public-only workerd outbound bindings, and private mesh security groups. ECS Exec should be enabled only where platform operator access is intended.
Service Connect and security groups are part of the internal mesh boundary. The shared
WDL_INTERNAL_AUTH_TOKEN current value must be identical across runtime,
d1-runtime, do-runtime, scheduler, workflows, and redis-proxy sidecars; the optional
previous value is accepted only as a maintenance-window rotation bridge. Callers
always send the current value, so token rotation is not rolling-safe unless traffic is
paused or the private fleet is restarted together. Scheduler is a client of runtime
internal dispatch and workflows tick; Workflows dispatches Durable Object alarms to
do-runtime. Neither scheduler nor workflows is a public service target.
Tenant-facing behavior is documented by the standalone CLI guide. Security-relevant tenant rules include:
- Tenant tokens are namespace-scoped unless the operator issues a broader role.
- The default public URL shape is namespace/worker path based.
- Custom domains and Wrangler
routesare operator-enabled, not general self-service. - Cross-namespace service bindings require target-side authorization.
- Tenant socket/fetch access to platform-private addresses is blocked at the runtime/workerd network boundary.
- Live tail is best-effort debugging, not audit history.
Logs are the primary platform audit/debug stream for many services. Do not log plaintext tokens, token hashes, secret values, raw platform credentials, or unbounded tenant payloads. Metrics labels must remain bounded and must not include namespace, worker, version, path, Redis key, token id, or raw error text.
Request ids may cross service boundaries, but they are sanitized and bounded before propagation.
- WDL does not claim Cloudflare account API parity.
- WDL does not implement Cloudflare edge cache semantics or expose
caches.default. - Internal mesh protocols do not currently use per-request mTLS between every platform service; the current application-layer mesh control is a shared static token.
- If a future deployment puts internal endpoints on a network where callers cannot be assumed to be platform code, add explicit caller-specific authentication before reusing this model.
- Tenant public application authentication is the tenant application's responsibility.
tests/unit/style-contracts.test.js: route channels, hidden Fetcher stripping, internal socket split, Fargate/task-role infrastructure guards, low-cardinality metrics, and other drift guards.tests/unit/auth-lib.test.js,tests/unit/auth-index.test.js,tests/integration/auth-worker.test.js,tests/integration/auth-platform.test.js: token and role boundaries.tests/unit/runtime-load.test.js,tests/unit/runtime-wrapper-generate.test.js,tests/unit/runtime-binding-surface.test.js,tests/unit/runtime-dispatch-workflows.test.js, andtests/integration/workflows-runtime-core.test.js: wrapper/binding exposure, tenant-realm provenance, private reporter lifetime, and real-workerd JSRPC behavior.tests/integration/service-bindings.test.js: service-binding capability delegation.tests/unit/runtime-ai-binding.test.js,tests/unit/control-ai-handler.test.js, andtests/integration/ai-binding.test.js: credential non-exposure, exact destination, public-only egress plumbing, bounded lifecycle, and DO teardown behavior.tests/unit/gateway-dispatch.test.js,tests/integration/gateway.test.js,tests/integration/routing-gateway.test.js: route and reserved namespace behavior.tests/integration/d1-*.test.js,tests/integration/durable-objects*.test.js, and workflows integration tests: owner/fence behavior for stateful bindings.