Skip to content
downpipes docs

Changelog and versioning

downpipes tracks more than one version, and they move independently. This page explains what each version line means, which version each artefact carries, and how the release artefacts relate to one another. It is for a developer or an auditor who needs to reason about compatibility. It also hosts the human-readable change record.

The headline distinction is the one to hold onto. The on-disk archive format carries its own version, downpipe/0.1.0, and it moves independently of any tool’s version. It is a semver version. While the major is 0, a change to any byte-level rule of the format bumps the minor, which is a new format identity rather than a revision of the old one, and a patch changes no byte-level rule. The reader, the recovery CLI and the engine each carry their own, separately moving version; changing one of them is not changing the format.

The version lines

There are four version lines, and they answer different questions.

Version lineWhat it isWhere it is setMoves when
Archive formatThe on-disk format string downpipe/0.1.0. It appears byte-identically in the cleartext root manifest and in the format’s key-derivation labels.The format constants, shared by the Go reference and the engine.Only a change to a format rule. While the major is 0, a byte-level change bumps the minor and is a new format identity; a patch changes no byte-level rule.
Tool (CLI) versionThe version of the offline downpipe reader and recovery CLI.Injected at release time by the build pipeline; an unstamped local build reports dev.Per CLI release.
Engine versionThe engine’s own release version, compared against the vendor-signed recommended version on the update check.A constant in the engine source, bumped per engine release.Per engine release.
Console versionThe console’s own build-stamped version, shown on the Provenance card and compared against the signed channel’s console component in the browser (the engine cannot know which console build a browser is running).Stamped by the console build into the bundle and its build-stamp file.Per console release.

The format version is the contract that matters for recovery. Because the reader and the format are versioned separately, an archive is readable by any reader accepting its format identity, whichever engine version produced it. A reader for the current format accepts exactly downpipe/0.1.x, so it opens every archive in the current format line and refuses one that names a different identity. That separation is what lets the recovery path stay independent of the engine.

The format version is distinct from the tool version

Do not conflate the two. The archive format string downpipe/0.1.0 lives in the format spec and moves only when a format rule changes. The tool version is a separate number that the CLI binary reports, and a plain local build of the CLI reports dev rather than a release number. A release of the CLI does not move the format version, and a change to the format does not pin the CLI to any one tool version.

The archive format version

The archive format is downpipe/0.1.0, specified normatively in the format spec and pinned by the conformance vectors. The version string is not cosmetic: it appears byte-for-byte in the root manifest and is mixed into the format’s HKDF and MAC labels, so it is bound into the cryptography rather than being a loose tag. Change the string and you change the derived keys, which is why a byte-level change produces a new format identity rather than an amendment to the existing one.

A change to any byte-level rule of the format is a new format version, not an erratum and not a changelog entry. While the major is 0, that change bumps the minor, and a reader built for the current format does not accept the result. A patch is reserved for a change that touches no byte-level rule, which is why a reader accepts the whole downpipe/0.1.x line rather than one exact string. That is a deliberately high bar. It means an archive’s interoperability is governed by the spec and its vectors, and a reader either accepts an archive’s format identity or it does not. The change record below tracks the engine, the console and the reader; it does not track format-rule changes, which are a spec concern.

The tool version and the engine version

The CLI’s version is injected at release time by the build pipeline through a linker flag. A plain go build, with no release stamping, leaves the version at dev, so an unstamped local build reports itself as dev. A build that the release pipeline did not cut reports dev, not a release number.

The engine carries its own version constant, separate from the CLI. The update check reads it and compares it against the recommended version in the vendor-signed channel: an update is available when the recommended version is newer than the engine’s own. The engine version also gates compatibility, since a release can declare a minimum engine version it must be applied onto, and the one-click apply refuses to apply a release onto too old an engine. A release may also carry a console component with its own version; the console compares that against its own baked build stamp in the browser, because the engine cannot know which console build a browser is running.

The release artefacts, and what each one is

The version numbers above are carried by several distinct artefacts. Each one is worth setting out before you reason about compatibility from a number.

The reader is built from source with Go 1.26 or newer, from a copy of the module you hold. downpipes-io/downpipe is a public, MIT-licensed repository, so go install github.com/downpipes-io/downpipe/cmd/downpipe@latest and a plain git clone both resolve. For an actual recovery, hold the source in advance. Also hold a Go toolchain that already satisfies the toolchain pin in the go.mod of the source you hold (the v0.3.6 release pins Go 1.26.10). Build with go build -mod=vendor, so the recovery itself makes no network call at the moment you need it. Install the tool sets out what that pin means for a build on a machine with no network.

ArtefactWhat it is
The git tagThe tag a release is cut from. A tag names a reader build, never an archive format version, and the two number spaces move independently.
A from-source vendored buildBuilt with go build -mod=vendor against the dependencies checked into ./vendor, with no module download, and with no network call at all provided the machine’s own Go already satisfies the toolchain pin in the go.mod of the source you hold. Run downpipe --help afterwards and confirm prune, recombine and unseal-export are listed before relying on it for a real recovery.
The release buildThe release pipeline builds against a tag and produces platform archives, CycloneDX SBOMs, a checksum file and a cosign Sigstore bundle over that checksum file. The macOS binaries are signed ad hoc and are not notarised by Apple. If you download a macOS archive with a web browser, macOS blocks the binary when you run it. Verify the archive first with the checksum and signature steps in the reader’s VERIFY.md. Then run xattr -d com.apple.quarantine ./downpipe on the unpacked binary. An archive you download with curl is not blocked.
The version a binary reportsThe release pipeline stamps the tag into the binary through a linker flag. A local build runs no such pipeline, so it reports dev.

Confirm the command set, not just the version string

Confirm the reader is complete with downpipe --help before a real recovery, and check that prune, recombine and unseal-export are listed. A from-source build stamps no release version, so the version string alone will not tell you whether the command set is complete.

Customers self-host their own engine and console, each running in their own Cloudflare account. The vendor serves the signed channel that self-hosted engines pull, and the base config pins its signer, so a base-config engine reports the channel as configured and verified. The deployed Cloudflare version id of a running engine is recorded for the update self-check, but it is not surfaced as a customer-facing version number; the version a customer reasons about is the engine version above.

The change record

