From 7159e425054f2ed434267e604144ccd7215000f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 16:42:45 +0200 Subject: [PATCH 1/2] docs(security): the task SBOM is the base SBOM, attested -- read it with cosign verify-attestation; provenance stays inline Held until tracebloc-engine#950 (backend#3317 lever c) merges: this page must not describe unshipped behaviour. Co-Authored-By: Claude Fable 5.1 --- environment-setup/security.mdx | 58 +++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/environment-setup/security.mdx b/environment-setup/security.mdx index aa6a44c..43d6277 100644 --- a/environment-setup/security.mdx +++ b/environment-setup/security.mdx @@ -107,14 +107,64 @@ Every training image tracebloc publishes under `tracebloc/*` on Docker Hub is si ### View the SBOM and provenance -The SBOM and build provenance live *inside* the image index as BuildKit attestations, so you read them with Docker's own tooling rather than cosign. Use the digest you just verified: +Every task image (`tracebloc/client--cpu` and `-gpu`) carries **two +attestations**, stored two different ways because two different tools write them: + +- **The SBOM is a cosign attestation on the image digest.** A task image is the + base image (`tracebloc/base`, `tracebloc/nlp` or `tracebloc/cv`) plus tracebloc's + own code copied into `/app` -- the task Dockerfiles install nothing, and a check + in the publish pipeline refuses any change that would. So the SBOM attached to a + task digest is the base image's SPDX 2.3 document for each platform the image + was built for (`linux/amd64`, and `linux/arm64` for CPU images), signed with the + same keyless identity as the image itself. Read it with cosign, using the digest + you just verified: + + ```bash + cosign verify-attestation \ + --type spdxjson \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github\.com/tracebloc/tracebloc-engine/\.github/workflows/docker-build\.yml@refs/heads/.+$' \ + tracebloc/@sha256: \ + | jq -r .payload | base64 -d | jq .predicate > sbom.json + ``` + + One in-toto statement is returned per platform, each wrapping the SPDX document + for that platform (the packages name their architecture in their `purl`s). The + identity regexp pins the SBOM to tracebloc's task-image workflow, exactly as + `cosign verify` pins the signature. A digest with no attestation fails with + `no matching attestations`. + +- **The build provenance lives inside the image index** as a BuildKit attestation, + so you read it with Docker's own tooling: + + ```bash + docker buildx imagetools inspect tracebloc/@sha256: --format '{{ json .Provenance }}' > provenance.json + ``` + + It is SLSA v1 and records how, and from what, the image was built -- including + the exact digest of the base image, under `buildDefinition.resolvedDependencies` + (`pkg:docker/tracebloc/@?platform=...`). That is the digest the + attested SBOM describes. + +**The base images carry their SBOM inline**, the way the task images did before +September 2026, so the same BuildKit command reads it per platform: ```bash -docker buildx imagetools inspect tracebloc/@sha256: --format '{{ json .SBOM }}' > sbom.json -docker buildx imagetools inspect tracebloc/@sha256: --format '{{ json .Provenance }}' > provenance.json +docker buildx imagetools inspect tracebloc/: --format '{{ json .SBOM }}' > base-sbom.json ``` -The SBOM is an SPDX 2.3 document per platform (`linux/amd64`, `linux/arm64`) listing every package in the image — tens of megabytes, hence the redirect to a file — and can be fed to whatever SBOM or vulnerability tooling you already use. The provenance is SLSA and records how, and from what, the image was built. +(For a multi-platform index the output is a `platform -> {SPDX}` map; for a single +platform it is `{SPDX}`.) + + +**Transition, one release.** Task digests published before this change (still +behind `:stg` and `:prod` until the next promotions) carry the SBOM inline, so +`docker buildx imagetools inspect ... --format '{{ json .SBOM }}'` answers for +them and `cosign verify-attestation --type spdxjson` does not; digests published +after it are the other way round. If one command returns nothing, try the other. +The SBOM content is the same either way: the packages of the base image the +task image layers on. + ### Signatures follow the digest, not the tag From ad2a5d087dbb2396cc3046843eb311fea6865c7a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 23:14:11 +0200 Subject: [PATCH 2/2] docs(security): the task SBOM attestation is a checksummed reference to the base's inline SBOM (backend#3439 part 3) Still held until tracebloc-engine#961 merges. Co-Authored-By: Claude Fable 5.1 --- environment-setup/security.mdx | 91 +++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/environment-setup/security.mdx b/environment-setup/security.mdx index 43d6277..1a3c8ca 100644 --- a/environment-setup/security.mdx +++ b/environment-setup/security.mdx @@ -110,14 +110,17 @@ Every training image tracebloc publishes under `tracebloc/*` on Docker Hub is si Every task image (`tracebloc/client--cpu` and `-gpu`) carries **two attestations**, stored two different ways because two different tools write them: -- **The SBOM is a cosign attestation on the image digest.** A task image is the - base image (`tracebloc/base`, `tracebloc/nlp` or `tracebloc/cv`) plus tracebloc's - own code copied into `/app` -- the task Dockerfiles install nothing, and a check - in the publish pipeline refuses any change that would. So the SBOM attached to a - task digest is the base image's SPDX 2.3 document for each platform the image - was built for (`linux/amd64`, and `linux/arm64` for CPU images), signed with the - same keyless identity as the image itself. Read it with cosign, using the digest - you just verified: +- **The SBOM is a cosign attestation on the image digest, and it points at the + base image's SBOM by checksum.** A task image is the base image + (`tracebloc/base`, `tracebloc/nlp` or `tracebloc/cv`) plus tracebloc's own code + copied into `/app` -- the task Dockerfiles install nothing, and a check in the + publish pipeline refuses any change that would. So the packages of a task image + are the packages of its base, and the attestation on a task digest is a small + SPDX 2.3 document (a few kilobytes) with one package -- the base image, by index + digest and platform -- and one *external document reference*: the base's full + SPDX SBOM, named by its own `documentNamespace` and by the SHA-256 of its exact + bytes. It is signed with the same keyless identity as the image itself. Read it + with cosign, using the digest you just verified: ```bash cosign verify-attestation \ @@ -125,14 +128,55 @@ attestations**, stored two different ways because two different tools write them --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp '^https://github\.com/tracebloc/tracebloc-engine/\.github/workflows/docker-build\.yml@refs/heads/.+$' \ tracebloc/@sha256: \ - | jq -r .payload | base64 -d | jq .predicate > sbom.json + | jq -r .payload | base64 -d | jq .predicate > sbom-ref.json ``` - One in-toto statement is returned per platform, each wrapping the SPDX document - for that platform (the packages name their architecture in their `purl`s). The - identity regexp pins the SBOM to tracebloc's task-image workflow, exactly as - `cosign verify` pins the signature. A digest with no attestation fails with - `no matching attestations`. + One in-toto statement is returned per platform (`linux/amd64`, and + `linux/arm64` for CPU images). The identity regexp pins the attestation to + tracebloc's task-image workflow, exactly as `cosign verify` pins the signature. + A digest with no attestation fails with `no matching attestations`. + + Then follow the reference to the full document: + + ```bash + jq -r '.packages[0].externalRefs[0].referenceLocator' sbom-ref.json + # pkg:docker/tracebloc/@sha256:?platform=linux/amd64 + jq -r '.externalDocumentRefs[0].checksum.checksumValue' sbom-ref.json + # <64 hex> -- the SHA-256 of the base SBOM's bytes + jq -r '.annotations[0].comment' sbom-ref.json + # where the bytes live: the base index, its attestation manifest, the SPDX + # layer digest and byte length, and the imagetools command below + ``` + + The full SBOM is the base image's inline BuildKit attestation, exactly as buildx + wrote it when the base was built. Read it with Docker's own tooling, at the base + digest the reference names (the same digest the task image's provenance names + under `resolvedDependencies`): + + ```bash + docker buildx imagetools inspect tracebloc/@sha256: --format '{{ json .SBOM }}' > base-sbom.json + ``` + + (For a multi-platform index the output is a `platform -> {SPDX}` map; for a + single platform it is `{SPDX}`.) Its `documentNamespace` is the + `externalDocumentRefs[0].spdxDocument` of the reference. + + To check the **bytes**, not just the content: `checksumValue` is the SHA-256 of + the SPDX attestation blob on the base index -- an OCI blob, so its digest *is* + that checksum, and any registry client can fetch it by that digest and hash it: + + ```bash + crane blob tracebloc/@sha256: | tee base-sbom.statement.json | sha256sum + # must print ; then + jq .predicate base-sbom.statement.json > base-sbom.json + ``` + + (`imagetools inspect` re-serialises the JSON, so `sha256sum` of *its* output is + not the checksum -- compare the blob, or compare the layer digest it reports + with `--raw` on the attestation manifest.) A checksum that does not match means + the base SBOM you fetched is not the one the task image was attested against; + a purl whose digest is not the one in the task image's provenance means the + attestation is not about the base this image was built from. - **The build provenance lives inside the image index** as a BuildKit attestation, so you read it with Docker's own tooling: @@ -144,7 +188,8 @@ attestations**, stored two different ways because two different tools write them It is SLSA v1 and records how, and from what, the image was built -- including the exact digest of the base image, under `buildDefinition.resolvedDependencies` (`pkg:docker/tracebloc/@?platform=...`). That is the digest the - attested SBOM describes. + attested SBOM reference names, and the publish pipeline refuses to attest a + reference to any other. **The base images carry their SBOM inline**, the way the task images did before September 2026, so the same BuildKit command reads it per platform: @@ -153,8 +198,14 @@ September 2026, so the same BuildKit command reads it per platform: docker buildx imagetools inspect tracebloc/: --format '{{ json .SBOM }}' > base-sbom.json ``` -(For a multi-platform index the output is a `platform -> {SPDX}` map; for a single -platform it is `{SPDX}`.) +**Why by reference.** `cosign attest` uploads the whole signed statement -- the +predicate inline -- to the Rekor transparency log, and Rekor has a size ceiling: +the CPU bases' SPDX documents are 21-26 MB per platform and every attempt to +attest one was refused (the 10-15 MB GPU documents went through). The full +document therefore stays where buildx wrote it, inline on the base image, and +the attestation binds the task digest to those exact bytes by checksum. Nothing +is lost in transparency: the reference is in Rekor under the workflow's identity, +and the bytes it names are content-addressed in the registry. **Transition, one release.** Task digests published before this change (still @@ -162,8 +213,10 @@ behind `:stg` and `:prod` until the next promotions) carry the SBOM inline, so `docker buildx imagetools inspect ... --format '{{ json .SBOM }}'` answers for them and `cosign verify-attestation --type spdxjson` does not; digests published after it are the other way round. If one command returns nothing, try the other. -The SBOM content is the same either way: the packages of the base image the -task image layers on. +A few GPU digests published on 2026-09-08 carry the full base document *as* the +cosign predicate (no `externalDocumentRefs`): for those, `sbom-ref.json` already +is the SBOM. The SBOM content is the same in every form: the packages of the +base image the task image layers on. ### Signatures follow the digest, not the tag