Verify a release yourself
This page shows how to confirm, yourself, that a downpipes release is what its channel entry says: built by CI from a specific tagged commit, and byte-identical from that build through the channel to what your account applied. At apply time your engine checks the channel signature and the signed digest automatically. This page is for the security team that wants to re-derive every step independently.
Check your release carries a provenance block
Every check below applies to a release whose channel entry carries a provenance block. Engine 0.3.5, the recommended release, carries one, and its four sidecars resolve. For a release without one, the console states “not published for this release” rather than linking to anything, and your console’s Provenance section shows what the release carries. The read-back gate runs in warn mode by default, recording evidence on every apply; set UPDATE_READBACK_MODE to enforce to make it refuse promotion.
What this proves, and what it does not
It proves that the bundle your engine applied is byte-for-byte the artefact CI built from the tagged source, that the build is repeatable by anyone from that same source, and that the platform held exactly those bytes at the moment your account promoted them.
It does not prove the source is benign. A defect or a backdoor written into the source would reproduce perfectly. Source review, which anyone can do against the published repository, is the separate assurance that covers that. Reproducibility and source review are different guarantees.
It does not prove real-time runtime execution. Cloudflare Workers, like every serverless platform, exposes no hardware attestation of the code running at a given instant. Your engine verifies the bytes at apply time, records the verdict in your own hash-chained audit trail, and re-compares the running deployment’s version identity against the last verified apply on every Security Centre evaluation (the update-version-drift check). Between those checks you are trusting the Cloudflare control plane and whoever holds access to your own account, the same trust every Worker there already carries.
What protects the release path
The steps below let you check a release yourself. This section explains what makes each check meaningful in the first place: the controls that stand between a source-code change and a bundle your engine will accept.
Getting a change into main. Every downpipes source repository requires a pull request and a green CI check before a change reaches main. There are no bypass actors on any repository ruleset, administrators included, so this applies uniformly regardless of who is proposing the change.
Signing the release tag. A release tag is signed by the maintainer with an SSH signing key held on a hardware security key: producing a signature needs the key’s PIN and a physical touch. The allowed-signers file also lists a second, software-held key, and a tag signed with that key also verifies. The signing key is registered on the maintainer’s GitHub account and listed in the repository’s allowed-signers file. The release workflow checks the tag’s signature against that file before it builds; a tag that does not verify never reaches step 1’s artefact.
Building reproducibly. The release workflow builds the artefact once. The build is deterministic: the artefact is a function of the tagged source, not of who built it or on what, so a rebuild from the tagged source on any machine gives the same bytes. Step 1 shows how to check that yourself.
Provenance, signing and transparency. Each release carries SLSA Build Level 3 provenance from the SLSA generator, a cosign keyless signature with a Rekor transparency-log entry, checksums, and a CycloneDX SBOM. Together these are what step 3 checks: what built the artefact, that it has not been altered since, and what it contains, all from public material rather than anything the vendor merely asserts.
Signing the update channel separately. The manifest your engine polls at update.downpipes.io is signed with a different key from the release tag: a hybrid Ed25519 plus ML-DSA-87 key whose secret is sealed to the same hardware security key used for release signing, using age encryption, with a physical touch required for every use. That secret is never stored in plaintext; an offline recovery copy is held under separate custody. Your engine verifies the manifest signature and pins the channel’s public key (step 2). This is why a compromised GitHub account, on its own, cannot push a malicious update to your engine: GitHub access lets someone merge code and cut a tag, but it does not hold the channel-signing secret, and a manifest built without that secret fails your engine’s pinned-key verification.
Compensating for a single maintainer. downpipes is maintained by a sole maintainer, so there is no second-person review gate on ordinary changes. The compensating controls are: no bypass actors on any ruleset, Dependabot monitoring on every repository, OpenSSF Scorecard on the engine, console, reader and control-plane repositories, Gitleaks and Semgrep scanning in CI, and a public verify route for issued documents.
Step 1: rebuild the artefact from source
The release artefact is a deterministic function of the tagged source: the same commit with its lockfile-pinned toolchain produces the same bytes wherever it is built.
git clone https://github.com/downpipes-io/engine && cd engine
git checkout vX.Y.Z
npm ci
node scripts/build-release.mjs
# prints { version, sha384, sha256, ... } and writes dist/engine-X.Y.Z.mjs
The sha384 printed here is the value to carry through every later step. The build refuses to run on a dirty tree or a stamped build-stamp.ts, so what you build is a function of the commit alone. If you would rather not rebuild the artefact yourself, start at step 2: everything from the signed channel onward verifies with public material alone.
Step 2: compare against the signed channel
The channel document your engine verifies is public. Fetch it and confirm the digest matches your rebuild:
curl -fsSL https://update.downpipes.io/stable.json
# components.engine.sha384 must equal your rebuilt sha384
The channel is signed with the pinned post-quantum hybrid key (Ed25519 plus ML-DSA-87, both halves required); your engine refuses any channel that does not verify under the pin your account holds. The provenance block inside the same signed body names the source commit, the release tag and the CI run, so the offline release key vouches for those pointers too. The same block names the Rekor transparency-log index when the publishing tool can read it from the cosign bundle. From engine 0.3.6 the tool can read the bundle form that cosign v3 writes. A channel that the tool made from a cosign v3 bundle before that has no index. The console then does not show its Transparency log (Rekor) row.
Step 3: verify the CI attestations
The attestation sidecars are republished on the channel host for any release whose channel entry carries a provenance block, so this step needs no access to our repositories. Read your own channel entry before fetching anything: if the entry for the version you are checking carries no provenance block, the sidecars were not published for that release and the paths below return 404. That is the state the warning above describes, not a tampering signal. Your console reports the same state as “not published for this release”.
BASE=https://update.downpipes.io/provenance/X.Y.Z
curl -fsSLO $BASE/engine.intoto.jsonl
curl -fsSLO $BASE/engine-X.Y.Z.mjs.cosign-bundle
curl -fsSLO $BASE/engine.SHA256SUMS.txt
curl -fsSLO $BASE/release-record.json
cosign verify-blob --bundle engine-X.Y.Z.mjs.cosign-bundle \
--certificate-identity-regexp '^https://github.com/downpipes-io/engine/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
dist/engine-X.Y.Z.mjs
slsa-verifier verify-artifact dist/engine-X.Y.Z.mjs \
--provenance-path engine.intoto.jsonl \
--source-uri github.com/downpipes-io/engine
The cosign identity pin means the signature is valid only if it was minted by the engine repository’s release workflow on a version tag; the keyless certificate and the signature are logged in the public Sigstore Rekor transparency log, so a signature served to you and hidden from everyone else is detectable.
Step 4: read your own account’s record
This is the step no vendor can do for you, and no vendor can fake after the fact. Open the console’s Licence and updates screen, Provenance section. It shows, from your engine’s own persisted records:
- the apply-time evidence: the signed digest your engine enforced at the last apply (recorded at promote time; the settled record and the hash-chained audit log both carry it) and the platform read-back verdict, whether Cloudflare’s own API returned byte-exactly those bytes before your account promoted them;
- an on-demand cross-check of your recorded digest against the published release record.
The Security Centre adds two standing checks: update-apply-provenance (the last apply left durable content-hash evidence) and update-version-drift (the running deployment is still the one the last verified apply left live; any out-of-band redeploy flips it). Both also ride into the signed evidence packs where a framework maps them: the drift check evidences ISO/IEC 27001 A.8.16 (monitoring activities), and the provenance check evidences NIS2 Article 21(2)(d) (supply chain security).
If any step fails
A mismatch at any step is what these checks exist to catch. Check first that you compared the same version (the channel moves; your account’s record names the version it applied). If the versions match and the digests do not, treat it as an incident: contact support@maelstrom.au, and keep the artefacts you fetched.
A 404 in step 3 is a different thing and is not an incident. It means the release you are checking was published without a provenance block, so there is nothing to fetch; your channel entry shows which state you are in. What still holds for such a release is the signed channel digest, which is what actually gates an apply, and your own account’s record of what it enforced.
Last updated .