The change record below follows the Keep a Changelog convention and Semantic Versioning. Each heading names the component it records, because the engine, console and tool lines move separately, as set out above. A tool version heading is not the archive format version, which is downpipe/0.1.0 and is tracked separately. Each entry states what the release contains. The line-by-line record, including dependency updates, is the CHANGELOG.md in each component’s repository.

Reader 0.3.6, 2026-10-26

The offline reader, the recovery CLI and the library. It reads the archive format line downpipe/0.1.x.

Count and size fields. The reader refuses a count or size field written as JSON null with exit 6, as it refuses any other non-canonical count.

Toolchain. The build and release toolchain is Go 1.26.10. It carries the fix for the Go standard library advisory GO-2026-6809 in crypto/x509.

Engine 0.3.10, 2026-11-01

The engine release that the signed stable update channel recommends. It writes the archive format downpipe/0.1.0. It pairs with console 0.2.10.

Post-update canary. After an update, the canary flight writes, seals, reads back, restores and verifies on the new version. A dead verdict on the new version rolls the update back. An ailing or pending verdict keeps the update with confirmation pending, and the hourly canary confirms it.

Gradual ramp. A ramp rolls back when the canary reads dead on the ramped version. A ramp also rolls back when the canary sang before the ramp and does not sing after it. A ramp completes when a verification that lands on the ramped version keeps it. A keep that lands elsewhere is inconclusive, and the ramp stays as it is.

Engine 0.3.9, 2026-10-25

An engine release. It writes the archive format downpipe/0.1.0. It pairs with console 0.2.10.

Backup RUNLOG and parsers. The engine refuses a signed count or size field that is JSON null, as it refuses any other non-canonical count. It treats a null array, array element or object on the path to a count the same way. A record’s packed object is the exception, and it may be null. An absent field stays absent. A RUNLOG line that holds a null count stops RUNLOG writes for every downpipe on that destination, because every writer reads the whole RUNLOG.

Restore. A restore refuses a Cloudflare account id or zone id that is not 32 lower-case hex characters before it sends any request. It encodes each id in the request path.

Discovery. With an environment discovery token and no discovery configuration, a downpipe source names only the engine’s own Cloudflare account. A cf-config zone belongs to its source’s account. The scheduler follows CF_ACCOUNT_ID when it changes.

Rollback and update. A rollback after a secret change uploads the prior code as a new version that keeps the current secrets, then deploys it. The engine refuses the rollback when that code does not match the digest it recorded for the version. For 15 minutes after a promote, a settle answered by the previous version answers inconclusive and leaves verification pending. The channel publisher refuses to sign a channel whose engine bundle reports a different version, no version or more than one version.

Support pack. A seal fault row, a seal fault span and a replication destination row carry their engine version. A row is marked stale when that version is not the running one. A red destination shows its current resolution beside it. A seal fault span closes when replication to its destination recovers and no listed downpipe still fails there. The replication, notify, destination, recovery refusal, status baseline and audit page sections show an error when their read fails.

Security. A caller without the Owner capability sees a push destination, or an OTLP metrics push destination, as the endpoint’s scheme, host and port only. The view says it redacted the rest. The approval inbox lists a pending push destination the same way for every caller.

The passkey registration routes refuse a passkey, OIDC or SAML session caller whose request carries no Origin or one other than the console’s. They also refuse a Cloudflare Access caller whose request carries an Origin other than the console’s. Every other mutating admin route applies the same rule.

Release files. SHA256SUMS.txt names the engine bundle by its file name alone, the same name as the SLSA provenance subject. The release SBOM names the exact generator version and is reproducible.

Console 0.2.10, 2026-10-25

The console component that the signed stable update channel carries beside engine 0.3.10. It needs engine 0.3.9 or newer. Applied on its own to an older engine, it is refused with that reason.

Sign-out. Sign-out asks the engine to end the passkey session and completes in the console at once. When the engine does not confirm within 3 seconds, a warning appears. It says the session can stay valid until it expires and how to end it now.

Transport. The console redirects a plaintext GET or HEAD on a non-loopback host to the https form with 308. It refuses any other method on such a host with 400, so a licence claim code always travels encrypted.

Restore form. The restore form trims and lower-cases a pasted Cloudflare account, zone or media account id. It accepts exactly 32 hexadecimal characters.

Push destinations. A push or OTLP endpoint cut to its scheme and host for a reader without the Owner capability carries the note “(scheme and host only; an Owner sees the full endpoint)”. A delivery refused because the destination host is not on the engine’s egress allowlist explains the reason in both delivery trails.

Updates. After an engine release is applied, the console verifies it once the new engine version answers. The end-of-support notice writes the date in full.

Release files. SHA256SUMS.txt names the console bundle by its file name alone. The release SBOM is reproducible, so two builds of one commit give the same bytes.

Engine 0.3.8, 2026-10-11

The engine release that console 0.2.9 ships beside. It writes the archive format downpipe/0.1.0. The UPGRADING.md file in the engine repository lists what to check before you upgrade.

Update support. The signed update channel carries the release date of every release. A release is supported until 90 days after the next release ships. GET /admin/updates reports supportedUntil and supportStatus (supported, ending or end-of-life) for the running version. The latest release has no supportedUntil.

Backup RUNLOG. The audit action runlog-resumed records an Owner’s attended POST /runlog/resign that publishes an absent RUNLOG.sig again from the engine’s pending signature. The support pack lists the action, and records a sig-restored anomaly for the same event. The support pack also shows each RUNLOG hand-over and relink, and a refused relink writes an audit row that names its refusal. A re-key that the RUNLOG pre-check refuses for a chain anomaly records an anomaly row that names it.

A RUNLOG append refuses an index that another run in that destination’s RUNLOG already holds, and the support pack records a duplicate-index anomaly. The scheduler holds new runs and reads every configured destination’s RUNLOG. It moves the run counter past the highest index any of them holds. The support pack records a counter-advanced-after-recovery row with where the counter moved from and to.

The recovery seed also takes the highest verified index across the configured destinations. After a control-plane recovery, the run counter can restart below the indices a second destination holds. Such a run fails and does not write a duplicate index.

