Skip to content
downpipes docs

The key ceremony and your recovery kit

Before downpipes can seal a single backup, it needs a set of keys. The key ceremony is the one-time act that mints them. It runs once, produces the key material the engine and the offline reader both consume, and hands you a recovery kit whose most important file is yours to take offline and keep.

This page is for a self-hoster who wants to understand the key model conceptually: what each key is for, where the ceremony runs, why the material is identical whichever way you run it, and what your standing duty is afterwards. The click-by-click setup lives in the setup guide; this is the why behind it.

Three keypairs, three jobs

The ceremony produces three keypairs. They are deliberately separated by capability, so the one key that can decrypt your data is never the key the engine holds to do its day job.

Keypair Private half lives What it can do What it cannot do
Break-glass Offline, with you The only universal recovery key; opens every archive this engine ever writes Nothing in the running engine, because the engine never holds it
Signer In your engine Signs every run and the run index, so a reader can prove a run is genuine Decrypt anything; it is a signing key, not a recipient key
Operational (optional) In your engine Reads archives back in place, which powers the unattended restore test Recover anything once it is removed; it is the deliberate opt-out for strict custody

The break-glass keypair is the heart of the model. Its public half goes to the engine so each run can be wrapped to it. Its private half, the part that can actually decrypt, is generated on your side and never sent anywhere. That single property is what lets you recover without Cloudflare and without the vendor.

The signer is the one private key the engine necessarily holds, because it must sign every run. A reader pins your signer’s public half and checks each run’s hybrid signature against it, so a run that was not signed by your signer is rejected. The signer cannot open an archive; signing and decryption are different keys on purpose.

The operational keypair is optional and is the default. Both of its halves go to the engine. Its private half is what lets the engine open one of its own runs in place to prove the backup restores, without a person present. Whether you keep it is your recovery posture, covered below.

Where the ceremony runs

You can run the ceremony in either of two places, and the choice does not change the key material at all.

In the browser, on the console, the ceremony generates everything client-side from the platform random source and shows you the exact commands to install the engine-side secrets. The break-glass private half is downloaded to your machine and is never transmitted to the engine or the vendor; there is no field and no upload path for it.

At deploy time, on the operator’s own machine, the deploy script generates the same set when the engine has no signer secret yet. Here the break-glass private key is born on the machine you are deploying from and never transits a browser tab, the vendor or the engine at all.

The output is byte-identical either way

Both paths produce the same labelled key files in the same byte formats, because they share the same generation code (makeRecipient and makeSigner in console/src/keygen.ts, mirrored in engine/scripts/generate-keys.ts). A break-glass identity made in the browser and one made by the deploy script are interchangeable, and either is read as-is by the engine and by the offline downpipe CLI.

How one offline key recovers every archive

Each backup run mints a fresh 256-bit master key, the root from which every file key in that run is derived. That master is never stored. Instead it is wrapped once per recipient into a master capsule, by a hybrid encapsulation that takes a recipient’s public key and produces a sealed copy only the matching private key can open (sealToRecipients, engine/src/crypto/capsule.ts).

The break-glass recipient is always one of those recipients, and it is listed first (loadRecipients, engine/src/keys-env.ts). So no matter how many other recipients a run is wrapped to, the offline break-glass identity alone can open the capsule, recover the master and from it derive the keys for every manifest and data segment. No other key and no vendor step is involved.

This is why the posture choice is purely about what the engine can read, never about whether you can recover. In the two-recipient posture a run is wrapped to both the operational recipient and the break-glass recipient, and either opens it. In strict break-glass-only it is wrapped to the break-glass recipient alone, so even a full compromise of your Cloudflare account yields no key that decrypts the data. The full trade-off is in recovery postures.

What the recovery kit contains

The ceremony writes a small kit. Two of its files are must-keeps, and they are must-keeps for different reasons: identity.key is the only thing that can unwrap an archive, and signer.pub is the only thing that can verify one. The offline reader hard-requires both, so keeping one without the other is not a degraded recovery, it is no recovery.

File What it is Where it belongs
identity.key The break-glass PRIVATE key, the only universal unwrap Offline, removed from the machine that made it. Keep it
recovery-sheet.txt The printable sheet: public fingerprints, the posture, a custody sign-off line, and a blank anti-rollback line to write the latest trusted run-index into Printed and stored with the key, or in your password manager
recipient.pub The break-glass PUBLIC key Safe to keep anywhere; the engine already has it
signer.pub The signer PUBLIC key, which the reader pins to verify runs Keep it, beside the sheet is fine. It is a public key and decrypts nothing, so it needs no protection of its own, but downpipe verify and downpipe restore both refuse to run without it

