Skip to content
downpipes docs

What one canary flight checks: the eight aspects

Each canary run checks eight deterministic aspects, in order: write-probe, delete-probe, seal, read-signature, runlog-freshness, decrypt-integrity, restore, and restore-verify. A flight stops at the first death, so the aspect that died shows where the destination’s path broke. The canary itself, the coalmine metaphor, the five liveness states and how to read the screen are covered on the canary; this page assumes you have read that and goes one level down, into what a pass of each aspect proves.

It is written for a self-hoster or an auditor who wants to know what evidence a green flight assembles, why the known-answer comparison is byte-exact, and why some flights report ailing rather than dead.

The eight aspects, in order

A flight runs a closed, fixed set of eight aspects against one destination, always in the same order, because each later aspect depends on the earlier ones succeeding. The set is the CanaryAspectKey union in types.ts, and the order is the order they are attempted in cycle.ts. A passing aspect is proven. A failing aspect is a death (a bit strayed where it must not). The exception is a failed write-probe or seal, which ends the flight ailing. An aspect that could not be attempted is skipped, and a note is an informational observation that is not a death.

#AspectWhat a pass proves
1write-probeThe destination is reachable, the credentials are valid, and write permission holds. A probe object is written and read back byte-exact through the real destination.
2delete-probeDelete permission holds against the probe object. A refusal is a note, never a death, because an immutable bucket legitimately keeps the prune out.
3sealThe known corpus sealed to the destination as a real archive write through the identical seal pipeline, producing the manifests and a RUNLOG entry.
4read-signatureOn read-back, the root and shard manifest signatures verify, so the tamper-evidence holds on the archive the flight just wrote.
5runlog-freshnessThe RUNLOG entry is present, chained, and reads as the latest run in its namespace.
6decrypt-integrityEvery record decrypts, re-passes its own plaintext hash, and matches the known corpus byte-for-byte.
7restoreThe decrypted known data is written back into the isolated canary cell through the destination. This is a plain write, not the engine’s restore module.
8restore-verifyThe restored cell is read back and byte-compared to the known corpus, so the round trip is whole.

The order matters for reading a result. A flight stops at the first death, so a death on aspect 6 tells you aspects 1 to 5 passed: the destination took the write, sealed a real archive, and read it back with its signatures and RUNLOG intact, but the decrypted bytes did not match the known data. The aspect that died shows where the path broke.

Why each aspect is separate

The aspects are not redundant; each one isolates a distinct failure. The write and delete probes test the destination’s permissions before any real archive exists, so a credential or a bucket-policy problem surfaces as a probe result rather than a confusing seal error. The seal exercises the real write pipeline. The read-signature and runlog-freshness aspects prove the archive verifies and sits correctly in its chain. The decrypt-integrity aspect is the byte-exact known-answer compare, the load-bearing one. The two restore aspects write the decrypted bytes back to the destination and then re-confirm them after that round trip.

The What the last flight proved panel on the console's Canary screen. A destination named Default destination is badged Alive, with the age of its last flight beside it. Beneath it the eight aspects are listed in order, each with a Pass badge and a plain result: Can write, the destination accepted and returned a probe object; Can delete, the destination allowed the probe object to be deleted; Sealed, sealed 7 records at 1580 bytes to the canary cell; Signatures verify, the root and shard manifest signatures verified and the operational recipient wrap opened; RUNLOG fresh, the RUNLOG entry verified fresh and chained; Bytes intact, 7 records decrypted and matched the known corpus byte for byte; Restored, 7 records restored over the isolated canary cell; and Restore verified, every restored byte matched the known corpus exactly.

The known corpus is deterministic

The data a flight seals is the known corpus in corpus.ts. It is not random and it does not read a clock. It is generated in code from constant seeds, so the bytes are identical on every flight and on every deployment. Because the known answer is fixed, a deviation of even one bit is detectable.

The corpus is exactly seven records, chosen to span the byte shapes that catch real corruption.

RecordShapeWhat it catches
canary/asciiA plain ASCII sentenceA basic round-trip and text mangling
canary/utf8Multibyte UTF-8 with accented Latin, an emoji and a CJK ideographAn encoding-layer mangle of multibyte characters
canary/all-bytesEvery byte value from 0x00 to 0xFFA byte that a transport or codec cannot carry cleanly
canary/zerosA run of all-zero bytesPadding or truncation that hides in zeros
canary/onesA run of all-0xFF bytesA stuck or flipped high bit across a block
canary/jsonA small structured JSON objectA serialisation or framing error
canary/block-1kA kilobyte from a deterministic generatorA silent transposition or truncation anywhere in a larger block

The kilobyte block is deterministic, not cryptographic

The kilobyte record is filled by a tiny deterministic generator seeded from a constant. It is explicitly not a cryptographic source of randomness. Its only job is to be non-trivial yet reproducible, so a quiet truncation, padding or transposition in the archive path shows up as a byte delta rather than hiding in a block of zeros.

The known bytes live only in the engine code. They are never shipped to the browser and never sent to the vendor. The console sees liveness and counts, not the corpus. The counts include the record count and byte total that the seal aspect reports.

Integrity is a byte-exact known-answer compare

Aspect 6, decrypt-integrity, is where the canary earns its keep. After the read-back verifies the signatures and the freshness, the flight decrypts every record and compares it to the known corpus. There are two layers of checking here, and the second is the stronger one.

