Verify-at-seal: every backup is read back and verified before it counts
A backup that was written is not the same as a backup that is recoverable. The bytes can land, the upload can return success, and the archive can still be partial or corrupt. Verify-at-seal closes that gap. Right after a run seals (the archive and the RUNLOG are written to your destination) and before the run is reported a clean success, the engine reads the just-written archive back from the destination and verifies it. A corrupt or partial backup is caught now, at seal time, rather than at the next periodic restore test up to a week later.
The check reuses the same reader the in-account drill, the canary and a real restore use, so it is the same verification machinery rather than a second, weaker code path. It runs inside your own Cloudflare account, against your own destination bytes. This is an engine-side check of signature, completeness and anti-rollback, not a Cloudflare-side or store-side guarantee.
Verify-at-seal is on by default. A run that passes earns a quiet trust badge on the runs screen; a run that comes back suspect raises a loud, honest signal. Either way the run completes.
What it does, in order
Verify-at-seal runs on every successful seal path: a run that finishes inline, a large run resumed across several slices, and the buffered fall-back path all run the same shared step before they post the clean completion.
The archive is already written
The run has sealed. The encrypted archive, the signed root and the RUNLOG chain entry are all on your destination. Nothing about verify-at-seal changes those bytes.
The engine reads it back
The engine re-opens the run from the destination as a read-only store, exactly as a drill does, and reads the signed root, the detached signature, the RUNLOG and each shard manifest.
It records a verdict
The verdict (verified or suspect, plus the tier that ran) is attached to the run row. On a verified run the engine also stamps the integrity-verified recency. On a suspect run it raises a critical posture finding and fires a critical notification.
Default-on, and fail-open
Two contracts govern the behaviour, and they are the load-bearing part of the design.
On by default. Verify-at-seal runs unless you explicitly turn it off. Only a falsey VERIFY_AT_SEAL value (0, false, no or off) disables it; a deployment that sets nothing gets the safety. When it is off the engine returns no verdict and never re-reads the destination.
Fail-open, always. The bytes are already written when verify-at-seal runs, so a verify failure must never make things worse. The check never throws (every path is wrapped), never mutates or deletes the destination, and never fails or blocks the run. Before the final verdict is recorded, a suspect first read is retried up to SEAL_VERIFY_ATTEMPTS (default 3, max 8) whole attempts with jittered backoff, so a transient read-after-write lag on the destination does not get mistaken for corruption; a retry that clears is recorded as verified with attempts/recovered fields on the verdict, and a failure that survives every attempt records a suspect verdict, raises a critical seal-verification posture finding and fires a critical notification, and the run still completes as ok. A verify that could not even be attempted (for example the engine is not fully configured, or the reader threw before reaching a verdict) is reported honestly as suspect with a coarse reason, never as a fabricated pass.
It is not a gate
Verify-at-seal cannot fail a backup, block a run, or roll anything back. It is a read-back check on an archive that already exists. A suspect verdict is a signal to investigate, not a deletion, a quarantine, or a rejected run.
Three tiers
Verify-at-seal runs at one of three depths. Tier-0 always runs. A keyed decrypt step runs additionally, and only when the engine is in a position to do it cheaply and without exceeding a cost ceiling; whether that decrypt step covers every record (full) or a strided sample (sampled-decrypt) depends on the run’s size. The read happens on your own metered Cloudflare account, so the depth is bounded on purpose.
| Tier | What it checks | Key needed | When it runs | Cost ceiling |
|---|---|---|---|---|
| Tier-0 (keyless attestation) | The hybrid root signature verifies, every signed shard is present and hashes to the signed root (completeness), and the RUNLOG anti-rollback freshness check holds | None | Always, on every run | Reads manifests and hashes, not record payloads, so it is cheap. The per-shard re-read is full up to SEAL_VERIFY_FULL_SHARDS shards (default 900); above that it re-reads only a bounded strided sample of SEAL_VERIFY_SHARD_SAMPLE shards (default 64), so a very large run cannot trip the platform subrequest cap |
| Full | Everything in Tier-0, plus the full keyed structural chain (capsule unwrap, key-commitment recompute, every record’s hash, the Merkle root), and a decrypt-and-hash check of every record | The operational read-back key | Only when the engine holds the operational read-back key and the run’s plaintext is at or below SEAL_VERIFY_FULL_BYTES (default 64 MiB) |
Bounded by the byte ceiling; every record in a small run is decrypted, so a flipped byte anywhere in it is caught now, not only if it happened to fall in a sample |
| Sampled-decrypt | Everything in Tier-0, plus the full keyed structural chain, and a decrypt-and-hash check of a strided sample of records | The operational read-back key | Only when the engine holds the operational read-back key and the run’s plaintext is above SEAL_VERIFY_FULL_BYTES but at or under SEAL_VERIFY_MAX_BYTES (default 5 GiB) |
Skipped entirely when the run’s plaintext is over SEAL_VERIFY_MAX_BYTES, so it can never become an unbounded re-read |
Tier-0 is the keyless chain attestation
Tier-0 re-opens the run from the destination and verifies the post-quantum hybrid signature over the stored root. That root covers the Merkle root, every per-record hash, the declared counts and the shard digests. Tier-0 then confirms every signed shard is present and hashes to the signed root, and runs the key-free RUNLOG anti-rollback check. No decryption key is needed for any of this. The per-shard presence-and-hash re-read is full for a run at or under SEAL_VERIFY_FULL_SHARDS shards (default 900); above that threshold it re-reads only a bounded strided sample of SEAL_VERIFY_SHARD_SAMPLE shards (default 64) instead, so a very large run’s at-seal verify cannot trip the platform’s subrequest cap. The root signature still authenticates the whole shard list either way. If the signature, the completeness or the anti-rollback flag fails, the verdict is suspect.
The keyed decrypt step: full for a small run, sampled above that
When the engine holds the operational read-back key, and the run is under the SEAL_VERIFY_MAX_BYTES ceiling, verify-at-seal also opens the run with that key (the full keyed structural chain) and decrypts and hash-checks records. For a run whose plaintext is at or below SEAL_VERIFY_FULL_BYTES (default 64 MiB), every record is decrypted and hash-checked: full coverage, so a flipped byte anywhere in a small run is caught now. Above that byte threshold, and up to SEAL_VERIFY_MAX_BYTES, a strided sample is decrypted instead; the sample defaults to three records and is capped at fifty. It is read evenly across the record list rather than only from the front, so corruption in a late shard is more likely to be caught. Each decrypted record’s plaintext hash is re-checked.
Why the tiering is honest
Tier-0 proves signature, completeness and anti-rollback for every record’s hash and every shard, on every run. The decrypt step is a small strided sample above the full-coverage byte threshold, not a full re-verification of every record’s plaintext in a larger run. The badge and the posture finding both say which tier actually ran, so the assurance you read is the assurance that was performed.
When the keyed decrypt step does not run
The keyed decrypt step (full or sampled-decrypt) is skipped, and the verify is honestly Tier-0 only, in three cases. There is no fabricated decrypt result in any of them; the verdict simply names the tier that ran, and why.
A downpipe with no way to reach the run’s key. The keyed step needs the run’s master, and it has two routes to one: the in-account operational key, or the per-run key the seal path still holds while it is finalising that run. A break-glass-only downpipe has no operational key and reaches the keyed tier by the second route, so it is no longer Tier-0 for posture reasons. Only a run where neither route is available falls back, and the verdict names that as the cause.
An oversized run. When the run’s plaintext exceeds the byte ceiling (SEAL_VERIFY_MAX_BYTES, default 5 GiB), the decrypt step is skipped entirely so verify-at-seal stays bounded on your metered account. Tier-0 still runs, because Tier-0 reads only manifests and hashes and is always cheap.
A run with too many shards. Tier-0 walks your run’s shard list, and the keyed step walks it again, so running both costs roughly twice the shard count in reads before a single record is opened. Above SEAL_VERIFY_DECRYPT_MAX_SHARDS (default 400) the keyed step is skipped. This one is worth understanding as a deliberate choice rather than a limit: past that point the keyed step would exhaust the platform’s per-invocation request budget and fail partway, and because verify-at-seal never blocks a run, a budget failure would surface as a suspect verdict telling you a perfectly good archive might be corrupt.
How the verdict surfaces per run
The verdict is shown on the runs screen, both in the status column and in the run detail drawer. The wire shape carries the status, the tier, the sample count and the time, plus a coarse reason on a suspect verdict only.
At the keyless tier the chain attestation alone produces the verdict, so the drawer names what was checked without claiming any record was decrypted. The same section names the record count it decrypt-checked and says the coverage was complete when the full-coverage tier ran, and names a sample count when the decrypt step was sampled, so the drawer always says which depth produced the verdict rather than a bare pass.
The run shows a quiet trust badge that names the tier that ran. A Tier-0 verify reads “verified at seal”; a sampled-decrypt verify reads “verified, sampled N” so you can tell a keyless chain attestation from a run that also decrypt-checked a sample. A full-coverage verify reads “verified, all N”, so the three depths are distinguishable at a glance. The drawer states each in plain English: “Verified at seal (signature, completeness and freshness)” for Tier-0, “Verified at seal (decrypt-checked N records)” for the sampled tier, and “Verified at seal (decrypt-checked all N records, full byte coverage)” for the full tier.
A Tier-0 drawer also says WHY the keyed check did not run, when the engine recorded a cause. A bare Tier-0 cannot tell an intentional configuration from a misconfigured one, and every cause a current run can carry is something you can act on: the keyed decrypt check switched off for this engine, a run over the check’s size ceiling, or a run with more shards than its budget allows. One further cause reads as history rather than as a current state. A run sealed before the seal path could reach the keyed check without an in-account key records break-glass, and the drawer says so plainly, because runs sealed since then do reach it. A run whose verdict carries no recorded cause reads exactly as it always has.
A run that decrypt-checked every record and one that only attested the signature chain are different assurances, so they read differently. The exception is a run with nothing to count: a first backup of an empty source is legitimately full coverage over zero records, and “verified, all 0” would read worse than the plain attestation, so it falls through to “verified at seal”.
The run shows a danger badge reading “seal suspect”, and the drawer adds a loud detail panel with the coarse, secret-free reason (for example “integrity check failed”, “object missing” or “freshness check failed”) and a line telling you to investigate before relying on the run. There is no soothing message: a suspect verdict is a real recoverability signal.
A run sealed before verify-at-seal existed carries no verdict and shows nothing here. That is honest absence, never a fabricated pass. The reason on a suspect verdict is always one of a fixed set of coarse buckets; it never carries a raw exception, a shard id, an object key, a value or a decryption key.
The recency-stamp nuance
A successful run normally stamps an integrity-verified recency, which is what lets the console say “integrity-checked daily” rather than “never integrity-checked”. Verify-at-seal refines that stamp without ever failing the run.
On a verified run the integrity-verified recency is stamped as usual. On a suspect verify the integrity-verified recency is not set, because the just-written archive did not read back cleanly and stamping it would be a false positive. The most recent verify-at-seal verdict (lastSealVerify) records both outcomes, verified and suspect, so a suspect read-back is observable to posture and to the status surface. The run itself still completes either way.
Two recency fields, two jobs
The integrity-verified recency is affirmative: its presence is proof that integrity was verified, so it is set only on a pass. The last seal-verify verdict records the truth of the last attempt, pass or fail, so a suspect outcome cannot be hidden by simply not stamping it.
Where a suspect verdict feeds
A suspect verify-at-seal verdict raises the seal-verification posture check, which is a critical-severity finding. A downpipe whose last verify-at-seal came back suspect fails that check until a clean run or a confirming drill clears it. The finding states plainly that the run still completed and the archive was not deleted (fail-open), and that a drill or a restore confirms recoverability. A downpipe that has never run with verify-at-seal on does not fail the check, because there is no suspect verdict to surface.
A suspect verdict also fires a critical notification through your configured channels. It reuses the existing critical restore-test event vocabulary, so a seal-verify failure pages the same people a failed restore test would. The notification carries the downpipe name, the coarse reason and the tier, and states that the run was not failed and the archive was not deleted.
The configuration knobs, precisely
A handful of environment values tune verify-at-seal. Each has a safe default and a bound, and a malformed value falls back to the default rather than disabling the safety.
-
VERIFY_AT_SEALgates the whole feature. It defaults on. Only a falsey value (0,false,nooroff) turns it off; any other value, or no value, leaves it on. -
SEAL_VERIFY_SAMPLEsets how many records the strided decrypt sample reads and hash-checks on a run above the full-coverage byte threshold. It defaults to 3 and is capped at 50. A value of 0 disables the sample (Tier-0 only); an out-of-range or malformed value falls back to the default. -
SEAL_VERIFY_FULL_BYTESsets the plaintext-size threshold at or below which the decrypt step covers every record (the “full” tier) instead of a strided sample. It defaults to 64 MiB. Set to 0 to disable full coverage and always use the strided sample. -
SEAL_VERIFY_MAX_BYTESsets the plaintext-size threshold above which the decrypt step (full or sampled) is skipped entirely (Tier-0 only). It defaults to 5 GiB. Tier-0 itself is never gated by this, because it reads only manifests and hashes. -
SEAL_VERIFY_ATTEMPTSsets how many whole read-back attempts verify-at-seal makes before recording a final verdict, retrying a suspect first read with jittered backoff to separate a transient read-after-write lag from real corruption. It defaults to 3 and is capped at 8. -
SEAL_VERIFY_FULL_SHARDSsets the shard-count threshold at or below which Tier-0’s per-shard presence-and-hash re-read is full. It defaults to 900 and is capped at 5000. -
SEAL_VERIFY_SHARD_SAMPLEsets how many shards Tier-0 re-reads (strided) on a run aboveSEAL_VERIFY_FULL_SHARDS. It defaults to 64 and is capped at 256. -
OPERATIONAL_PRIVATEis not a verify-at-seal knob. Its presence decides HOW the keyed step reaches the run’s master, not whether that step runs: with it the run is opened by decapsulating its recipient wrap, which also proves that wrap opens; without it the run is opened from the per-run key the seal path holds. A break-glass-only downpipe therefore still gets the keyed tier. -
SEAL_VERIFY_DECRYPT_MAX_SHARDScaps the shard count at which the keyed step runs at all, defaulting to 400. Tier-0 walks the shard list and the keyed step walks it again, so both together cost roughly twice the shard count in reads. Above the cap the verdict is honestly Tier-0 rather than a run that exhausts the platform’s per-invocation request budget and reports a good archive as suspect.
Related pages
- How downpipes works covers the seal-then-prove flow that verify-at-seal completes.
- The posture score explains how the critical seal-verification finding weighs into your overall posture.
- Prove recoverability describes the drill and restore test that periodically re-verify an archive and confirm a suspect verdict.
- Recovery postures explains the break-glass-only posture, in which verify-at-seal still reaches the keyed tier by using the run’s own key.
Last updated .