From c4bc48da9bac3da8846aea023f72035aaa4fefda Mon Sep 17 00:00:00 2001 From: Adrien Langou Date: Thu, 13 Aug 2026 10:42:13 +0200 Subject: [PATCH 1/2] feat(build): embed auditable Rust dependency metadata Signed-off-by: Adrien Langou --- .agents/skills/sbom/SKILL.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.agents/skills/sbom/SKILL.md b/.agents/skills/sbom/SKILL.md index 852259293d..b08550739b 100644 --- a/.agents/skills/sbom/SKILL.md +++ b/.agents/skills/sbom/SKILL.md @@ -13,7 +13,11 @@ The OpenShell SBOM tooling produces source-tree CycloneDX JSON SBOMs using Syft, SBOMs are **release artifacts only** -- they are generated on demand and not committed to the repository. Output lands in `deploy/sbom/output/` (gitignored). -Release Dev and Release Tag image builds separately embed cargo-auditable metadata in the staged gateway and supervisor binaries. This metadata describes the binary's Rust dependency graph and lets Syft discover Cargo packages from the binary itself. It is not a complete image SBOM and is not an OCI SBOM attestation; publishing such an attestation remains separate work. +Release Dev and Release Tag image builds separately embed cargo-auditable +metadata in the staged gateway and supervisor binaries. This metadata describes +the binary's Rust dependency graph and lets Syft discover Cargo packages from +the binary itself. It is not a complete image SBOM and is not an OCI SBOM +attestation; publishing such an attestation remains separate work. ## Prerequisites From 3362af38204f4fc9a20827fec8b164de1f42a7d6 Mon Sep 17 00:00:00 2001 From: Adrien Langou Date: Thu, 20 Aug 2026 14:16:07 +0200 Subject: [PATCH 2/2] feat(build): publish OCI SBOM and provenance attestations Signed-off-by: Adrien Langou --- .agents/skills/sbom/SKILL.md | 21 +++++++--- .github/workflows/branch-e2e.yml | 2 + .github/workflows/docker-build.yml | 7 ++++ .github/workflows/rust-native-build.yml | 2 + architecture/build.md | 36 +++++++++++------ docs/security/verifying-images.mdx | 36 +++++++++++++++++ tasks/scripts/docker-build-image.sh | 15 ++++--- tasks/scripts/verify-image-sbom.sh | 53 +++++++++++++++++++++++++ 8 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 docs/security/verifying-images.mdx create mode 100755 tasks/scripts/verify-image-sbom.sh diff --git a/.agents/skills/sbom/SKILL.md b/.agents/skills/sbom/SKILL.md index b08550739b..6996ffcf66 100644 --- a/.agents/skills/sbom/SKILL.md +++ b/.agents/skills/sbom/SKILL.md @@ -13,17 +13,28 @@ The OpenShell SBOM tooling produces source-tree CycloneDX JSON SBOMs using Syft, SBOMs are **release artifacts only** -- they are generated on demand and not committed to the repository. Output lands in `deploy/sbom/output/` (gitignored). -Release Dev and Release Tag image builds separately embed cargo-auditable -metadata in the staged gateway and supervisor binaries. This metadata describes -the binary's Rust dependency graph and lets Syft discover Cargo packages from -the binary itself. It is not a complete image SBOM and is not an OCI SBOM -attestation; publishing such an attestation remains separate work. +Pushed gateway and supervisor images carry an SPDX SBOM and minimal SLSA provenance as OCI attestations. Branch E2E, Release Dev, and Release Tag image binaries embed cargo-auditable metadata, so their image SBOMs include linked Rust crates. ## Prerequisites - `mise install` has been run (installs Syft and other tools) - The repository is checked out at the root +## Inspecting an Image SBOM + +BuildKit uses its default Syft scanner and attaches one SPDX document per platform. Read one without pulling the image: + +```bash +docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest \ + --format '{{ json (index .SBOM "linux/amd64").SPDX }}' +``` + +Validate the final attestation, requiring a Cargo package for an auditable image: + +```bash +tasks/scripts/verify-image-sbom.sh ghcr.io/nvidia/openshell/gateway:latest --require-cargo +``` + ## Inspecting an Auditable Image Binary Opt into auditable metadata when staging a local image binary: diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index 2b4d9d5d46..659027a8d6 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -86,6 +86,7 @@ jobs: with: component: gateway image-tag: ${{ github.sha }} + auditable: true build-supervisor: needs: [pr_metadata] @@ -97,6 +98,7 @@ jobs: with: component: supervisor image-tag: ${{ github.sha }} + auditable: true build-cli: needs: [pr_metadata] diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2e7abe2008..3563e667a5 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -294,6 +294,10 @@ jobs: volumes: - /var/run/docker.sock:/var/run/docker.sock steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs['checkout-ref'] || github.sha }} + - name: Log in to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin @@ -309,3 +313,6 @@ jobs: --prefer-index=false \ -t "${image}:${{ needs.resolve.outputs.image_tag_base }}" \ "${refs[@]}" + + - name: Verify merged manifest SBOM attestation + run: tasks/scripts/verify-image-sbom.sh "ghcr.io/nvidia/openshell/${{ inputs.component }}:${{ needs.resolve.outputs.image_tag_base }}" ${{ inputs.auditable && '--require-cargo' || '' }} diff --git a/.github/workflows/rust-native-build.yml b/.github/workflows/rust-native-build.yml index 3496f4779f..24124d0ae4 100644 --- a/.github/workflows/rust-native-build.yml +++ b/.github/workflows/rust-native-build.yml @@ -40,6 +40,8 @@ on: required: false type: string default: "" + # Branch E2E and release gateway/supervisor image builds opt in. + # CLI images and standalone glibc-static supervisor validation keep false. auditable: description: "Embed cargo-auditable dependency metadata in the binary" required: false diff --git a/architecture/build.md b/architecture/build.md index 7daf4a7c66..7e65b987d4 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -131,27 +131,39 @@ step via the `rust-native-build.yml` workflow (per-architecture, per-component) and uploads the result as an artifact that the image build job downloads back into the staging directory before running Buildx. -Gateway and supervisor binaries staged into Release Dev and Release Tag images -are compiled through `cargo auditable` (pinned in `mise.toml`), which embeds a -`.dep-v0` section describing the Rust dependencies actually compiled into the -binary. That section holds data rather than symbols, so it survives the +Gateway and supervisor binaries staged into branch E2E, Release Dev, and Release +Tag images are compiled through `cargo auditable` (pinned in `mise.toml`), which +embeds a `.dep-v0` section describing the Rust dependencies actually compiled +into the binary. That section holds data rather than symbols, so it survives the workspace's `strip = true` release profile, and Syft can catalog the crates present in image binaries instead of inferring them from the source tree. This is a different artifact from the source SBOM produced by `syft dir:.` in -`tasks/sbom.toml`, which describes the checkout, and from an OCI SBOM -attestation, which remains out of scope. - -Only release image builds are auditable. `docker-build.yml` and -`rust-native-build.yml` take an `auditable` input that defaults to false, so PR -and E2E image builds and standalone release artifacts remain non-auditable. The -CI image gains the pinned `cargo-auditable` tool through `mise install --locked` -but ships no auditable OpenShell binary of its own. Local staging opts in with +`tasks/sbom.toml`, which describes the checkout, and from the image SBOM +attestation below, which describes a published image. + +`docker-build.yml` and `rust-native-build.yml` take an `auditable` input that +defaults to false. The branch E2E gateway and supervisor image builds set it to +true so E2E exercises release-parity image binaries; other PR image builds and +standalone release artifacts stay non-auditable. The CI image gains the pinned +`cargo-auditable` tool through `mise install --locked` but ships no auditable +OpenShell binary of its own. Local staging opts in with `OPENSHELL_AUDITABLE=1`. sccache's `RUSTC_WRAPPER` is unset only around auditable builds, because it would otherwise wrap `cargo-auditable`'s workspace wrapper and be misidentified as `rustc`. Auditable builds are verified by scanning the built binary with Syft and requiring at least one decoded Cargo package; the check runs only for those builds. +Pushed Docker images carry minimal SLSA provenance and a per-platform SPDX SBOM +generated by BuildKit's default Syft scanner. The registry exporter uses OCI +media types and `oci-artifact=true`, so each attestation identifies its subject. +GHCR exposes these through the image index because it has no referrers API. + +Attestations require a registry-backed image index. Local builds therefore keep +`--provenance=false`, and Podman builds carry neither attestation. +`tasks/scripts/verify-image-sbom.sh` verifies the merged multi-arch tag and runs +with `--require-cargo` for auditable builds, so those attestations must also +contain Cargo packages. + Runtime layout: - **Gateway**: `gcr.io/distroless/cc-debian13:nonroot` base, GNU-linked binary at diff --git a/docs/security/verifying-images.mdx b/docs/security/verifying-images.mdx new file mode 100644 index 0000000000..4b9704d3b5 --- /dev/null +++ b/docs/security/verifying-images.mdx @@ -0,0 +1,36 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Verify the Contents of Published OpenShell Images" +sidebar-title: "Verify Image Contents" +slug: "security/verify-image-contents" +description: "Read the SBOM attestation attached to published gateway and supervisor images to audit what each image contains." +keywords: "Generative AI, Cybersecurity, Supply Chain, SBOM, Container Images" +position: 2 +--- + +Published gateway and supervisor images carry one SPDX SBOM per platform as OCI attestations. + +## Inspect an Image + +Read a platform's document without pulling the image: + +```shell +docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest --format '{{ json (index .SBOM "linux/amd64").SPDX }}' +``` + +List the packages instead of the full document: + +```shell +docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest --format '{{ range (index .SBOM "linux/amd64").SPDX.packages }}{{ .name }}@{{ .versionInfo }}{{ println }}{{ end }}' +``` + +The same commands work for `ghcr.io/nvidia/openshell/supervisor`. + +## Coverage + +Every SBOM lists the base-image packages. Release Dev and Release Tag images also list the Rust crates compiled into their OpenShell binary. + + +OpenShell also publishes minimal SLSA provenance. It records how BuildKit produced the image, including its source revision, build platform, and base-image materials, without the extra build parameters included by full provenance. + diff --git a/tasks/scripts/docker-build-image.sh b/tasks/scripts/docker-build-image.sh index 8570180f12..08ba00e066 100755 --- a/tasks/scripts/docker-build-image.sh +++ b/tasks/scripts/docker-build-image.sh @@ -168,14 +168,19 @@ if [[ "${IS_FINAL_IMAGE}" == "1" ]]; then TAG_ARGS=(-t "${IMAGE_NAME}:${IMAGE_TAG}") fi +ATTESTATION_ARGS=(--provenance=false) OUTPUT_ARGS=() if [[ -n "${DOCKER_OUTPUT:-}" ]]; then OUTPUT_ARGS=(--output "${DOCKER_OUTPUT}") elif [[ "${IS_FINAL_IMAGE}" == "1" ]]; then - if [[ "${DOCKER_PUSH:-}" == "1" ]]; then - OUTPUT_ARGS=(--push) - elif [[ "${DOCKER_PLATFORM:-}" == *","* ]]; then - OUTPUT_ARGS=(--push) + if [[ "${DOCKER_PUSH:-}" == "1" || "${DOCKER_PLATFORM:-}" == *","* ]]; then + if ce_is_docker; then + # Attestations require a registry-backed image index. + ATTESTATION_ARGS=(--provenance=mode=min --attest type=sbom) + OUTPUT_ARGS=(--output "type=image,push=true,oci-mediatypes=true,oci-artifact=true") + else + OUTPUT_ARGS=(--push) + fi else OUTPUT_ARGS=(--load) fi @@ -191,7 +196,7 @@ ce_build \ -f "${DOCKERFILE}" \ --target "${DOCKER_TARGET}" \ ${TAG_ARGS[@]+"${TAG_ARGS[@]}"} \ - --provenance=false \ + ${ATTESTATION_ARGS[@]+"${ATTESTATION_ARGS[@]}"} \ "$@" \ ${OUTPUT_ARGS[@]+"${OUTPUT_ARGS[@]}"} \ . diff --git a/tasks/scripts/verify-image-sbom.sh b/tasks/scripts/verify-image-sbom.sh new file mode 100755 index 0000000000..1703b0608a --- /dev/null +++ b/tasks/scripts/verify-image-sbom.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +# Validate the final image's SBOM attestation and, for auditable builds, require +# at least one Cargo package discovered from the binary metadata. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/container-engine.sh" + +usage() { + echo "Usage: verify-image-sbom.sh [--require-cargo]" >&2 +} + +IMAGE=${1:-} +REQUIRE_CARGO=${2:-} +if [[ -z "${IMAGE}" || $# -gt 2 || ( -n "${REQUIRE_CARGO}" && "${REQUIRE_CARGO}" != "--require-cargo" ) ]]; then + usage + exit 2 +fi + +if ! ce_is_docker; then + echo "Error: SBOM attestations are produced on the Docker/buildx path; ${CONTAINER_ENGINE} has no imagetools equivalent" >&2 + exit 2 +fi + +echo "==> Inspecting SBOM attestation of ${IMAGE}" +SBOM_JSON="$(ce buildx imagetools inspect "${IMAGE}" --format '{{ json .SBOM }}')" +COUNTS="$( + jq -r ' + [.. | objects | .SPDX? | select(type == "object")] as $documents + | [ + ($documents | length), + ([$documents[] | .. | strings | select(startswith("pkg:cargo/"))] | length) + ] + | @tsv + ' <<<"${SBOM_JSON}" +)" +read -r SPDX_COUNT CARGO_COUNT <<<"${COUNTS}" + +if [[ "${SPDX_COUNT}" -eq 0 ]]; then + echo "Error: ${IMAGE} carries no SPDX SBOM" >&2 + exit 1 +fi +if [[ "${REQUIRE_CARGO}" == "--require-cargo" && "${CARGO_COUNT}" -eq 0 ]]; then + echo "Error: ${IMAGE} SBOM contains no Cargo packages" >&2 + exit 1 +fi + +echo "SBOM attestation verified: SPDX=${SPDX_COUNT}, Cargo=${CARGO_COUNT}"