A RUNLOG append refuses an anti-rollback floor that is not a whole number above zero, with the integrity reason floor-invalid. When two first backups race on one empty destination, the run that loses the create backs off and re-reads, and records no anomaly. If the signature has not landed when its attempts run out, an inline run parks and resumes. A seal verify that reads a RUNLOG while another run writes it records no integrity fault when the next read verifies. A RUNLOG writer waits for a held lock long enough to take its turn.

Replication. Replication copies the recovery bundle to every destination that holds runs. A destination that you reach only through replication verifies and restores with --signer-fingerprint like any other. During a new destination’s first sync, the other downpipes keep replicating their runs. A support pack replication row for a destination that is reachable but holds no run carries copy: "none-yet": the destination has no restorable copy yet.

The Security Centre check “Sources fan out to two destinations” counts, for each downpipe, the destinations that hold its latest successful run. A destination that is still catching up, or one whose copies keep failing, counts once replication reports the run there.

Configuration backup. The Cloudflare configuration backup captures per-bucket R2 settings. For each bucket, the r2-bucket-config surface records CORS rules, lifecycle rules, bucket lock, custom domains, public r2.dev access, Sippy and event notifications. The engine does not write bucket settings back: the restore plan marks them captured, not restorable, and gives the manual step. The config history is signed under its own key, so a global sign-out leaves every stored history entry verifiable.

A deleted D1 source reads as deleted, not unavailable. An R2 bucket-lock refusal reads as ObjectLocked.

Support pack and posture. A failed run names its destination, and each destination fault names its destination. A refused destination save, and a refused clear or remove, records the refusal class and the blockers behind it. The support pack reports each destination’s write facts: key class, checksumRequired and the region hint. It reports the update channel state and why an update channel fetch failed.

The site-fault legend states the evidence each fault undermines. The SCIM service provider configuration states that you must also remove a member who signs in through Cloudflare Access in Cloudflare Access, because SCIM cannot reach it. A SCIM deprovision ends their sessions in the engine.

After an account recovery, the support pack states the Owner actions still pending. Its keys section reports operationalRetired.state (stale-key-held, applied or not-set), and for stale-key-held it names the POST /admin/keys/break-glass-only route that deletes the operational key.

The Security Centre grades a console served on a workers.dev hostname with a low-severity “Console on your own domain” check, console-on-workers-dev. It is not raised when CONSOLE_ORIGIN is unset, unparseable or not https.

Security. A session that starts with a recovery code does not satisfy step-up. Step-up needs a passkey, either an existing passkey or one enrolled after recovery. Every engine response carries the same hardening headers on every path and status, including cache-control: no-store. /admin/health, /ready and / answer 405 with Allow: GET, HEAD for any method other than GET, HEAD and OPTIONS. Admin write routes check the caller’s access before they read the request body.

A body that is not a JSON object gets 400, and a denied request always writes its audit row. Importing an estate needs both keys.ceremony and access.policy. The engine chooses the store behind a destination endpoint from its host alone, so a query, a fragment or userinfo cannot select a different store.

The sign-in, break-glass bearer and recovery-code limiters key an IPv6 client by its /64 and an IPv4 client by its address. Dependency install scripts are off, and the install guide uses npm ci. Deleting a member’s role, including a SCIM deprovision, deletes their sign-in network prefixes and their identity-provider group record with it. Offboarding a member by address deletes the group record, including for a member who holds access only through an identity-provider group. A group record that is not refreshed for 90 days is deleted.

The parsers that read untrusted input refuse malformed input with their own errors. They refuse a count written with a fraction, an exponent, a sign or a leading zero, or a count key that differs only in case. They refuse a JSON null line in a RUNLOG or a shard.

They also refuse a root or update signature file that is not base64url, and a config snapshot that is not JSON. They refuse a sealed control-plane export with a malformed capsule member, and a control-plane import body with no canonical form. A SCIM PATCH body of JSON null gets 400. An ID token key set without a keys array fails closed. The export secret check is bounded by nesting depth.

Safe-apply. Safe-apply reads the uploaded engine module back from Cloudflare and compares it with the signed digest before it promotes the version. GET /admin/status.artefactSha384 is the SHA-384 of the main module bytes that the engine uploaded.

Console 0.2.9, 2026-10-11

The console component that the signed stable update channel carries beside engine 0.3.8.

Update support. The Updates section of the Licence and updates screen shows an end-of-support notice for the running engine. The notice shows as a warning from 30 days before the engine’s supportedUntil date, and as a danger notice after it. Nothing shows for the latest release, or when the engine sends neither supportedUntil nor supportStatus.

Map and audit. On the map, a destination that is reached on every pass but holds no run reads “no restorable copy yet”. The audit log labels the runlog-resumed action “Backup RUNLOG signature file restored by an Owner”.

Identity providers. On the SAML connection form, the note under Email trust names the routes to the privileged roles. Access admin, Approver and Restore operator need a group mapping, a connection that verifies the email, a passkey or Cloudflare Access. Owner needs one of the last three, because a group cannot confer Owner.

Costs. On the cost estimate, the inputs card and the destination pricing card have the same height in the two-column layout. The offline recoveries field has a full-width row.

Install. Dependency install scripts are off, and the install guide uses npm ci.

Reader 0.3.5, 2026-10-11

The offline reader, the recovery CLI and the library. It reads the archive format line downpipe/0.1.x.

Signature library. The ML-DSA library, filippo.io/mldsa, is at 1.0.0. It reads and writes the ML-DSA-87 key and signature bytes that the format spec defines. The bytes are the same: the frozen ML-DSA-87 key and signature in the conformance tests verify unchanged.

Toolchain. The build and release toolchain is Go 1.26.9. It carries the fixes for ten Go standard library advisories in net/http, crypto/tls, net/textproto and os.

Engine 0.3.7, 2026-10-05

This engine release writes the archive format downpipe/0.1.0. It records the offline-key posture statement key-posture-ack/v3 and refuses v2, so it needs console 0.2.8. The UPGRADING.md file in the engine repository lists what to check before you upgrade.

Keys and custody. The console key install writes the configuration recovery key pair and the configuration wrap key, as npm run deploy does. When the console install puts the wrap key in place, the engine encrypts the credentials that it already stores, and it reports how many. POST /admin/keys/add-config-recovery adds the configuration keys to an engine that has a signer and lacks them. The audit action config-recovery-added records each successful call, with a denied row for a caller without the capability.