The first layer is the archive’s own integrity. The reader in reader.ts recomputes each record’s plaintext SHA-384 as it reassembles it, so a record that decrypts to anything other than what was sealed throws. That catches a flipped bit in a stored segment, a tampered manifest, or a corrupted RUNLOG, each of which maps to the right dead aspect.

The second layer is the known-answer compare. Even an archive that is internally self-consistent can still hold the wrong data. So the flight matches each decrypted record against the known corpus by exact bytes. The match is by value, not by name, so it is order-independent. A known record that never appears, or an unexpected record that appears, both count as a drift. The aspect detail then reports how many records diverged and how many bytes strayed.

Why the known-answer compare is the strongest check

A canary that trusted only the archive’s own hashes would call an internally-valid archive of the wrong data alive. An archive sealed with one byte of one record changed still passes its own internal hashes, and the byte-exact compare against the known data calls it dead. That is the difference between proving an archive is self-consistent and proving it holds exactly the data that was sent.

Isolation: a pure key-rewrite namespace

A flight must exercise the real seal and read code, but it must never collide with a real archive. That is what PrefixedDestination in prefixed-dest.ts does. It wraps a real destination and prepends a fixed prefix, _CANARY/runs/<runId>/, to every put, get, conditional-put, delete and list. The seal pipeline and the reader address objects by their ordinary logical keys, and through this wrapper those keys land under the canary prefix instead.

The wrapper is a pure key rewrite. It adds no behaviour of its own, so the canary runs the identical code paths a real backup uses, only namespaced. So a flight tests the real path, not a stand-in. Because every object a flight writes lives under its own run prefix, a whole flight’s residue is one prefix to delete, and it can never land in the same place as a customer archive (which lives under different prefixes). The restore aspects write back into that same isolated cell, never a real or production binding.

Deeper detail: object-lock status forwards unchanged, and the read-side store

Object-Lock configuration is a property of the whole bucket, not of a key prefix, so the wrapper forwards an object-lock-status probe to the inner destination unchanged. When the inner destination cannot run the probe, the wrapped destination reports “unknown” rather than a verdict. The wrapper’s list also strips the namespace prefix back off the returned keys, so the caller sees logical keys, mirroring how the prune lists run trees. On the read side, the canary adapts the destination into the reader’s object store with the same prefix; a missing object throws, so an absent canary object fails the read-back loudly rather than verifying nothing.

Ailing is not death, and a delete refusal is a note

The aspect outcomes carry a deliberate distinction the rest of the system depends on. A death (fail) is detected drift on the known answer. Ailing is a fault that stopped the check completing, and it is reported as a status, not as a death. A failed write-probe or seal aspect still shows as a fail on an ailing flight.

A configured destination that cannot be read at all reports pending rather than crying wolf before setup. A write probe that fails halts the flight as ailing, because an unreachable path is not proven drifted. A delete that an immutable bucket refuses is recorded as a note on aspect 2, never a failure, because lacking the delete capability on an immutable destination is a posture observation, not a dead canary. A signature, freshness or integrity failure on the canary’s own freshly-written archive, by contrast, is a real drift and is reported dead.

From engine 0.3.6, a RUNLOG or RUNLOG.sig whose signature does not verify on the canary’s own cell is a death on runlog-freshness. The canary wrote and signed that RUNLOG seconds before, so a failed signature means the bytes changed at the destination. A RUNLOG check that could not run for another reason, such as an absent RUNLOG, reads ailing on runlog-freshness.

A delete refusal is informational, never a failure

The delete-probe is a coarse observation. A refused delete only suggests immutability; it is noted, not failed. Immutability is reported by the posture immutability check, fed by a real capability probe of the bucket’s Object-Lock configuration. The posture does not infer immutability from this delete behaviour. See immutability and attestation for that check.

Every posture runs all eight aspects

The read aspects need a key that opens the canary’s own cell. Where an operational key is present the flight opens through the recipient capsule, so the wrap decapsulation is exercised on every flight. Where the engine holds no in-account read-back key, the flight opens its own cell with the per-run master it just sealed under, so the full eight-aspect cycle runs and reports alive on a clean flight.

Neither path exercises the break-glass wrap. The engine has never held that private key and cannot, so no running engine proves the offline key opens what was sealed to it. That property is proven by attended verification, where you supply the key yourself; for the recovery it underwrites, see break-glass and offline recovery.

Which aspect each fault kills

Each fault maps to one dead aspect. A flipped data bit kills decrypt-integrity, a tampered manifest kills read-signature, a corrupted RUNLOG or RUNLOG.sig kills runlog-freshness, and an internally-valid archive of the wrong data fails the known-answer compare. A deleted RUNLOG reads ailing, not dead. A clean flight matches every known record byte-for-byte and reads alive.

The “Preview a death” button on the canary screen is a pure client-side simulation that never calls the engine, so do not read it as a real flight result.

Where this fits

For the canary concept, the five liveness states and how to read the screen, start with the canary. For configuring it, choosing its destinations, the cadence and the alert behaviour, read configure and operate the canary.

The integrity chain a flight reads back (the signatures and the RUNLOG) is described in verify at seal, and the underlying signing and encryption in cryptography. For the keyed drill that proves a real backup is recoverable, which the canary deliberately does not do because it carries synthetic data, read prove recoverability. For the no-custody trust model that the engine-side byte comparison rests on, read the no-custody trust model.

Last updated .