diff --git a/environment-setup/security.mdx b/environment-setup/security.mdx index aa6a44c..1a3c8ca 100644 --- a/environment-setup/security.mdx +++ b/environment-setup/security.mdx @@ -107,14 +107,117 @@ 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, 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 \ + --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-ref.json + ``` + + 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: + + ```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 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: ```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. +**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 +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. +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