The engine counts the configuration recovery key as held only when both halves are set. A re-key or Add configuration keys completes a lone private half with the public half derived from it. Every run’s recovery bundle carries a copy of the current signer’s signer.pub, and reader 0.3.4 can pin that copy with the signer fingerprint on your recovery sheet. A strict break-glass-only choice made in the console stays in place after a later npm run deploy.

A strict break-glass-only re-key deletes the operational pair, as the switch to break-glass only does. While the OPERATIONAL_RETIRED marker is set, the engine seals to the break-glass key alone. It then uses no operational key, even one that is still set.

GET /admin/status reports operationalRetired. The posture check operational-retired-key-held (high) fails while an operational key is set beside the marker. It names the switch to break-glass only as the fix. Adding an operational key deletes the marker first.

Sessions. A native sign-in session (OIDC, OAuth2 or SAML) is controlled by its signed-in subject, not by the email the identity provider asserted. The session list, ending one session, “end my other sessions” and sign-out reach only the caller’s own subject. An Owner ending a member’s sessions by email is unchanged.

Passkey and token sessions still key on email. A session minted before this control existed carries no subject, so it is omitted from a native caller’s list until it expires, at most 12 hours. A session exchanged from the break-glass ADMIN_TOKEN bearer still ends when an operator deletes or rotates that secret.

A native sign-in session also ends when the grant that admitted it is removed. The session checks, on every request, that its subject still holds a role row or a mapped group on its connection. A refused session counts the session-grant-lapsed auth signal, and the support pack counts it too. An OIDC or OAuth2 sign-in ends the session the browser presented, also when the provider sends no iss callback parameter.

Security. A refused sign-in or a refused request no longer pushes real rows off the audit log’s 10,000-row cap. Failed recovery sign-ins, failed passkey, SSO and step-up sign-ins, and refused requests fold into counted rows: the first attempt from a source in a minute writes a row, and a later attempt in that minute adds to a count that the next row for that source carries. A failed or recovery sign-in row carries the count in target.attempts, and a refused request’s row carries it in target.deniedAttempts, so the rows sum to the attempts made.

The refused-request fold has its own ceiling of 9 rows an hour, and the recovery and failed sign-in folds share a separate ceiling of 20. Refusals past the ceiling are written on one denied-overflow row an hour, naming the engine as actor with the count in target.deniedAttempts. The pending counts of all three folds are kept in Durable Object storage, so a restart does not lose a count that is waiting for its row. If a SIEM rule or an alert counts audit rows to count attempts, sum target.attempts and target.deniedAttempts instead: counting rows under-reports a flood from this release on.

The audit log page says which refusals Workers Logs records.

One Owner can no longer remove or demote another Owner alone while two or more Owners exist. The change queues for a second Owner’s approval, whether or not config approval is required, and the Owner being removed may approve it; an access-admin may not. Revoking every sign-in factor of a current Owner is refused while two or more Owners exist: remove the Owner role first, then revoke. The bare-token break-glass is exempt.

A SCIM deprovision of an Owner, while another Owner exists, demotes them to Viewer rather than deleting them. It ends every session they hold at once. The audit log names the SCIM connector as the actor, and the new scim-owner-demoted alert (warning) reaches every rule that selects it or all events. With one Owner, the SCIM deprovision of that Owner is still refused.

Arming a downpipe’s retention enforce flag needs the restore.apply capability, re-checked in the Durable Object. The same applies to lowering or removing a keepRuns or keepDays bound, or unpinning a replica, while enforce is already on. An Operator holds downpipe.write but not restore.apply, so an Operator cannot arm the scheduled retention prune. A narrowing edit, and turning enforce off, still need only downpipe.write.

The edit also needs a second person when one exists: if anyone other than the editor holds restore.approve, the edit queues as a pending config change, and it applies only when a different holder of restore.approve approves it. Until then the scheduled prune reads the stored policy and deletes nothing new. The break-glass admin token still applies at once, and so does an edit where nobody else holds restore.approve; that arm writes a retention-armed-alone audit row and raises the retention-armed-alone alert, at warning.

A restore approval now carries the exact plan its hash binds. This includes the write target, the Cloudflare config account, zone and surfaces, Media and D1 detail, the existing-record choice, and the scope. Before, an approver saw only the write target’s binding name, read as “Original bindings” even when the request redirected the write. The restore request, approve, reject and apply audit rows carry the same detail.

Turning off the restore-approval requirement or the update-approval requirement is no longer immediate for an attributable Owner once two or more Owners exist: the disarm queues for a second Owner, and the policy stays on until they approve it. The bare break-glass token still disarms either at once. Every disarm of either policy now raises the dual-control-disabled alert, naming the policy it turned off.

POST /admin/roles returns no registration link for a member who enrolled before. The engine sends that link to the member’s own address. The response gives inviteState as "emailed-to-member" or "withheld". A passkey enrolled through an invite writes a passkey-invite-enrol audit row.

An S3-compatible destination, BEACON_URL and each call to an OIDC provider pass a send-time screen. The engine resolves the host name over DNS-over-HTTPS and refuses the request when an answer is an internal address. The syslog-over-TLS sink applies the same screen before it connects.

An email that holds ”:” is refused wherever the engine normalises one: a role write, an invite, a sign-in bind, a passkey enrolment and a recovery request. Such an email could otherwise name another person’s role-storage key. An Owner can still remove a stored row whose email holds ”:”, and the Owner guards run on it.

The access endpoint refuses a replayed SAML assertion for as long as the verifier would accept it: the replay marker lives until the assertion’s NotOnOrAfter plus the connection’s clock-skew allowance.

Spending an armed owner-action approval re-checks that the approver still holds the Owner role. It refuses the spend, and records a guard-refused entry in the governance faults of the support pack, when the approver was removed or demoted after approving.

A group-to-role mapping scoped to an identity-provider connection is its own mapping, apart from a global mapping for the same group name. In-console restores, drills and proofs refuse a destination whose run log was put back to an older, validly signed copy. The control-plane import and restore routes apply the per-caller rate limit. HTTP Basic credentials for the ServiceNow channel, and for OAuth 2.0 and OIDC client_secret_basic, are the base64 of the UTF-8 bytes, so a password or client secret with a character above U+00FF works.

