Skip to content
downpipes docs

Security properties and their limits

This page lists the security properties downpipes has, the limit on each, and the residual risks.

Four properties and their limits

Each row below pairs a stronger property that downpipes does not provide with the property it does provide, and gives the reason.

Not providedWhat holdsWhy
Quantum-proof, or quantum-safePost-quantum hybridThe seal combines a classical algorithm (X25519, Ed25519) with a post-quantum one (ML-KEM-1024, ML-DSA-87), and both halves must hold. Hybrid is a hedge against one half breaking, not a promise about the future.
Tamper-proofTamper-evidentThe audit log is a SHA-384 hash chain. A holder of the underlying storage could re-chain the whole log, but any edit or truncation short of a full re-chain is detectable against an exported head hash. The chain detects a change; it does not prevent one.
Reports are verified in the browserThe console asserts a signature is present and well-formed; full verification is out of bandThe verifying key is not in the browser. Full verification recomputes hashes and checks the hybrid signature against the signer key you pinned, which the engine or the offline reader does, not the console page.
The attestation proves Cloudflare wrote the bytesThe attestation is an engine-side completeness and anti-rollback checkAn attestation confirms the run is complete and that history has not been rolled back behind a high-water mark you keep offline. It is not a check performed by Cloudflare or by the destination store.

Post-quantum hybrid, not quantum-proof

The encryption suite is hybrid by construction. Key encapsulation combines X25519 with ML-KEM-1024 over HKDF-SHA-384, and signatures combine Ed25519 with ML-DSA-87, both halves required with no downgrade path. The point of hybrid is that if either the classical half or the lattice half is broken, the other still holds, so your longest-retention archives are sealed against the harvest-now-decrypt-later window today.

The threat model records one limit here. The post-quantum library is not guaranteed constant-time, which matters wherever a private key is decapsulated in the account. Where an operational key has been added, the in-account engine does decapsulate that long-lived private key, and in more than one place: the sampled-decrypt step of verify-at-seal runs on the production seal path and opens the just-written run with the operational key, the hourly canary opens its own synthetic run the same way, and scheduled drills and in-console restores both load the operational identity to open archives. The narrowing that does hold is that the break-glass private key is never loaded or decapsulated in the account at all, only its public half is wrapped to.

So the strict break-glass-only posture, which is what you get unless you opt into the second recipient, removes the operational decapsulation surface entirely. That posture does not pay for it in coverage, either: verify-at-seal and the hourly canary reach the full keyed tier there by using the run’s own single-run key, which the engine holds while it is sealing that run. Opening a run from its own key performs no key encapsulation at all, so there is no decapsulation for a timing side channel to observe, whereas the two-recipient route decapsulates a long-lived key on every run. No constant-time post-quantum build is in place, so decapsulation with an operational key can be exposed to a timing side channel.

Tamper-evident, not tamper-proof

The audit log and the run log are append-only hash chains. They make a later edit detectable, which is what tamper-evident means. They do not make the storage physically unwritable, which is what tamper-proof would imply. If you want modification prevention rather than modification detection, that lives in the storage layer.

Pair downpipes with a retention lock in compliance mode at a destination that enforces one. That is S3 Object Lock, Google Cloud per-object retention, or a locked Azure version-level immutability policy. Governance mode can be lifted by a principal with the bypass permission, and Cloudflare R2 enforces no lock that downpipes can set or read.

An attestation is an engine-side check, not a Cloudflare-side or store-side one

When a run reports as verified, the engine has re-read the archive and recomputed its hashes, checked the Merkle root and the declared record counts, and verified the hybrid signature against the signer key you pinned at ceremony time rather than any key the archive asserts about itself. A signed run log anchors freshness and linearity against an offline high-water mark, so a destination-bucket attacker who serves you an older validly-signed run is observable to a careful recoverer. None of this is a check that Cloudflare runs or that the destination store runs. It is the engine, and the same checks run independently in the offline reader.

Report verification is out of band

The console can tell you a signature is present and structurally well-formed. It cannot tell you the signature is correct, because the verifying key is not in the browser. Full verification runs in the engine on a drill, or in the offline reader against your destination bytes and your pinned signer, with neither the vendor nor Cloudflare in the loop.

cf-config restore is not one-click

downpipes backs up Cloudflare zone and account configuration through one registry of 313 surfaces. Restore behaviour differs by surface, as set out below.

