Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 107 additions & 4 deletions environment-setup/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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-<task>-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/<image>@sha256:<digest> \
| 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/<base>@sha256:<base index digest>?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/<base>@sha256:<base index digest> --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/<base>@sha256:<checksumValue> | tee base-sbom.statement.json | sha256sum
# must print <checksumValue>; 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/<image>@sha256:<digest> --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/<base>@<tag>?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/<image>@sha256:<digest> --format '{{ json .SBOM }}' > sbom.json
docker buildx imagetools inspect tracebloc/<image>@sha256:<digest> --format '{{ json .Provenance }}' > provenance.json
docker buildx imagetools inspect tracebloc/<base>:<cpu|gpu> --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.

<Note>
**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.
</Note>

### Signatures follow the digest, not the tag

Expand Down