RUNLOG verification. Every RUNLOG writer verifies the stored RUNLOG under the engine’s own key before it changes or re-signs it. This covers the seal append, the retention prune, the replication append and the re-key hand-over. The engine never re-signs a RUNLOG that does not verify on its own; runs to that destination fail at the RUNLOG write until an Owner acts. Each RUNLOG write puts its signature at _RECOVERY/RUNLOG.sig.pending first, and an interrupted write resumes from it only when it verifies.

RUNLOG routes. Six Owner-only routes need the keys.ceremony capability: GET /admin/runlog/signature and POST /admin/runlog/resign, GET /admin/runlog/floor and POST /admin/runlog/floor/reset, and GET /admin/runlog/chain and POST /admin/runlog/relink. Each GET route needs no step-up; each POST route does. The notify events runlog-resigned, runlog-floor-reset and runlog-relinked report each attended action, at warning. The audit actions runlog-handover, runlog-resigned, runlog-floor-reset and runlog-relinked use the runlog-signature target kind.

The Keys and break-glass screen’s Posture tab shows “Destination RUNLOG signature” for a destination whose RUNLOG does not verify. It shows the digest of the stored bytes and the signer it checked them against. An Owner re-signs the exact bytes they confirm, with a passkey and a typed confirmation.

A redeployed destination can keep an anti-rollback floor the new bucket cannot satisfy. An Owner resets that floor, under “Anti-rollback floor” on the same tab, down to the highest index the bucket holds now, with a passkey and a typed confirmation; the reset signs nothing. A RUNLOG that verifies can still carry an anomalous chain for one downpipe; an Owner relinks that one chain, under “Chain anomaly”, once, with a passkey and a typed confirmation.

If a downpipe’s RUNLOG chain ever holds two entries with the same index, the engine stops sealing that downpipe to that destination. It cannot repair the chain without rewriting signed history. Attach the downpipe to a fresh destination to keep backing up; earlier runs stay readable from the old destination.

A signer re-key hands each destination’s RUNLOG over from the old signer to the new one. New runs wait up to 15 minutes for the hand-over, and the old public key stays until every destination is handed over. If a RUNLOG does not verify under the old key, the re-key stops with 409 before any secret is written, and names the destination.

Identity. A weakly verified email binds only a Viewer or Operator invite, or a custom role with no people, access or approval permission. A SAML connection with the trust-idp email policy is weak: the identity provider asserts the address with no verified flag. An OAuth2 connection that names an email field with no verified-email field is weak for the same reason.

An Owner, access-admin, approver or restore-operator invite, built-in or custom, stays pending on a weak email. The engine refuses the sign-in when the person holds no other grant. The engine also refuses to create a SAML connection with the require-flag email policy and no verified attribute; the reason names the attribute.

Security fixes. Each OIDC and OAuth2 call on the sign-in path stops after 10 seconds. The recovery sign-in, and the passkey sign-in and enrolment routes, refuse a body over 64 KiB with 413 body_too_large. The SAML ACS POST reads no more than 512 KiB of a body, and a larger body fails the sign-in. A Google connection with hdDomain refuses a sign-in whose hd claim is absent or different, even when the verified email is in that domain.

Preflight destination evidence in the support bundle no longer carries a console-set endpoint or bucket, or the host name of any endpoint. A restore target that is not an object is refused with 400. A target whose binding, namespaceId or bucketName is present but not a non-empty string gets the same 400. The OTLP push stops reading a collector’s response after 16 KiB, and refuses a larger declared Content-Length before it reads.

Fixes. Restores, drills and keyless attestations refuse a signed root whose envelope names another algorithm suite, chunk size or codec, as the Go reader does. A fanned-out seal no longer fails as “fan-out worker stalled” when a duplicate range report arrives while another range reports. A config change that config approval queues no longer sends a success audit line to Workers Logs for its dry run. The authn-failure audit row of a failed passkey sign-in records the CF-Connecting-IP address as sourceIp, as the SSO callback rows do.

Destination removal refuses to drop the last proven copy of a run whose origin was removed. The singular clear of the default destination runs the same check. The destination removal refusals count a run as copied when a destination holds it above its contiguous window, not only inside it. The control-plane-reconciled row that an Owner’s estate import writes records the Owner’s sign-in method, not a token. POST /admin/downpipes/cf-config/mode writes an audit row, and config history records the capture-mode change. A replication fault reads runlog-corrupt when the target RUNLOG fails its integrity check, and copy-failed when one object copy fails, not unreachable.

Step-up. Proposing to turn the restore-approval or the update-approval policy on or off needs step-up, the same as approving a pending change. A stale session gets a step-up challenge, and nothing queues or changes. Arming takes effect at once, after step-up; only disarming with two or more Owners queues for a second Owner.

Release signer. GET /admin/status and the support pack report updateSignerFingerprint, the edmldsa1: fingerprint of the key the engine verifies updates with. When RELEASE_SIGNER_PIN is set, the engine compares it with that fingerprint. A mismatch sets a status warning that names the fix, and fails the new release-signer-pin posture check, at medium. With no pin declared, the check is not raised.

Before you apply. Four API responses change for dual control. POST /admin/downpipes returns 403 when a caller without restore.apply sends an edit that lets the scheduled prune delete more. A bulk upsert returns status: "error" with error: "forbidden" for that item. POST /admin/downpipes returns 202 with a queued config change for that edit when another holder of restore.approve exists; the edit applies after a different holder approves it.

With two or more Owners, an Owner’s request to turn off restore approval or update approval returns 202 with a pending owner action. POST /admin/roles for a member who enrolled before returns no inviteToken: read inviteState and inviteNotice instead. If a script reads these responses, change it before you apply.

Backups and restores. A D1 WITHOUT ROWID table pages by its PRIMARY KEY and can resume mid-table. An AWS S3 bucket with an Object Lock default retention rule works as a destination with no mode set. A D1 backup reads a BLOB column that holds any byte value. The engine records a D1 capture defect by its class. A BLOB that it cannot encode has the class blob-encode, and its run row reads “source read error”.

POST /admin/restore/verify checks the request, including the selector, before the role gate, as POST /admin/restore and POST /admin/restore/request do, so a malformed request is a 400 whatever the caller holds.

