Skip to content

Commit 3362af3

Browse files
committed
feat(build): publish OCI SBOM and provenance attestations
Signed-off-by: Adrien Langou <alangou@nvidia.com>
1 parent c4bc48d commit 3362af3

8 files changed

Lines changed: 150 additions & 22 deletions

File tree

.agents/skills/sbom/SKILL.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,28 @@ The OpenShell SBOM tooling produces source-tree CycloneDX JSON SBOMs using Syft,
1313

1414
SBOMs are **release artifacts only** -- they are generated on demand and not committed to the repository. Output lands in `deploy/sbom/output/` (gitignored).
1515

16-
Release Dev and Release Tag image builds separately embed cargo-auditable
17-
metadata in the staged gateway and supervisor binaries. This metadata describes
18-
the binary's Rust dependency graph and lets Syft discover Cargo packages from
19-
the binary itself. It is not a complete image SBOM and is not an OCI SBOM
20-
attestation; publishing such an attestation remains separate work.
16+
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.
2117

2218
## Prerequisites
2319

2420
- `mise install` has been run (installs Syft and other tools)
2521
- The repository is checked out at the root
2622

23+
## Inspecting an Image SBOM
24+
25+
BuildKit uses its default Syft scanner and attaches one SPDX document per platform. Read one without pulling the image:
26+
27+
```bash
28+
docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest \
29+
--format '{{ json (index .SBOM "linux/amd64").SPDX }}'
30+
```
31+
32+
Validate the final attestation, requiring a Cargo package for an auditable image:
33+
34+
```bash
35+
tasks/scripts/verify-image-sbom.sh ghcr.io/nvidia/openshell/gateway:latest --require-cargo
36+
```
37+
2738
## Inspecting an Auditable Image Binary
2839

2940
Opt into auditable metadata when staging a local image binary:

.github/workflows/branch-e2e.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ jobs:
8686
with:
8787
component: gateway
8888
image-tag: ${{ github.sha }}
89+
auditable: true
8990

9091
build-supervisor:
9192
needs: [pr_metadata]
@@ -97,6 +98,7 @@ jobs:
9798
with:
9899
component: supervisor
99100
image-tag: ${{ github.sha }}
101+
auditable: true
100102

101103
build-cli:
102104
needs: [pr_metadata]

.github/workflows/docker-build.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ jobs:
294294
volumes:
295295
- /var/run/docker.sock:/var/run/docker.sock
296296
steps:
297+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
298+
with:
299+
ref: ${{ inputs['checkout-ref'] || github.sha }}
300+
297301
- name: Log in to GHCR
298302
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
299303

@@ -309,3 +313,6 @@ jobs:
309313
--prefer-index=false \
310314
-t "${image}:${{ needs.resolve.outputs.image_tag_base }}" \
311315
"${refs[@]}"
316+
317+
- name: Verify merged manifest SBOM attestation
318+
run: tasks/scripts/verify-image-sbom.sh "ghcr.io/nvidia/openshell/${{ inputs.component }}:${{ needs.resolve.outputs.image_tag_base }}" ${{ inputs.auditable && '--require-cargo' || '' }}

.github/workflows/rust-native-build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ on:
4040
required: false
4141
type: string
4242
default: ""
43+
# Branch E2E and release gateway/supervisor image builds opt in.
44+
# CLI images and standalone glibc-static supervisor validation keep false.
4345
auditable:
4446
description: "Embed cargo-auditable dependency metadata in the binary"
4547
required: false

architecture/build.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,27 +131,39 @@ step via the `rust-native-build.yml` workflow (per-architecture, per-component)
131131
and uploads the result as an artifact that the image build job downloads back
132132
into the staging directory before running Buildx.
133133

