Skip to content
downpipes docs

How the update channel stays safe: trust model, rollback, and gradual rollout

This page explains why the controlled update channel is safe to use and how its recovery controls work. It is for a self-hoster who wants to understand the model rather than just follow the steps: what is trusted and what is verified, how a bad version is undone, what the opt-in gradual rollout really does, and where the honest gaps are. The day-to-day flow is on applying updates, and turning the channel on is on activating updates.

The core promise is narrow and defensible. An update channel can carry two components under one signature: the engine Worker, and, when a release ships one, the console’s static assets, each with its own apply and rollback path. Your archives are immutable objects in your own bucket and your recovery runs offline from those bytes and your recovery kit, so neither is touched by a deploy. The worst a bad release can do is a brief, rollback-able availability blip on the live engine and, if a console component shipped, the console. Everything below is how that promise is kept.

Pull, not push; verify before deploy

Nothing is ever pushed to your engine. The engine pulls a vendor-signed channel from inside your own account and checks it against a key you pinned during activation. Two guards then stand between a verified channel and a live deployment.

The signature and artefact guards

The signature guard is the pinned hybrid signature: an Ed25519 signature alongside an ML-DSA-87 signature, both halves required, verified against your pinned key and never a key the channel asserts. The artefact guard is a SHA-384 check: before anything is uploaded, the harness recomputes the downloaded bundle’s digest and requires an exact match against the hash the signed channel declared. A missing or mismatched hash is a refusal, and nothing is deployed. The deploy credential is a one-shot token scoped to the “Edit Cloudflare Workers” template, collected at apply time and never stored.

One signature covers every component

Since channel schema v2 the one signed document covers every component of a release, not just the engine. A components map describes each one: the engine as a worker module, and the console as a static-assets bundle, a single self-describing artefact carrying the console’s shell worker, every file it serves with a per-asset SHA-256, and its static config. The same pinned signer, the same detached hybrid signature and the same freshness guards cover each component’s hash and metadata, so nothing new is trusted: no second host, no second key, no new trust surface. The SHA-384 check runs per component, and the engine-only artefact list keeps being published alongside the map indefinitely, because deployed older engines read only that list; a console artefact is never placed in it, so an old engine can never select console bytes for its own script.

The console artefact gets full discipline

The console artefact deserves a precise word, because it is code that runs in the operator’s browser, the same browser that generates break-glass keys. It gets the full engine-grade discipline: its whole bundle is hash-covered by the signed channel, including the shell worker that sets the console’s security headers, so a console update cannot silently weaken those headers without the hash saying so.

A guard against dropped bindings

There is a third, quieter guard that prevents the most insidious brick. When the harness uploads a new version, Cloudflare replaces the binding set rather than merging it, so a version that forgot a binding would come up without its archive, its Durable Objects or its signing secret. The driver therefore re-sends every binding the engine had: each non-secret binding verbatim, and the secret binding types listed so Cloudflare preserves the secrets and their values. If it encounters any binding type it cannot prove it can carry forward, it refuses the upload rather than risk dropping it. Binding preservation is enforced, not assumed. The console deploy holds the same line: the apply reads the console script’s live settings first and preserves its bindings, including the ENGINE service binding and its assets wiring, rather than trusting the artefact to know your install.

The console ownership proof

Deploying to a second script raises a question the engine refuses to leave implicit: is that script actually this deployment’s console? The engine learns the console’s script name from CONSOLE_WORKER_NAME (default downpipe-console), but a name is configuration, not proof. Before the apply touches the console script it reads the script’s live bindings and requires the ENGINE service binding to point back at this engine, the script named by WORKER_NAME. If that proof fails, the console component is refused and nothing is uploaded.

The proof is also what makes the deploy-target guarantee honest. A Cloudflare API token cannot be scoped to a script name, so the one-shot token you paste could technically edit any Worker in the account for as long as you hold it. What the engine enforces is the target: the safe-apply writes only to the script named by WORKER_NAME and, for a console component, only to the script that both carries the configured console name and proves ownership through its ENGINE binding, never to any other script.