A restore over a Secrets Store source no longer records a restore fault for each secret it skips. The support pack’s restoreFaults no longer carries an other row for that skip, and it still records a real fault in the same restore.

A downpipe can name the deploy-time destination (deploy) in its destination list, and the seal, replication, drill, prune and restore paths serve it. The engine serves deploy from the DEST_R2 binding or the DEST_* variables, with or without its stored row, so a downpipe that names deploy keeps its backups whatever the default destination is. The engine refuses to clear the default with POST /admin/destination and {"config": null} while a downpipe names the default. The engine refuses to resolve deploy only when the deployment has no DEST_R2 binding and no DEST_* variables, and the reason is deploy-binding-missing. While the deployment declares either, the support pack does not count a downpipe that names deploy in danglingDestinationRefs.

The Stream backup reads the MP4 download that a video already has, and it never creates one. A Secrets Store backup records the scopes, the comment and the Worker wiring of each secret. Each Cloudflare configuration run carries a coverage record that names each surface the run has no record for, with the reason. The restore dry run reports which live KV keys and R2 objects an apply would replace, and onExisting: "skip" keeps them.

Replication and destination removal. A destination added to a downpipe with two or more destinations receives each run still in the run history, oldest first within that downpipe. Replication copies a run from any destination of its downpipe that already holds it when the run’s origin cannot supply it: the origin no longer exists, its run log will not read, or the run is not in it. A run that no destination holds stays stranded.

The replication pass has its own time allowance and shares the tick’s subrequest budget. Once a destination holds the shared segment store, the pass visits every downpipe in turn each tick, so one downpipe with several destinations does not use the whole tick. A seal raises a destination’s holdsIndex only when no earlier run is missing there.

Both removal refusals count the runs one at a time, so the count falls as replication copies them, and each names the wait and says it can take hours. A refusal states how many of the runs belong to a downpipe with fewer than two destinations, because replication does not copy those. Each refusal gives the count of older runs that are outside the run history. The orphan refusal also states when its count holds. A forced removal that drops runs returns a notice that states how many. When a second owner approves the removal, the approve response carries the same notice.

The singular clear of the default destination carries the same orphan refusal as the plural remove, with no force override. Only a destination still configured counts as holding a proven copy of a run.

Retention. A retention prune that stops partway finishes on a later pass. On a versioned bucket, the prune counts an object behind a delete marker as hidden, not reclaimed. A refused delete counts as a lock only when the object’s lock headers show one. A scheduled prune that changed the archive writes an audit event.

Audit and alerts. The audit log records notification channel and rule changes, each support bundle served to the console, and each vendor pull of the diagnostics bundle. A source attach or detach names the bindings it changed. A time-boxed grant records its expiry, and a new SAML connection records its email-trust policy. The generic webhook body carries dedupKey, and a recovery carries recovered: true. A receiver that rejects unknown keys rejects these bodies, so change such a receiver before you upgrade.

Evidence and posture. The signed evidence pack reads the lock on every destination that an enabled downpipe writes to. A row that needs a retention lock that is not in force reads “not met”. A row that binds a backup administrator also needs compliance mode. A check that cannot fail reads “stated, cannot fail”.

The Security Centre and the signed immutability report read the destination set in the console, so an estate whose destination is set in the console passes destination-configured. The seal-verification check fails while VERIFY_AT_SEAL switches verify-at-seal off. The canary counts its interval from the tick that flew the last flight.

Support pack and updates. The support pack names the classified cause of each ailing canary, and the canary baseline the last update settle compared against. It dates the last admin-route fault, the last refused manual recovery and each posture override. The update-applied and update-rolled-back audit entries name the canary baseline. A siteFaults section names each fault site that recorded a fault, with its fault class, a count, and when it was first and last seen. A replica-target-fault row, and a failed destination row in the replication section, carry nextStep, the next step for its reason.

Deploy. npm run deploy needs Node.js 22.18 or later, or 23.6 or later on the 23 line. The engine ignores CONTROL_PLANE_EXPORT_SEALING_DISABLED, and it seals the configuration export whenever it holds a break-glass or config-recipient public key.

After npm run deploy or an update-channel apply, artefactSha384 in GET /admin/status is the SHA-384 of the main module that the upload sent. artefactSha384Source says where that value came from. The dry-run preview of a two-component update judges the console against the engine that the same apply installs. tools/publish-channel.mjs refuses to sign a channel whose console component has no risk class.

Before npm run deploy changes anything, it checks that node_modules matches package-lock.json. It then typechecks, builds the bundle with a wrangler dry run and runs npm run validate:customer, the checks that guard your data and keys. None of these checks reads whether a sibling checkout is behind its own origin/main, so a release-tag checkout deploys. The script compares the deployed version of the Worker before and after the upload. When the upload did not change it, the script stops with “NOTHING WAS DEPLOYED”.

For a break-glass-only install, the recovery sheet says how the posture was chosen. It names what is off: scheduled restore tests, drills and scheduled retention pruning. It names what still works: an in-console restore or prune with identity.key in the browser.

Configuration export. The signed configuration export carries a destination cost rate that is not a whole number, such as 4.5, as a decimal string. The import reads that string back as the saved value. The engine accepts a downpipe’s cadenceSeconds and restoreTestCadenceSeconds, an STS durationSeconds and a WORM retentionDays only as integers up to 2^53 - 1, because a signed object cannot carry a larger number. An export pass that stops before it writes records the class unsignable or pass-fault. The support pack carries that class, and the environment-self-backup check fails and states the cause.

Known issue. When a destination is added to an estate that already holds data, the first ticks copy the shared segment store. Downpipes then take turns for runs, so a new destination can take several hours to catch up.

Known issue. The Security centre check that sources fan out to two destinations counts the destinations each downpipe writes to, not the copies that exist. It can pass while a destination catches up or while copies to it fail. The destination’s replication count shows the true state.

Known issue. Replication copies runs, not the recovery bundle. A destination that holds only replicated copies has no signer.pub until a run seals there. To recover from it, pass the kit’s signer.pub with --signer.

Console 0.2.8, 2026-10-05

The console component released with engine 0.3.7. It needs engine 0.3.7 or later, because it sends the key-posture-ack/v3 statement.

Setup. Setup at /setup is the first run. It asks one question at a time, in this order: Welcome, Connect, Destination, Protect, Keys, Apply, First backup and Finish. “Your backup plan” lists each decision as you make it, and at Finish it becomes a receipt that you can print. The team invite is at /setup/team. First run and setup explains each step.

Keys. The key ceremony generates the configuration recovery key and the configuration wrap key, and the install writes them to your engine with the signer. The Keys screen offers Add configuration keys for an engine that has a signer and lacks either key. The signer private key installs straight to your engine and does not download. The recovery sheet prints a restore command that pins the signer fingerprint, for use with reader 0.3.4. RECOVER-SPLIT.txt states the share format, so a compatible Shamir tool can recover the key without the console.

Identity providers. The “Add a SAML 2.0 provider” form defaults Email trust to trust-idp. A note under the picker says which roles an emailed invite can receive: on trust-idp, only Viewer, Operator, or a custom role with no people, access or approval permission. Access admin, Approver and Restore operator need a group mapping, a connection that verifies the email, a passkey or Cloudflare Access. Owner needs one of the last three, because a group cannot confer Owner.

Every role, Owner included, binds under require-flag once the verified attribute is set. The form shows an “Email verified attribute” field when you choose require-flag, and you cannot add the connection without it. The SAML guide tells you to keep Email trust on trust-idp unless your IdP sends a verified-email attribute.

Backup RUNLOG. The Posture tab’s “Destination RUNLOG signature” section shows a signer re-key’s RUNLOG hand-over and the hold on new runs. An Owner can re-sign a RUNLOG that does not verify. An Owner can also reset a destination’s “Anti-rollback floor” or relink one downpipe’s “Chain anomaly”. Each action needs a passkey and a typed confirmation.

Provenance. The Licence and updates screen shows the update-signer fingerprint beside the declared release-signer pin, and the engine’s warning when the two do not match.

The identity-provider list shows trust-idp with a neutral badge. A require-flag connection with no verified attribute shows a warning badge that says it admits nobody. The Generic OAuth2 form shows the same role note once you set the Email field and leave the Email-verified field blank.

Restores. The restore plan shows which live KV keys and R2 objects an apply would replace, and “Keep live keys that already exist” keeps them. The run picker lists sealed runs only. The detail drawer of a failed run shows a Support reference to quote to support.

Access and audit. Each role picker offers only the built-in roles that you can grant, and names the others with the reason. The group mapping table names the connection of a scoped mapping, and Remove deletes that row only. The audit log names the notification channel and rule changes and each support bundle downloaded or generated, and its filter has a Notifications group. The To date of the audit log filter includes the day that you pick.

Removing or demoting an Owner while a second Owner exists shows the same pending notice as any other queued change. The row stays in place until a second Owner approves it. The owner-approvals inbox names the two new disarm actions, turning off the restore-approval and the update-approval requirements, in plain words.

The downpipe editor disables Enforce deletion, and the two limit fields once enforcement is on, for a caller without the restore-apply permission, naming the reason. The setup wizard’s pruning presets are withheld from that caller the same way. A restore approval card shows the plan the engine hashed. It shows the write target, with a danger chip on a redirected KV namespace, R2 bucket or cross-account write. The card shows the Cloudflare config account and surfaces, with a chip on a surface outside the default set. It shows any Media and D1 detail, the existing-record choice, the scope and the read source.

The audit log names “Passkey enrolled from an invite” rows in the Identity and sign-in group. A notification rule can select the “Owner demoted by SCIM” alert.

Downpipes list. On a large fleet, the Downpipes list reads run history for the rows on screen first. An enabled downpipe that has run shows Loading in State until the list reads its run history. While a status filter is on, a note counts the downpipes whose run state is still loading. Each one that matches the filter joins it when its run state loads.

Other screens. The Costs screen has an “Offline recoveries per month” input, and its pricing list starts with the destinations that your downpipes write to. Each pull credential has an “Expires after (days, optional)” field.

The canary screen names each destination that one flight does not reach. A refused canary flight or canary change shows the engine’s own reason. The Destinations screen names the lock mode of an enforced Object Lock, and it warns when Object Lock is on and nothing retains the archives. A push that you set up from the Custom endpoint tile stays on that tile.

Reader 0.3.4, 2026-10-05

The offline reader, the recovery CLI and the library. It reads the archive format line downpipe/0.1.x.

Signed report verification. The verify-report command checks the signature on an assurance report that the engine generated. It reads the report JSON and your signer.pub, and it needs no archive and no identity. It makes no network call and is not on the recovery path. The command reference sets out its flags and exit codes.

The signer fingerprint. verify and restore take --signer-fingerprint with the signer fingerprint from your recovery sheet. The reader uses the signer.pub copy in the bucket’s recovery bundle only if its fingerprint matches. After a re-key, a run that the old signer sealed still needs the old signer.pub.

Stricter reads. Every command that opens a run refuses a root manifest whose envelope names a suite or a chunk size other than the pinned values. The reader checks the number form of each segment’s offset and length when it opens a shard manifest. inspect prints the scopes, the comment and the Worker wiring of a secret, and each record’s keyNameHash. It prints a line for every record under the heading records (key hash + identity + restore descriptors):. A script that parses this output must expect the new heading and a line for each record.

Restore metadata. restore --sink file keeps a KV record’s metadata and expiration, and an R2 record’s HTTP and custom metadata, in a sidecar file next to the restored value. The env sink cannot keep metadata, so it prints a warning for each record that carries it. The value itself still restores either way.

Object read caps. Before it checks a signature, the reader caps the root manifest and the RUNLOG at 256 MiB each, and a detached signature at 64 KiB, and refuses an oversized object from its size rather than reading it.

Prune and receipts. prune --apply exits 14 when the pass leaves a planned object in the destination. It counts a refused delete apart from a failed one, and it keeps a superseded run’s tree while one of that run’s segments remains. The prune receipt is version 2. When records fail, a restore receipt records the code that the process exits with.

Provisioner commands. The preflight Secrets Store check reports the free slots and does not fail on a low count. The init and update runbooks say that the engine can store the optional read-only discovery token, and that the engine’s keys are Worker secrets.