134-
Gateway and supervisor binaries staged into Release Dev and Release Tag images
135-
are compiled through `cargo auditable` (pinned in `mise.toml`), which embeds a
136-
`.dep-v0` section describing the Rust dependencies actually compiled into the
137-
binary. That section holds data rather than symbols, so it survives the
134+
Gateway and supervisor binaries staged into branch E2E, Release Dev, and Release
135+
Tag images are compiled through `cargo auditable` (pinned in `mise.toml`), which
136+
embeds a `.dep-v0` section describing the Rust dependencies actually compiled
137+
into the binary. That section holds data rather than symbols, so it survives the
138138
workspace's `strip = true` release profile, and Syft can catalog the crates
139139
present in image binaries instead of inferring them from the source tree. This
140140
is a different artifact from the source SBOM produced by `syft dir:.` in
141-
`tasks/sbom.toml`, which describes the checkout, and from an OCI SBOM
142-
attestation, which remains out of scope.
143-
144-
Only release image builds are auditable. `docker-build.yml` and
145-
`rust-native-build.yml` take an `auditable` input that defaults to false, so PR
146-
and E2E image builds and standalone release artifacts remain non-auditable. The
147-
CI image gains the pinned `cargo-auditable` tool through `mise install --locked`
148-
but ships no auditable OpenShell binary of its own. Local staging opts in with
141+
`tasks/sbom.toml`, which describes the checkout, and from the image SBOM
142+
attestation below, which describes a published image.
143+
144+
`docker-build.yml` and `rust-native-build.yml` take an `auditable` input that
145+
defaults to false. The branch E2E gateway and supervisor image builds set it to
146+
true so E2E exercises release-parity image binaries; other PR image builds and
147+
standalone release artifacts stay non-auditable. The CI image gains the pinned
148+
`cargo-auditable` tool through `mise install --locked` but ships no auditable
149+
OpenShell binary of its own. Local staging opts in with
149150
`OPENSHELL_AUDITABLE=1`. sccache's `RUSTC_WRAPPER` is unset only around auditable
150151
builds, because it would otherwise wrap `cargo-auditable`'s workspace wrapper and
151152
be misidentified as `rustc`. Auditable builds are verified by scanning the built
152153
binary with Syft and requiring at least one decoded Cargo package; the check runs
153154
only for those builds.
154155

156+
Pushed Docker images carry minimal SLSA provenance and a per-platform SPDX SBOM
157+
generated by BuildKit's default Syft scanner. The registry exporter uses OCI
158+
media types and `oci-artifact=true`, so each attestation identifies its subject.
159+
GHCR exposes these through the image index because it has no referrers API.
160+
161+
Attestations require a registry-backed image index. Local builds therefore keep
162+
`--provenance=false`, and Podman builds carry neither attestation.
163+
`tasks/scripts/verify-image-sbom.sh` verifies the merged multi-arch tag and runs
164+
with `--require-cargo` for auditable builds, so those attestations must also
165+
contain Cargo packages.
166+
155167
Runtime layout:
156168

157169
- **Gateway**: `gcr.io/distroless/cc-debian13:nonroot` base, GNU-linked binary at

docs/security/verifying-images.mdx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
title: "Verify the Contents of Published OpenShell Images"
5+
sidebar-title: "Verify Image Contents"
6+
slug: "security/verify-image-contents"
7+
description: "Read the SBOM attestation attached to published gateway and supervisor images to audit what each image contains."
8+
keywords: "Generative AI, Cybersecurity, Supply Chain, SBOM, Container Images"
9+
position: 2
10+
---
11+
12+
Published gateway and supervisor images carry one SPDX SBOM per platform as OCI attestations.
13+
14+
## Inspect an Image
15+
16+
Read a platform's document without pulling the image:
17+
18+
```shell
19+
docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest --format '{{ json (index .SBOM "linux/amd64").SPDX }}'
20+
```
21+
22+
List the packages instead of the full document:
23+
24+
```shell
25+
docker buildx imagetools inspect ghcr.io/nvidia/openshell/gateway:latest --format '{{ range (index .SBOM "linux/amd64").SPDX.packages }}{{ .name }}@{{ .versionInfo }}{{ println }}{{ end }}'
26+
```
27+
28+
The same commands work for `ghcr.io/nvidia/openshell/supervisor`.
29+
30+
## Coverage
31+
32+
Every SBOM lists the base-image packages. Release Dev and Release Tag images also list the Rust crates compiled into their OpenShell binary.
33+
34+
<Note>
35+
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.
36+
</Note>

tasks/scripts/docker-build-image.sh

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,19 @@ if [[ "${IS_FINAL_IMAGE}" == "1" ]]; then
168168
TAG_ARGS=(-t "${IMAGE_NAME}:${IMAGE_TAG}")
169169
fi
170170

171+
ATTESTATION_ARGS=(--provenance=false)
171172
OUTPUT_ARGS=()
172173
if [[ -n "${DOCKER_OUTPUT:-}" ]]; then
173174
OUTPUT_ARGS=(--output "${DOCKER_OUTPUT}")
174175
elif [[ "${IS_FINAL_IMAGE}" == "1" ]]; then
175-
if [[ "${DOCKER_PUSH:-}" == "1" ]]; then
176-
OUTPUT_ARGS=(--push)
177-
elif [[ "${DOCKER_PLATFORM:-}" == *","* ]]; then
178-
OUTPUT_ARGS=(--push)
176+
if [[ "${DOCKER_PUSH:-}" == "1" || "${DOCKER_PLATFORM:-}" == *","* ]]; then
177+
if ce_is_docker; then
178+
# Attestations require a registry-backed image index.
179+
ATTESTATION_ARGS=(--provenance=mode=min --attest type=sbom)
180+
OUTPUT_ARGS=(--output "type=image,push=true,oci-mediatypes=true,oci-artifact=true")
181+
else
182+
OUTPUT_ARGS=(--push)
183+
fi
179184
else
180185
OUTPUT_ARGS=(--load)
181186
fi
@@ -191,7 +196,7 @@ ce_build \
191196
-f "${DOCKERFILE}" \
192197
--target "${DOCKER_TARGET}" \
193198
${TAG_ARGS[@]+"${TAG_ARGS[@]}"} \
194-
--provenance=false \
199+
${ATTESTATION_ARGS[@]+"${ATTESTATION_ARGS[@]}"} \
195200
"$@" \
196201
${OUTPUT_ARGS[@]+"${OUTPUT_ARGS[@]}"} \
197202
.

tasks/scripts/verify-image-sbom.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
3+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
set -euo pipefail
7+
8+
# Validate the final image's SBOM attestation and, for auditable builds, require
9+
# at least one Cargo package discovered from the binary metadata.
10+
11+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12+
source "${SCRIPT_DIR}/container-engine.sh"
13+
14+
usage() {
15+
echo "Usage: verify-image-sbom.sh <image-ref> [--require-cargo]" >&2
16+
}
17+
18+
IMAGE=${1:-}
19+
REQUIRE_CARGO=${2:-}
20+
if [[ -z "${IMAGE}" || $# -gt 2 || ( -n "${REQUIRE_CARGO}" && "${REQUIRE_CARGO}" != "--require-cargo" ) ]]; then
21+
usage
22+
exit 2
23+
fi
24+
25+
if ! ce_is_docker; then
26+
echo "Error: SBOM attestations are produced on the Docker/buildx path; ${CONTAINER_ENGINE} has no imagetools equivalent" >&2
27+
exit 2
28+
fi
29+
30+
echo "==> Inspecting SBOM attestation of ${IMAGE}"
31+
SBOM_JSON="$(ce buildx imagetools inspect "${IMAGE}" --format '{{ json .SBOM }}')"
32+
COUNTS="$(
33+
jq -r '
34+
[.. | objects | .SPDX? | select(type == "object")] as $documents
35+
| [
36+
($documents | length),
37+
([$documents[] | .. | strings | select(startswith("pkg:cargo/"))] | length)
38+
]
39+
| @tsv
40+
' <<<"${SBOM_JSON}"
41+
)"
42+
read -r SPDX_COUNT CARGO_COUNT <<<"${COUNTS}"
43+
44+
if [[ "${SPDX_COUNT}" -eq 0 ]]; then
45+
echo "Error: ${IMAGE} carries no SPDX SBOM" >&2
46+
exit 1
47+
fi
48+
if [[ "${REQUIRE_CARGO}" == "--require-cargo" && "${CARGO_COUNT}" -eq 0 ]]; then
49+
echo "Error: ${IMAGE} SBOM contains no Cargo packages" >&2
50+
exit 1
51+
fi
52+
53+
echo "SBOM attestation verified: SPDX=${SPDX_COUNT}, Cargo=${CARGO_COUNT}"

0 commit comments

Comments
 (0)