Cloudflare API token scopes for deploying downpipes
This page states exactly which Cloudflare API token permissions you need to deploy or upgrade downpipes, how tightly to scope the token, and the lifecycle it moves through. It is written for the operator who creates the token and for the security reviewer who has to sign off on what privilege the deployment holds. It is the written evidence behind the configuration-and-deployment assessment.
The single most important property is the one a reviewer should check first. The running engine holds no Cloudflare API token that can write to or deploy your account. A deploy token exists only for the minutes it takes to deploy or upgrade, and is revoked afterwards. Its binding-based sources (KV, R2, D1, Secrets Store) are reached exclusively through Worker bindings, which is least privilege by construction. The only Cloudflare API token the running engine may hold is an optional, read-only, account-scoped discovery token, used by the REST-based sources (cf-config, Workers, Stream, Images) to enumerate what to back up; you only create it if you use those sources, and it can never write. The vendor still holds nothing, which is the foundation of the no-custody model. Everything else on this page is detail around those properties.
The three privilege states
The deployment moves through three distinct states, and keeping them separate is what keeps the runtime posture least privilege.
Bootstrap and deploy
To stand the engine and console up the first time you create a Cloudflare API token with the permissions listed below. It is powerful: it can write Workers, create KV namespaces and R2 buckets, write Secrets Store entries, and edit DNS on the account. For that reason it is short-lived. You create it, run the deploy, confirm readiness, and then revoke it. It does not live in CI, it is never sent to the vendor, and it stays on the operator’s own machine, sent only to api.cloudflare.com. The one supported exception to “never in the console” is a single in-portal privileged operation that asks you to paste a one-shot scoped token (for example attaching a source), where the token is used for that one read-modify-write and discarded, never stored.
Runtime
Once deployed, the engine holds no Cloudflare API token that can write to or deploy your account. Its binding-based sources (a KV namespace binding, an R2 bucket binding, a D1 database binding, a Secrets Store binding) and its destination are reached through Worker bindings and its Durable Objects; a binding is the capability itself, scoped by the platform to exactly the named resource. The REST-based sources (cf-config, Workers, Stream, Images) are read instead with the optional, read-only, account-scoped discovery token, which can never write and is the only Cloudflare API token the running engine may hold. So the only account-wide credential the Worker’s environment may contain is a read-only one; there is no standing write or deploy credential. Even a full compromise of the running Worker yields only read access through that token plus what its bindings grant, never the ability to deploy Workers, rewrite config, or otherwise mutate the wider account. This is the least-privilege-by-construction property that underpins no custody.
The break-glass private key is a separate matter and is never on the engine at all. It is generated on your own machine or in the browser during the key ceremony, and never transmitted to the engine or the vendor. Recovery works offline from the destination bytes and that key alone.
Update and upgrade
To deploy a new version, edit bindings, or rotate a secret, you re-create the same scoped token, perform the change, and revoke it again. The deploy capability is summoned only for the duration of the operation. The engine never gains a deploy token to do this itself: an upgrade is an operator action from a trusted machine, not a self-update the Worker performs with a stored credential.
The token permissions
Create an Account API token (My Profile, then API Tokens, then Create Custom Token) with the permissions below. Every one is an Account-level permission; none is a User grant or a Zone-wide grant beyond the DNS edit.
| Permission | Level | Why it is needed |
|---|---|---|
| Workers Scripts: Edit | Account | Upload and update the downpipe-engine and downpipe-console scripts. This also covers the scheduled */15 cron trigger, so no separate cron permission is required, and it covers creating and migrating the engine’s Durable Objects as part of the script upload. |
| Workers KV Storage: Edit | Account | Create the KV namespaces the engine reads as sources, and bind them to the worker. |
| Workers R2 Storage: Edit | Account | Create the destination archive bucket and any R2 source buckets, and bind them to the worker. |
| D1: Edit | Account | Create the D1 databases the engine reads as sources, and bind them to the worker. |
| Secrets Store: Edit | Account | Create the Secrets Store entries the engine reads through a binding (for example the signer private and the recipient public halves), and put their values. |
| Workers Routes: Edit | Account | Attach the worker(s) to their custom-domain route. In the shipped routeless topology this is the console’s domain; see the note below. |
| DNS: Edit | Account, the relevant zone | Provision the DNS records the custom-domain route resolves through. The console (and the engine in a split topology) is custom-domain only; the workers.dev route is disabled by policy. |
| Account Settings: Read | Account | Read the account metadata wrangler needs to resolve the account and its subscriptions during deploy. |
What is deliberately not in this list
There is no runtime deploy or write token, because nothing in the running system uses any of these deploy permissions; the only credential the runtime may hold is the optional read-only discovery token for the REST-based sources, which is a separate read-only grant and can never write. Cron triggers are covered by Workers Scripts: Edit, so do not add a separate trigger permission. The engine’s Durable Objects are created and migrated through the same Workers Scripts: Edit permission as part of the script upload, so no separate Durable Objects permission is required either. Outbound email is enabled separately in the dashboard with its own sending-domain onboarding and is not part of this token. Cloudflare Access is configured in the dashboard or with its own token and is not required to deploy.
The route and DNS permissions are about the console domain
In the shipped topology the engine has no public hostname. The console is the only public surface; it reaches the engine over a worker-to-worker service binding and proxies the admin and support paths same-origin. So in the normal case the Workers Routes and DNS permissions provision the console’s custom domain, not an engine route. The permission list in the engine’s CLOUDFLARE-PERMISSIONS.md shows route examples for both an engine and a console host, which read as if the engine always has its own route; treat those examples as stale relative to the routeless default. You only need a route and DNS record for the engine itself if you deliberately choose the split topology, which gives the engine its own custom domain (for example to front the two surfaces with separate Access policies). Scope the route and DNS permissions to the host you are actually creating, so you do not over-scope to an engine route that does not exist. The layouts are described on the topology page.
Scope the token tightly
When you create the token, narrow it as far as Cloudflare allows.
- Account resources: limit the token to the single account you are deploying into (“Include”, then “Specific account”), never “All accounts”.
- Zone resources: for the DNS edit, limit to the specific zone that hosts your domain, never “All zones”.
- Script filter: where Cloudflare offers it, scope the Workers permission to the named scripts
downpipe-engineanddownpipe-consoleso the token cannot rewrite an unrelated worker. - TTL: set the shortest expiry the workflow allows, for example a few hours, as a backstop in case you forget to revoke it. Manual revocation immediately after the deploy is still the primary control.
Create it, use it, then revoke it
This is the whole deploy lifecycle of the token.
Create the token
Use My Profile, then API Tokens, then Create Custom Token. Add the permissions above, scope it to the one account and the one zone (and to the named scripts if the filter is offered), set a short TTL, and create it. Copy the value once; Cloudflare shows it only then.
Use it on your own machine only
Export it for the deploy session and never write it to a file that outlives the session:
export CLOUDFLARE_API_TOKEN=<the token> export CLOUDFLARE_ACCOUNT_ID=<your account id>Then provision and deploy with
npm run deploy, following the first deploy walkthrough. The token is sent only toapi.cloudflare.com. It is never sent to the vendor, never placed in the console for routine use, and never committed.Confirm readiness
Check that
GET /admin/statusreportsready: true(the signer present, the recipient public present, a destination resolves) and that a first source backs up. The console drives this check.Revoke the token
As soon as the deploy is confirmed, revoke it from the API Tokens dashboard (Delete, or Roll then Delete). After this the system runs with no Cloudflare deploy token anywhere: the engine holds no write or deploy credential (at most the optional read-only discovery token for the REST-based sources), and there is no stored deploy credential to leak.
Re-elevate only when you next deploy or upgrade
Repeat from the first step for an upgrade, a binding change, or a secret rotation, and revoke the token again at the end. The deploy capability is never left standing.
The same token also reads your live bindings at deploy
There is a second, narrower use of these credentials worth calling out, because the safe deploy path depends on it. npm run deploy runs a binding reconcile that reads the engine worker’s live bindings before it deploys, so a deploy cannot silently drop a source you attached from the console. That read uses the same CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID you exported above, and it needs only the Workers Scripts: Edit permission to read the script settings. Setting them is not the only way it can read, though: with either one missing it falls back to your own wrangler session, which is why a deploy authenticated by wrangler login alone is still binding-safe. It refuses to deploy only when the path it took genuinely failed, for example an under-scoped token or no wrangler session either, rather than ship a config that might drop your sources; a first deploy with nothing to preserve is the documented exception. The full behaviour is on the deploy safety and bindings page.
The reviewer-facing property
For a reviewer signing off on least privilege, the evidence is concrete and checkable.
The deploy token is scoped (account-limited, zone-limited, and ideally script-limited), short-lived, and explicitly revoked after use. The runtime holds no Cloudflare API token that can write to or deploy your account: the engine’s write capabilities are its Worker bindings, which are enumerable against wrangler.toml, and each binding is platform-scoped to one named resource. If you use the REST-based sources, the runtime additionally holds one optional, read-only, account-scoped discovery token (set as DISCOVERY_API_TOKEN, or activated and stored at runtime from the console); it can enumerate and read but never write. A reviewer can confirm this directly by inspecting the deployed worker’s bindings and environment: the only Cloudflare credential present is at most that read-only discovery token, never a write or deploy token. The deploy capability is re-elevated on demand for upgrades and dropped again, so there is no standing high-privilege credential between deploys.
Two further points strengthen the runtime posture rather than the deploy posture. A binding is standing read access for the life of the deployment, not just during a run, so the binding set is the engine’s privilege; bind only the sources whose loss genuinely matters, and treat secrets, signing-key namespaces and ephemeral auth state as deliberate, separately risk-accepted inclusions rather than defaults. And the vendor holds nothing throughout: not your data, not your keys, and not a Cloudflare token, so there is no vendor-side party who could act on your account even if they wanted to. The optional read-only discovery token the engine may hold for the REST-based sources is the customer’s own and is never transmitted to the vendor, so it does not change the vendor-side picture.
A separate, edit-scoped token for a configuration restore
There is one more Cloudflare credential in the product, and a reader who plans only for the deploy token and the discovery token will meet it for the first time in the middle of a recovery. Writing your Cloudflare configuration back needs a second, edit-scoped Cloudflare API token, and it is not a widening of either token above.
Nothing in the backup path needs it. The cf-config source is read entirely with the read-only discovery token, so a customer who never restores configuration never creates this one. It exists because re-applying configuration is a write against your live account, and the whole point of the runtime posture is that the engine holds no credential that can perform such a write. So you supply it yourself, per restore, in the restore request. The console’s restore form carries a single field for it. The engine never stores it, never writes it to a log, and never folds it into the dual-control approval hash, so an approval can be shown and recorded without carrying the credential. A dry run is different again: it only reads live config to compute the diff, so a read-scoped token previews a restore and writes nothing.
Under-scope this token and the apply fails, so scope it from a dry run
The apply is fail-open per item, which means one item your token cannot write is skipped and the rest of the surface still applies. The surface is not let through on the same terms. Each refusal is classified, and a surface left short of what the archive holds is raised as a restore failure carrying the count per refusal class and the remedy for each, so ok and complete come back false and the receipt records the surface with verified: false. An apply that wrote nothing cannot report as a clean one.
One class is exempt on purpose: an entitlement refusal means your account’s plan does not carry that surface, so there is no item to restore and the apply is right not to fail.
The skipped-record count is a separate thing and still will not show you this, because it counts records the engine deliberately never attempted, and a token that is merely too narrow was attempted and refused. For which items refused and why, read the per-surface outcomes the apply returns. Build the token from a dry run rather than from a permission list. What each in-band surface actually needs is enumerated on backing up and restoring your Cloudflare configuration.
Treat it the way you treat the deploy token: create it for the restore, scope it to the one account and the specific zone, use it, then delete it. It is a third short-lived operator credential, not a fourth standing one, and the runtime posture above is unchanged by it.
A separate token for the update channel
The signed update channel uses a different, narrower deploy credential and is a deliberately separate concern. Publishing a new signed channel is an operator action that needs only Workers Scripts: Edit (or, for the static channel files, R2 write), not the full deploy permission set above. The channel itself is pull-only and pinned to a vendor signing key, so the running engine never gains a credential to update itself. Keep the update-channel credential separate from your deploy token so that managing releases does not require the broader account-write scope. The mechanics are on the upgrades and rollback page.
Where this fits
This page is the canonical home for the deploy token permissions. The end-to-end deploy that uses the token is first deploy. Why npm run deploy must read your live bindings, and what the reconcile does with this same token, is on deploy safety and bindings. The reasoning that the vendor holds no standing credential at all is in the no-custody trust model. To decide whether you need an engine route at all, see topology.
Last updated .