Engine 0.3.5, 2026-09-21

The public release workflow built and attested this engine release. It writes the archive format downpipe/0.1.0.

Backup destinations are Cloudflare R2, Amazon S3, Google Cloud Storage and Azure Blob Storage. An Azure Blob destination supports immutable (WORM) retention and the Azure Government and Azure China cloud endpoints. It signs in as a Microsoft Entra service principal or with a SAS credential. The engine refuses a destination endpoint that resolves to a private, loopback or link-local address, and the destination client screens every request against the host it is about to reach. A DEST_ENDPOINT of http://localhost, http://127.0.0.1 or http://[::1] is refused. The syslog-over-TLS audit-export sink refuses a private, loopback or link-local host when you save the destination, and again before it opens a socket.

A self-serve licence binds to the Cloudflare accounts that activate it, up to the band’s estate count. The engine records its own Cloudflare account id after its first verified source attach or update, and at the end of a self-update. The engine status reports that account id, and the console sends it when it activates a licence.

A restore into a Cloudflare D1 database that is refused because the target already holds data writes nothing for that database. The SLA compliance report states the exact time window each row measures. After a recovery-code sign-in, a new passkey enrolment holds the new recovery codes pending until you confirm them in the console. The previous codes keep working until then. The offline reader 0.3.3 ships alongside this release, on its own tool version line.

Console 0.2.6, 2026-09-21

The console component released beside engine 0.3.5. The public release workflow built and attested it.

The Integrations screen shows every SIEM, observability, ITSM and notification destination as a logo tile, and shows which are connected, off or available to add. From the console you configure and monitor SIEM log push (Splunk HEC and other common formats), OTLP metrics push, and pull feeds for Prometheus, Microsoft Sentinel, Cribl and Exabeam.

You can add Azure Blob Storage as a destination, beside Cloudflare R2, Amazon S3 and Google Cloud Storage. Next to a second destination, the default Cloudflare storage from deploy time is labelled as the deploy-time binding and offers no Replace option, because it holds no separate credential. Multi-destination cost estimates add the storage, write and restore or drill costs of every destination in a 3-2-1 setup.

A D1 restore can restore a subset of tables. Media and object restores are queued and tracked per item, with self-identifying labels. For break-glass key recovery, the browser reassembles an M-of-N Shamir key quorum and offers the key for download, with no command line. A licence activates with a short claim code sent by email, which the console exchanges for the signed licence token.

Security Centre, with passkeys, recovery codes and the owner-approval inbox, is available immediately after deploy, before the guided setup finishes. After a forced passkey re-enrolment, the console asks you to confirm that you saved the new recovery codes before it retires the previous codes. Access and security lists every live session for the account, with a Sign out on each. A pasted admin token is exchanged for a minted session and then discarded, so the browser does not keep the token. Every console file picker refuses a file above 64 KiB before it reads the file.

Reader 0.3.3, 2026-09-21

The offline reader, the recovery CLI and the library. The public release workflow built and attested it. It reads the archive format line downpipe/0.1.x.

The offline recovery path. The keygen, inspect, keys, verify, attest, restore, selftest, prune, recombine and unseal-export commands. The recovery path imports no telemetry and no vendor SDK. It calls no vendor service and no Cloudflare API, and it reads only the archive you point it at. A holder of the destination bucket bytes and the offline break-glass key can recover with no vendor in the loop.

Verification and attestation. End-to-end verification against an operator-pinned signer, the recipient set, the key commitment, every shard hash, the declared record count, the Merkle root and each record’s hash, with freshness enforced against the signed run log. A keyless attestation that verifies the root signature, the shard hashes and the run log without the break-glass identity and materialises no plaintext.

Restore targets. A dry run by default that plans the writes and reports conflicts, with --apply to write. The targets are a file per record, dotenv lines to stdout, and a discard sink that decrypts and verifies every record while writing nothing. A restore never overwrites existing target state.

The hybrid envelope. The post-quantum hybrid envelope: X25519 with ML-KEM-1024 for confidentiality with AES-256-GCM in a streamed cipher, and Ed25519 with ML-DSA-87 signatures where both halves are required and there is no downgrade, with HKDF-SHA-384 throughout. The hybrid combiner is pinned by a known-answer vector.

Reading from a bucket. Reading archives from a local directory, an S3-compatible bucket or an Azure Blob container, with every redirect refused. S3 credentials come from the standard AWS environment variables, and Azure credentials from AZURE_STORAGE_KEY or AZURE_STORAGE_SAS_TOKEN. The Azure backend has no delete, so prune --apply refuses against an Azure container.

The provisioner path. The setup, preflight, init and update commands used to stand up or upgrade the engine. The preflight command makes read-only GET-only calls to the Cloudflare API with an operator-supplied token. The setup command is a dry run by default that prints its plan and contacts nothing; under --apply it writes to Cloudflare, creating an Access application and policy and putting Secrets Store entries and Worker bindings. The init and update commands are plan printers that make no network call. None are on the recovery path.

Exit codes and version reporting. The six SPEC-normative exit codes, and codes 7 to 13 outside that set, as set out on the CLI exit codes. The version, --version and -v outputs report the build version, injected at release time and reported as dev for an unstamped local build.

Why the format version and the tool version are kept apart on purpose

The format constants live in a leaf package that imports none of the crypto, reader or command code, so the version-pinned byte rules sit in one place that the writer, the reader and the conformance vectors all share. The engine carries a parallel copy of those same constants, which must match the Go reference byte for byte: a label or a size that diverges between the two would be a silent interoperability break.

Keeping the format version in that leaf, separate from the tool and engine versions, is what lets the tool evolve without touching the format and lets the format move without pinning the tool. It is also why recovery holds across versions: an archive written by any engine version is openable by any reader that accepts the archive’s format identity, with no dependency on matching version numbers between the writer and the reader.

Where this fits

  • The command reference documents the downpipe CLI whose version this page describes, including the version command.
  • The open-core boundary explains which parts are MIT and which are source-available under the Elastic License 2.0, and how the from-source vendored build works as the no-network install path.
  • Applying an update from the channel covers how the engine version is compared against the vendor-signed recommended version.
  • Prove recoverability shows the offline reader recovering an archive independent of the engine version that wrote it.

Last updated .