Sequenced with gates, reported honestly

When a release moves both components, the order is fixed: the engine applies first and must settle, a keep verdict from the canary, or from the self-check on an engine whose canary cannot gate, before the console component applies. A new console may call new engine API, and engine API changes are additive, so engine-new with console-old is the direction that is always safe; applying the console first could not make that guarantee.

The failure semantics never pretend two Workers are one atomic unit. If the engine refuses or rolls back, the console is not applied: the release was not applied, and the console is unchanged. If the engine settles and the console upload or promote then fails, the console auto-rolls back to its recorded target and the engine keeps the new version; that partial state is reported as exactly what it is, and the console component is retriable alone. True cross-worker atomicity is impossible on the platform, so the design is sequencing plus gates plus the additive-compatibility rule, stated plainly rather than papered over.

The settle itself is judged with patience and recorded with discipline. Only a dead canary, a bit strayed on the new code, rolls the engine back immediately; a verdict that is neither alive nor dead is retried on a short bounded schedule before anything is decided, because the settle runs in the window where the code swap has just reset the engine’s Durable Objects and a transient failure there is expected. And the settled outcome is persisted before any rollback deploy is issued, then surfaced on the update status. That ordering matters because a rollback deploy swaps the running code out from under the very request reporting the outcome; persisting first means the console can always re-read the recorded truth instead of asserting a guess. The engine’s first live apply, on 2026-07-03, is what taught this lesson: the promote and the automatic rollback both executed correctly against real Cloudflare, and the one dishonest moment, a generic error reported after the version had already been reverted, is the moment the persistence-first rule now removes.

The standalone rollback

Rollback is a first-class control, always available, and independent of any in-flight apply.

Triggering a rollback

It does not require that an update be mid-flight; the engine records a known-good version the first time you apply an update from the console and keeps it until you are back on a known-good version, so a target plausibly exists whenever an apply has happened.

When you trigger it, the harness resolves the recorded known-good version, reads the current live version so it knows what it is reverting away from, deploys the known-good version (atomically), and canary-verifies that it landed healthy. If the live deployment is already that version, it is a clean no-op. If there is no recorded target, it refuses rather than deploying anything blind.

No second-owner approval on revert

By design, a standalone rollback needs no second-owner approval. Rollback is the safe recovery direction, the same direction the automatic settle rollback takes, and gating it would impede incident response by leaving a lone owner unable to revert an unhealthy engine. So dual control is kept on the consequential direction (trusting new code) and never on the revert.

Per-component rollback

Rollback is per component too. A console rollback is a first-class control of its own: engine-driven with the same one-shot token, back to the console’s recorded target, offered one-click by the console when its post-apply self-check sees the wrong version serving, and available alone without touching the engine. And the anti-rollback floor is kept per component: the engine and the console each remember the newest version they have successfully settled, meaning canary-passed rather than merely attempted, so a correctly-signed channel that recommends an older version is refused for that component.

The downgrade-to-recover opt-in does not cross that floor, and reaching for it mid-incident wastes time. The floor check runs first and unconditionally, and refuses even with the opt-in set, because pushing an engine below a version it has already run would reintroduce an already-patched vulnerability. An older target that cannot be compared to the floor at all is refused too, failing closed. What the opt-in actually relaxes is the forward-only guard against the version currently running, and it may cross the compatibility floor for a same-or-forward re-pin. If you need to go back, the standalone Roll back control is the path: it targets a recorded known-good version by id and is deliberately exempt from the floor. System-controlled backward motion through that control stays allowed; channel-driven backward motion stays refused.

The console states a rollback as a controlled, safe outcome, never an alarm: The version the engine landed back on is named in the actual message, so the live line reads, for example, “Rolled back to <version>. The canary sings on it; your data and recovery were never affected.” where <version> is the recorded known-good Cloudflare version id, not a fixed phrase. Even a rollback whose canary could not confirm health, or one that failed outright, reports that nothing was half-applied and that your data and recovery are unaffected, because archives are immutable and restore is out of band.