Of the 313 surfaces, 60 auto-restore in-band: the engine reads your current live configuration, computes a diff against the snapshot, and applies only the fields that differ, item by item, behind a dry-run preview. Those 60 are the only ones a default restore writes back.

A default restore writes none of the other 253 back. That is what a default restore does, and 253 is not the same as the backup-and-preview only count. 228 surfaces have no write path at all: the engine cannot re-apply them by any route, so a restore plan names the tier-specific guidance for applying the change yourself, out of band. They need dependency-ordered creation with id remapping, or carry write-only values such as certificate private keys and service-token secrets a flat replay cannot safely reproduce.

The remaining 25 sit between the two. They carry a writer generated from Cloudflare’s schema. A generated writer fixes the path and the method but not the natural key or which server-computed fields an update refuses, and those two decide whether it corrupts data, so these writers are off by default and never run on a restore that names no scope. To use one, name the surface in the restore request’s surfaces allow-list; the resolved list is hashed into the approval, so the approver sees exactly which surface they are authorising (resolveCfConfigSurfaces, engine/src/admin/approvals.ts). A named surface gets a live diff preview and re-applies in band, behind a signed plan.

Restore behaviourSurface countWhat the engine does
Auto-restore in-band60Reads live, diffs against the snapshot, applies only changed fields item by item, additive by default, behind a dry-run preview
Off-by-default writer, opt-in25Nothing on a default restore. Reached only when the request names the surface, which binds it into the approver’s plan hash; then it diffs and applies like an in-band surface
Backup-and-preview only228Captures the surface and verifies it is recoverable; a restore plan names the tier-specific guidance (dependency order, or a re-provision checklist), not a diff, and you apply the change out of band

Those three account for the whole registry: 60 plus 25 plus 228 is 313. Separately, 85 surfaces carry a writer, which is the first two rows added together. The default-restore figure does not belong in the last row of that table, because the opt-in surfaces would then be counted twice.

A different axis you may see in the code

The in-band surfaces are the ones whose diff-driven writer is on by default. That is not the same axis as the internal restore-tier label, where the registry splits into idempotent (176), ordered (76) and reprovision (61). The idempotent count of 176 is how cleanly a surface could replay in principle, not how many auto-restore. 60 surfaces restore in-band and 228 are backup-and-preview only. Reading the idempotent count as the in-band count would nearly treble the figure for what downpipes writes back.

Two further points apply. The auto-restore path runs from the console, behind the dry-run preview and, when turned on, dual control. And the account scope is guarded rather than left to the operator: the engine refuses an apply whose target account is not provably the archive’s signed origin until the operator types that account id back.

Coverage shows unknown as unknown, never as green

The configuration backup adapter is fail-open per surface. A surface the token cannot read, a plan-excluded product, or a deprecated endpoint becomes an explicit unavailable marker, not a fatal error or silent success. When the size of a surface has not been measured yet, the relevant scorer stays inert rather than reporting a number it does not have. The principle the portal follows is that an unknown state is shown as unknown. It is never coloured green to imply a coverage or a verification that has not happened.

The default-posture operational-key residual

The key ceremony produces up to three archive keypairs, and the engine loads them with deliberate asymmetry. The break-glass private key is never in the account; the engine wraps to its public half and can never unwrap it. The signer private key is in the account, because it has to sign each run. The operational recipient is optional. The ceremony also makes two configuration keys, and neither can read an archive. One opens the engine’s own configuration export, and the other wraps stored destination credentials.

In the two-recipient posture the engine also holds an operational key that can decrypt, to support in-account drills and read-back. The consequence, recorded in the threat model, is that a full compromise of your own Cloudflare account, plus the destination bytes, can read past archives through that operational path. The break-glass key cannot be reached this way, because its private half is offline.

The strict break-glass-only posture omits the operational recipient, and it is what an account gets unless someone deliberately adds a second recipient. A full account compromise then yields ciphertext and no key that opens it.

The cost that posture carries is narrow: your engine still verifies every run as it seals it and still flies its hourly canary, because both use that run’s own single-run key rather than a stored one. What it cannot do is reopen a run it sealed earlier without you. The unattended work is what stops: scheduled restore tests, the automated drill, and in-account retention pruning, each of which needs a key at a moment when nobody is there to supply one. An in-console restore still works, because you are there to supply the key: the console’s break-glass panel takes your break-glass private in the browser, derives that run’s key there, and wipes it when the restore finishes. Recovery does not move offline; the unattended proof does, and you prove past runs at an attended verification instead.