The recovery sheet is written to be honest about custody. It records exactly where each key went: the signer private, the break-glass public and the operational pair to your engine, and to the vendor, in its own words, nothing. It also carries an anti-rollback line: a blank field where you write the latest RUNLOG index you trust, updating it after each run, and pass to the offline reader as its --min-runlog-index so a rollback to an older validly-signed run is caught rather than silently accepted.

The sheet and the private key alone will not recover an archive

The printed sheet carries fingerprints, not key material, so it is not a source for signer.pub, and neither is the archive: the recovery bundle written to your bucket holds documentation only, and the signed root manifest records the signing key’s fingerprint rather than the key. downpipe verify and downpipe restore both take --signer as a required flag and exit on usage without it (cmd/downpipe/verify.go, cmd/downpipe/restore.go), and --allow-unverified does not waive it: it downgrades a bad signature to a warning, and a missing flag is not a bad signature. So a custodian holding identity.key and the sheet, and nothing else, cannot restore. Keep signer.pub with them.

If you lose the break-glass key, nobody can recover your archives for you

There is no copy of identity.key on any server and no vendor recovery path, so a lost break-glass key is irrecoverable by design, which is the price of no-custody. If you have already lost one, read break-glass recovery for what a surviving key can still open, and rotating your keys for how to re-key what comes next; an archive sealed under a key you no longer hold cannot be reopened by a new one. To make sure a single loss is never fatal, the recovery sheet suggests splitting the identity across several offline holders, an M-of-N custodian split, so that no single person holds the whole key. You set N, the number of shares, anywhere from 2 to 16, and M, the number of them needed to reconstruct the key, from 2 up to N.

Your standing duty after the ceremony

The ceremony hands you one job that no automated step can do for you: take identity.key off the machine that generated it and store it offline, and keep signer.pub where you can find it again. An encrypted USB drive with a printed companion, a corporate password manager, or a custodian split all work for the private key; the recovery sheet has a line to record which you chose and who holds it. signer.pub needs no such care, only survival, so the simplest thing is to keep it with the sheet.

When you record that choice, the console asks for each holder’s name or role and the date they confirmed they hold the copy, one row for a single custodian or one row per share of an M-of-N split. Those entries are public metadata written onto the recovery sheet, never the key or a share value, so they are safe to fill in plainly, and the date is prefilled to today while staying editable. Leaving both fields blank records no sign-off line. Re-download the recovery sheet after you enter them so it captures who holds what.

Two habits make the key worth having. Retain old keys after a rotation, because an archive sealed before a rotation is recoverable only by the key that sealed it. And exercise recovery before you need it, because a key you have never tested is a promise, not a capability. A drill that uses the operational key proves the engine can read its own archives, but it does not exercise the offline break-glass path; a genuine break-glass drill recovers a sampled run with identity.key on an isolated machine. See prove recoverability for that drill.

A note on the signer’s seed form

One detail surprises people who look closely. The signer private is stored as a compact 64-byte value, an Ed25519 seed plus an ML-DSA-87 seed, not the much larger expanded ML-DSA secret.

The reason is purely practical. The expanded ML-DSA-87 secret is roughly 4896 bytes, too large for a Cloudflare text binding’s limit, while the 64-byte seed form stays around 86 base64 characters. The engine expands the seed deterministically back into the full signing key at load time (loadSigner, engine/src/keys-env.ts). The compact form is the same signing key by another representation, and it still cannot decrypt anything; it only signs.

Where this fits

These pages take you from understanding the keys to choosing, using and rotating them.

The key formats, in bytes

For readers who want the exact on-disk shapes the engine and the offline CLI agree on:

The break-glass and operational recipients share one format. The public half is an X25519 public key (32 bytes) followed by an ML-KEM-1024 encapsulation key (1568 bytes), 1600 bytes in total. The private half is an X25519 scalar (32 bytes) followed by an ML-KEM-1024 seed (64 bytes), 96 bytes in total, which is what identity.key carries. The reader parses exactly 96 bytes for an identity (parseIdentity, engine/src/crypto/keys.ts).

The signer’s public half is an Ed25519 public key (32 bytes) followed by an ML-DSA-87 public key (2592 bytes). Its private half is the 64-byte seed pair described above.

Each file is a single labelled line, a label then a base64url value: downpipe-identity-v1 for the break-glass private key, downpipe-recipient-v1 for a recipient public key, and downpipe-signer-public-v1 for the signer public key (engine/src/crypto/keys.ts). A public fingerprint is a SHA-384 over the public encoding, prefixed dpr1: for a recipient and edmldsa1: for the signer, which is what the recovery sheet records so you can confirm a key’s identity without exposing it.

Last updated .