The opt-in gradual ramp serves real traffic

A normal apply is atomic: promote to one hundred percent, canary-gate, keep or auto-rollback. The gradual ramp is a separate, explicit, per-release opt-in, off by default. It uploads the new version, ramps a fraction of live traffic to it (one to ninety-nine percent) with the same verify-before-deploy, migration and compatibility guards as the atomic path, then canary-gates the new code. If the canary sings, the ramp holds at that percentage awaiting an explicit promote-to-one-hundred; if it does not, the whole deployment is rolled back to one hundred percent on the prior version. The ramp is engine-only: a static-assets swap is atomic at promote and has no traffic-percentage concept, so a ramp request naming the console is refused.

The honest framing matters, because it changes when you should use it. A ramp serves the new version to a fraction of your real production traffic. It is not an isolated preview or a sandbox. An isolated preview URL would need a workers.dev host, which downpipes does not use, so the ramp is the feasible substitute rather than a risk-free trial. The console states this as a standing caveat on the ramp control itself, before you ever run one, not as something the outcome message repeats back to you.

The practical consequence is that for a single, low-traffic engine, the atomic promote plus canary is the recommended default. A ramp earns its keep when you have enough live traffic that a percentage slice is a meaningful sample; on a quiet engine it mostly adds steps without adding signal, and the atomic path is already canary-gated and reversible.

Release metadata, alerts and dual control

What the channel carries

The channel carries more than a version number, and all of it is under the same signature, so it is tamper-evident without a new trust surface.

Field What it is How it is used
changelog Structured change lines grouped by type Shown in the “What’s in this update” panel
impact Human impact notes Shown alongside the changelog
requiredSteps Explicit operator actions, optionally blocking Shown as a prominent “Required” badge; a blocking step must be ticked as acknowledged before Update now enables, one checkbox per blocking step, so you cannot apply past an action the release needs. A non-blocking step is informational only and does not gate
minEngineVersion The oldest engine a release may apply onto The compat guard refuses an apply onto too-old an engine
compat A human note paired with the floor Shown when a release has a compatibility floor
riskClass routine, migration, or breaking Drives the console tone and the dual-control rule

Alerts are proactive, never automatic

Alerts are proactive but never act. A once-per-version alert fires on the cron when a newer version is available, so you learn about an update without polling; it never auto-applies. Applying is always operator-driven.

Dual control is targeted, not blanket

Dual control is targeted, not blanket. When the dual-control gate is on, a second Owner’s approval is required only for a migration or breaking release, bound to the specific target version and artefact hash and never to the deploy token. A routine release applies with a single Owner click even when the gate is on, which keeps the common patch case fast. An unlabelled or unrecognised risk class is treated as migration-class, so an old or garbled channel gets the second-owner approval rather than slipping through as routine. The settle step gates the same way, on the keep direction only; the rollback and the dry-run are never gated.

Components change none of that shape. The release-level risk class is the default and a component may carry its own override, so a console-only hotfix can ship as routine even in a series whose engine release was migration-class. Dual control gates a live apply as one action: one approval per apply, regardless of how many components the release carries, never a second approval queue.

Provenance: recorded at apply, published with the release

The provenance story has two halves, and each half is owned by the party that can actually prove it.

The vendor half rides the signed channel. A release published through the attested ceremony carries a per-component provenance block inside the signed body: the source commit, the release tag, the CI run that built and attested the artefact, the Rekor transparency-log index of its keyless signature, and channel-relative paths to the attestation files (the SLSA statement, the cosign bundles, the canonical checksums and the release record). The offline release signature covers those pointers, and the ceremony refuses to sign bytes CI did not attest, so a present block means the offline signer checked the attestation at signing time. A release published before this machinery carries no block, and the console says so plainly rather than linking to anything. Verify a release yourself is the recipe for checking all of it independently.