PostureWhat the engine can decryptWhat a full account compromise readsWhat you give up
Two recipients (opt-in)Past archives, via the operational keyPast archives, when that key is presentNothing operationally
Break-glass-onlyNothing at restCiphertext only, no usable keyReopening a run it sealed earlier without you: scheduled restore tests, the automated drill and in-account retention pruning. An attended in-console restore still works

Recovering downpipes itself is in-account, and some state re-establishes rather than restores

The engine writes a signed, no-custody export of its own control plane to the destination bucket, letting the console rebuild a wiped scheduler from it. Two limits apply. It is in-account recovery for configuration, not an off-account integrity anchor: an attacker who holds your account and its signer key could write a fresh export, so the export proves authenticity against your pinned key, not freshness after a total wipe. And it recovers configuration, not everything. The downpipe and destination configuration, the role table and the owner-governed policy return; identity-provider connections, notification routing, sessions, passkeys and the audit-log body are re-established after the rebuild rather than restored, and account-held secrets ride only as wrapped envelopes or as markers that they must be re-entered.

The auto-heal that resumes backups after a wiped scheduler depends on a destination it can find, which means a destination declared at deploy time rather than only set in the console, because a console-only destination lived in the state that was wiped. A full account loss is recovered by rebuilding the engine and re-entering that configuration, with your data restorable from the bucket throughout. Recovery time is dominated by standing up a fresh Cloudflare account, not by downpipes. The end-to-end steps and their prerequisites are in recovering downpipes itself.

Assurance and compliance: what downpipes does not hold

downpipes holds no SOC 2 attestation and no ISO 27001 certification. Both are demand-gated, which means they are pursued only when a named deal requires one, because a self-hosted product that holds none of your data and none of your tokens has no vendor-side system to attest in the usual sense. The compliance pages are a mapping of the product against a control set, not a certificate. No product makes you compliant.

The security posture that does have evidence is the identity component, which carries a single OWASP ASVS 5.0 Level 2 / Level 3-equivalent self-assessment (for an authentication component). The cited evidence is verified against the production code path. It is a self-assessment, not an external audit.

There is no live assurance dashboard and no live billing surface in the product. The restore screen’s Browse by date calendar lists a downpipe’s retained runs by day from a bounded run history, and choosing one picks that run by id, never an arbitrary instant.

The audit and SIEM feed carries operator identity, including actor email, source IP, role and approver emails. It is not free of personal information.

The offline reader and the archive format

The recovery path is an MIT-licensed Go reader. The reader, the format specification and the conformance vectors are MIT and open source; the platform around them is source-available under the Elastic License 2.0 and is not open source. The open-core boundary sets out which part is which.

The module is github.com/downpipes-io/downpipe. go install github.com/downpipes-io/downpipe/cmd/downpipe@latest resolves against the public module proxy, and a plain clone of the repository also works. That said, a real recovery should still use a copy built from source held in advance, since a break-glass procedure should not depend on the module proxy or GitHub being reachable at the time. A plain build stamps its version string as dev, since it does not run the maintainer’s release pipeline. Confirm the binary with downpipe --help before citing it for a real recovery. The archive format is downpipe/0.1.0, a versioned constant that appears byte-identically in every archive, so the bytes plus your offline key recover with a conformant reader regardless of the engine version.

Building that copy has two halves, and only one of them is unconditional. Every dependency is vendored, so go build -mod=vendor downloads no module, and that half holds anywhere. The network half does not.

go.mod pins toolchain go1.26.6, deliberately, so that a stale local Go cannot build a reader carrying fixed vulnerabilities, and under Go’s default GOTOOLCHAIN=auto a machine whose own Go is older fetches that pinned toolchain from the module proxy before it compiles anything. On a machine that has never held it, the vendored build fails outright rather than falling back. So the build needs no network only when the machine’s own Go already satisfies the pinned toolchain version. A machine that has built the reader before already holds the toolchain; a recovery machine usually does not, and a toolchain download is hard to debug mid-incident.

The format is versioned by semver; it is not frozen. While the major is 0, a change to a byte-level rule bumps the minor and is a new format identity rather than an erratum against the existing one, and a patch changes no byte-level rule. A reader therefore opens the downpipe/0.1.x line, not any archive whatever its version.

Where this fits

The pages below either set the context for these properties or carry the underlying detail.

Last updated .