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 key formats are 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 for your archives, plus two configuration keys set out below. The archive keypairs 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, opt-in) | In your engine | Reads archives back in place, which powers the unattended restore test | Recover anything once it is removed; adding it is the deliberate opt-out of 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 a private key the engine must hold, because it signs 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 not the default. Strict custody, where your engine holds no key that can read an archive, is what the ceremony offers you first. In setup’s Keys step, the offline-key-only choice comes first and neither choice is pre-selected (POSTURES, console/src/lib/setup-flow/inventory.ts). On the Keys screen, the strict-custody checkbox is ticked when the screen loads (strictCustodyDisclosure, console/src/screens/keys/posture.ts). The operational key is what you opt into rather than what you must know to decline.
If you do take it, both of its halves go to the engine, and 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. Which you choose is your recovery posture, covered below, and it is reversible in the direction that matters: adding an operational key later is a targeted install, not a re-key.
The ceremony also makes two configuration keys, in both postures, and both install straight to the engine. The configuration recovery keypair (CONFIG_RECIPIENT_PUBLIC and CONFIG_RECIPIENT_PRIVATE) opens only the engine’s own sealed configuration export. The configuration wrap key (CONFIG_WRAP_KEY) is a 32-byte AES-256 key that encrypts the credentials the console stores. Setup saves the read-only token and the destination key before the keys exist. When the console’s key install puts the wrap key in place, the engine encrypts those credentials too (rewrapAfterKeyInstall, engine/src/admin/config-rewrap.ts). Neither key opens an archive, and neither is in your kit (runKeyCeremony, console/src/keygen.ts).
Where the ceremony runs
You can run the ceremony in either of two places, and the choice does not change the key formats at all.
In the browser, on the console, the ceremony generates everything in your tab from the platform random source. It then installs the engine-side secrets for you. There is no terminal step.
In setup, the Keys step makes the keys and the Apply step installs them. You paste one narrowly scoped deploy token at Apply. The console posts it together with the in-memory key material to the engine, and the engine writes its own secrets with it (POST /admin/keys/install, engine/src/admin/router-keys.ts; installCeremonyKeys, console/src/lib/setup-flow/install.ts). The same token then attaches the sources you picked, and the token is never stored and never logged.
Apply also offers your own wrangler or CI as a peer route: it lists the equivalent wrangler secret put commands for operators who manage the engine as infrastructure-as-code (selfDeployPlan, console/src/lib/setup-flow/actions-apply.ts). The Keys screen keeps its own ceremony, with the token on the same screen, for rotation and re-keying.
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, on either route.
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 deploy from, and never transits a browser tab, the vendor or the engine.
The key formats are identical either way
Both paths produce the same labelled key files in the same byte formats, because they use mirrored 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. It is never stored in an archive in the clear. Instead it is wrapped once per recipient into a master capsule. A hybrid encapsulation takes the recipient’s public key and produces a sealed copy only the matching private key can open (sealToRecipients, engine/src/crypto/capsule.ts). A run too large for a single Worker invocation also keeps its master wrapped in its own checkpoint until it completes, so a later invocation can derive the same keys; that is engine-internal, bounded to one run and deleted at completion, and it is set out in full on cryptography.
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 a full compromise of your Cloudflare account yields no key that decrypts a sealed archive. One exception: an attacker holding both your Durable Object storage and SIGNER_PRIVATE can recover the master of a run that is still in flight at that moment, because a run too large for one invocation keeps its master wrapped in its checkpoint. That is bounded to runs in progress, and an attacker holding the signing key can already forge archives outright. The full trade-off is in recovery postures, and the wrap itself is on cryptography.
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. signer.pub is the key the offline reader pins to verify one, and the reader refuses to run without a pinned signer.
From engine 0.3.6, the recovery bundle in your bucket also holds a copy of the current signer’s signer.pub. From reader 0.3.4, the signer fingerprint on your printed sheet can pin that copy instead. Every run overwrites the copy. A bucket that no engine 0.3.6 has written to has no copy. After a re-key the copy is the new signer’s, so it cannot verify a run that the old signer sealed. In those cases identity.key without signer.pub 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 section (the console adds it when you record a custody choice), 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 |
operational.pub | The operational PUBLIC key. The console downloads it only when you choose an operational 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. downpipe verify and downpipe restore refuse to run without a pinned signer. The bucket copy is only ever the current signer’s key, so keep every signer.pub, including the old one after a re-key |
The recovery sheet records custody. The deploy script’s sheet records where each key went: the signer private, the break-glass public, the operational pair if you chose one, the configuration recovery keypair (named on the sheet from engine 0.3.6) and the configuration wrap key to your engine, and to the vendor, in its own words, nothing. The console’s sheet states that identity.key was never sent to the engine or the vendor, and that the configuration keys install to your engine. 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 do not recover every run
The printed sheet carries fingerprints, not key material, so it is not a source for signer.pub. downpipe verify and downpipe restore refuse to run without a pinned signer and exit on usage (cmd/downpipe/signer_source.go). --allow-unverified does not waive that: it downgrades a bad signature to a warning, and a missing signer is not a bad signature.
From engine 0.3.6, the recovery bundle in your bucket also holds signer.pub. From reader 0.3.4, --signer-fingerprint takes the signer fingerprint from your sheet. The reader uses the bundled copy only if its fingerprint matches, so the trust anchor stays your offline sheet. A copy that someone replaced fails the check (exit 2). For a run your current signer sealed, in a bucket an engine 0.3.6 has written to, identity.key and the sheet are enough.
Every run overwrites the bundled copy, so it is always the current signer’s key. Only a run by engine 0.3.6 or later writes it. After a re-key, a run that the old signer sealed needs the old signer.pub. A custodian who holds only identity.key and the sheet cannot restore those runs. Keep every 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. Each entry is 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.
A sign-off needs a holder. From console 0.2.7, a blank holder records no sign-off, even when the date is filled in, so the sheet never dates a hand-over to nobody. On an M-of-N split, a share with no holder keeps its line on the sheet with no date. The sheet prints blank lines to fill in by hand wherever no holder was recorded. 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 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 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.
- To choose between the two postures, read recovery postures.
- For what to expect when you rotate the break-glass key, add an operational key, or re-key completely, read rotating your keys.
- To see what the engine can and cannot read, and the vendor’s empty hands, read the no-custody trust model.
- To prove the offline path works, follow prove recoverability and the restore flow.
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. The recovery sheet records the fingerprint so you can confirm a key’s identity without exposing it.
Last updated .