The customer half lives in your own records. At apply time the engine verifies the downloaded bytes against the signed digest (as it always has), and now also persists that digest on the pending and settled lifecycle records and in the hash-chained audit log, so which content hash went live is durably provable in your own account. Between upload and promotion the engine additionally reads the just-uploaded version back from Cloudflare’s own API and hashes it against the signed digest: the read-back verdict is recorded on the same records, and an unavailable read is recorded as exactly that rather than assumed verified. The gate runs in warn mode by default, recording evidence on every apply; enforce mode, which refuses promotion on anything but a verified read-back, is enabled only after the platform byte-identity experiment, and a refusal can never brick, because nothing has been promoted yet and the previous version keeps serving.

Two Security Centre checks keep this honest between applies. update-apply-provenance confirms the last applied update left content-hash evidence. update-version-drift compares the running deployment’s version identity (the version_metadata binding) against the version the last verified apply left live, so a redeploy outside the update flow, whether a deliberate wrangler deploy or an unexpected actor, flips a visible finding instead of passing unremarked. Both ride into the signed evidence packs where a framework maps them: the drift check under ISO/IEC 27001 A.8.16, the provenance check under NIS2 Article 21(2)(d).

A running Worker still cannot hash its own deployed bytes at runtime, so the engine’s self-stamped artefact SHA-384 remains documentation rather than a verified assertion; the values above are the verified ones because they were computed and recorded at apply time. The hash that actually gates a deploy is always the signed channel’s SHA-384 over the downloaded artefact, unaffected by anything the engine self-reports.

The console carries its own version identity the same honest way. The console build stamps a version into the bundle and emits it at a build-stamp path on the console’s own origin, and the Provenance card shows both component versions side by side, closing the old blindness where a customer could not even know their console was stale. After a console apply, the page checks that its origin is actually serving the new version before it claims success, and offers the one-click console rollback when it is not. The stamp is documentation of what is deployed, not a verified assertion; the hash that gates a console deploy is always the signed channel’s, checked before upload.

A freshly checked-out engine reports no artefact hash. The stamp is applied at build, and the committed source ships only a placeholder sentinel, so a source checkout, a test run or any unstamped build honestly reports absence rather than a fabricated value. Do not expect a raw git checkout to print its own digest.

The honest gaps

A trust page that only listed strengths would not be honest. These are the real limits today, stated plainly so you plan around them.

The engine reports a Cloudflare version id (cfVersionId) on its status, but no console screen surfaces it. Use the version names the apply, settle and rollback flows show you rather than expecting a Cloudflare version id in the console.

The provenance cross-check is on-demand, not a standing badge. The Provenance section can fetch the published release record and compare it against the digest your account verified, but it does so when you ask (and states honestly when the channel host refuses browser reads); the standing indicators are the two Security Centre checks, which grade evidence presence and version drift, not a live byte comparison. Runtime attestation of the executing isolate does not exist on Workers and is not claimed.

The live deploy drivers are validated against a stubbed Cloudflare. The engine driver has now also run against a real account exactly once, on 2026-07-03, where one promote and one automatic rollback both executed correctly; the console static-assets driver has not run live at all. One flight is not a battle-tested path, so the first live apply and the first rollback drill on a given deployment remain supervised first-live gates, and a console-carrying release is the most supervised of all. Auto-rollback only happens inside the apply or settle request: the hourly canary can detect an unhealthy live version and raise a critical alert, but the engine holds no standing deploy credential and so cannot revert itself out of band. Once a settle has decided, the recorded outcome on the update status is the authoritative result to trust.

A console deployed before the component-aware feature cannot render any of it. The first console-capable rollout on an existing install requires one manual console redeploy, per the self-host runbook; after that one deploy, console fixes are channel-driven like engine ones.

Where this fits

For the step-by-step apply, preview and one-click flow, see applying updates. To turn the channel on in the first place, see activating updates. For the operator-CLI upgrade path and the migration and schema-epoch rules that bound a rollback, see upgrades and rollback. For the canary that gates every keep decision, see the canary, and for the hybrid signature scheme, see cryptography.

Last updated .