Skip to content

Attach provenance and SBOM attestations to published images#1350

Merged
mbouaziz merged 1 commit into
mainfrom
docker-attestations
Jul 15, 2026
Merged

Attach provenance and SBOM attestations to published images#1350
mbouaziz merged 1 commit into
mainfrom
docker-attestations

Conversation

@mbouaziz

@mbouaziz mbouaziz commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Docker Scout reports "Required supply chain attestations missing" on every skiplabs/* image. Worse, without provenance Scout has to guess an image's origin — it prints Base image was auto-detected and attributes findings to an inferred base rather than the real one. (That guessing is not theoretical: it produces confidently wrong base-image attributions.)

This emits provenance (mode=max) and SBOM attestations, using bake's native --provenance / --sbom shorthands.

Scoped to push flows on purpose

Attestations require the docker-container driver — which is exactly what the named builder used for --push/--push-only provides. Local builds export via --load on the default docker driver, which rejects them outright:

ERROR: Attestation is not supported for the docker driver.
Switch to a different driver, or turn on the containerd image store

So enabling them unconditionally — e.g. by putting attest in docker-bake.hclwould break every local build. Gating on push avoids that while still attesting everything we publish.

Verified both directions with bake --print:

path resolved attest
push (--provenance/--sbom) [{type: sbom}, {mode: max, type: provenance}]
local (--load) absent ✅

Other drawbacks considered

  • mode=max records build args, so a secret passed as a build arg would be embedded. The only ones passed here are STAGE and SKIP_CAPACITY, neither sensitive. A comment records this so it stays true.
  • Attestations add unknown/unknown entries to the image index. Normal docker pull ignores them, and nothing in this repo parses manifest lists or iterates platforms, so no tooling is affected.
  • The skipruntime target exports with --output type=local (to extract libskipruntime.so) and is never pushed, so it's unaffected.
  • Registry support: Docker Hub supports OCI image indexes with attestation manifests.

Test plan

  • bake --print confirms attest present on the push path, absent on the local path
  • Confirmed the docker driver rejects attestations (reproduced the error) — the reason for the gating
  • Confirmed the docker-container driver produces them: exported an OCI index containing platform=unknown/unknown type=attestation-manifest alongside the image manifest
  • shellcheck --exclude SC2181 --exclude SC2002 clean (matches Makefile:104)
  • CI green
  • After merge + a republish, confirm Scout's "Required supply chain attestations missing" policy passes and Base image was auto-detected no longer appears

Relates to #1348 (automating publishing — a GitHub Actions workflow would emit these natively).

Docker Scout reports "Required supply chain attestations missing" on every
skiplabs/* image, and without provenance it has to guess where an image came
from, printing "Base image was auto-detected" and attributing findings to a
base it inferred rather than the one actually used.

Emit provenance (mode=max) and SBOM attestations via bake's --provenance and
--sbom shorthands.

These are deliberately restricted to push flows. Attestations require the
docker-container driver, which is what the named builder used for
--push/--push-only provides. Local builds export through --load on the default
docker driver, which rejects them outright:

  ERROR: Attestation is not supported for the docker driver.

so enabling them unconditionally -- e.g. by putting attest in docker-bake.hcl
-- would break every local build. Verified both directions with `bake --print`:
attest is present on the push path and absent on the local path.

Other drawbacks considered:

- mode=max records build args, so a secret passed as a build arg would be
  embedded. The only ones passed here are STAGE and SKIP_CAPACITY, neither
  sensitive. Noted in a comment so it stays that way.
- Attestations add unknown/unknown entries to the image index. Normal pulls
  ignore them, and nothing in this repo parses manifest lists, so no tooling
  is affected.
- The skipruntime target exports with --output type=local and is never pushed,
  so it is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mbouaziz
mbouaziz merged commit 5a9a737 into main Jul 15, 2026
3 checks passed
@mbouaziz
mbouaziz deleted the docker-attestations branch July 15, 2026 15:01
mbouaziz added a commit that referenced this pull request Jul 16, 2026
Closes #1348.

## Problem

The `skiplabs/*` images CI runs on were published by a maintainer
running `bin/release_docker_ci_images.sh` locally, with their own Docker
Hub credentials. Nothing else ever rebuilt them, and it shows:

- `skiplang-bin-builder` sat ~a year stale carrying **3 critical / 68
high** CVEs (#1338), because the only thing that ever published it was a
human remembering to.
- `skdb-base` silently stopped building after the Ubuntu 24.04 bump and
no one noticed until a manual rebuild happened to surface it (#1290) —
CI runs *against* the published images, so it structurally cannot catch
this.
- `skiplang:latest` and `skip:latest` on Docker Hub are currently a week
apart despite being stages of the *same* Dockerfile, so CI jobs run
against inconsistent compiler builds.

## What this does

Builds and pushes the images from GitHub Actions instead, on three
triggers:

- **`workflow_dispatch`** — on demand
- **`push` to `main`**, path-filtered to the files the images are built
from — this is what fixes the #1338 class of rot (pin bumped, republish
forgotten)
- **weekly `schedule`** — picks up base-image security updates, since
with no layer cache every run re-resolves `FROM` and re-runs `apt`

### Scope: four toolchain images

Publishes the new **`ci` group** in `docker-bake.hcl`: `skiplang`,
`skiplang-bin-builder`, `skip`, `skdb-base`. These are toolchain images
— their final stages hold compiled binaries and apt packages but no repo
source, so any green `main` commit can rebuild them and republishing is
not a release.

`skdb` and `skdb-dev-server` are deliberately excluded: they bake in
source and their tags carry release semantics. (`skiplabs/skdb` has in
fact never existed on Docker Hub, despite being in bake's `default`
group.)

### Source of truth

The `ci` group is authoritative. The workflow derives its per-image tag
overrides from `bake --print ci` at runtime rather than listing them,
`release_docker_ci_images.sh` now takes its list from the same group (so
the manual path and the automated one can't drift, and a manual publish
finally includes `skiplang-bin-builder`), and a new
`bin/check_ci_images.sh` asserts every image CircleCI pulls is in the
group — replacing the old `grep .circleci/base.yml` derivation, which
structurally could not express `skiplang-bin-builder`.

### Runner strategy

Native `ubuntu-24.04` + `ubuntu-24.04-arm` in parallel (arm64 hosted
runners are free on public repos), each pushing by digest, then a merge
job creates the multi-arch tags. QEMU was rejected: ~4× slower and it
would emulate a self-hosting compiler bootstrap. **Nothing rewrites a
`:latest` tag until every platform of every image has pushed
successfully**, and the merge validates all images before publishing
any.

Provenance (`mode=max`) + SBOM attestations are emitted via
bake-action's `provenance`/`sbom` inputs, matching what
`bin/docker_build.sh` already does for the manual path (#1350), so
images published either way carry the same attestations.

## Verification

The parts that can't be exercised without pushing were verified locally
against a registry on buildx v0.35.0:

- a target that is both published **and** consumed as a named context
(`skdb-base` builds `FROM skip`) keeps its own push-by-digest output —
settled at solve time, not just `bake --print`;
- each per-platform push is an OCI index carrying its attestation
manifest inline;
- `imagetools create` preserves **both** platforms' attestations into
the merged tag (verified: merged index = 2 platform manifests + 2
attestation manifests).

`actionlint` (1.7.7) and `shellcheck` are clean on everything here.

## Notes for the reviewer

- **Requires org secrets `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`**
(scoped push token) before the first run — the repo has none today. The
first run should be a watched `workflow_dispatch`, not the weekly
schedule.
- **Stacked on the actionlint fix** (first commit): CI currently
installs actionlint **1.7.6** — the `v1.7.7` in the URL only selects the
downloader script, which defaults to 1.7.6 — and 1.7.6 rejects `runs-on:
ubuntu-24.04-arm`. That commit stands on its own but this workflow needs
it to lint.
- **Conscious tradeoff:** the workflow does not run CI against the
freshly built images before overwriting `:latest`. That would roughly
double the pipeline; the digest/attestation guards catch push-level
failures but not "builds fine, breaks CI". `cancel-in-progress: false`
and a watched first run are the mitigations.
- Actions are pinned to major tags (`@v4`/`@v7`), matching repo
convention (no dependabot). SHA-pinning would harden this first
credential-holding workflow but the actions would then never update —
happy to switch if preferred.
- Expect `unknown/unknown` rows on Docker Hub for these images — that's
how BuildKit stores attestation manifests, not a bug.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants