Skip to content
Merged
Show file tree
Hide file tree
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
17 changes: 16 additions & 1 deletion .agents/skills/sbom/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +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:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/branch-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
with:
component: gateway
image-tag: ${{ github.sha }}
auditable: true

build-supervisor:
needs: [pr_metadata]
Expand All @@ -97,6 +98,7 @@ jobs:
with:
component: supervisor
image-tag: ${{ github.sha }}
auditable: true

build-cli:
needs: [pr_metadata]
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -309,3 +313,14 @@ jobs:
--prefer-index=false \
-t "${image}:${{ needs.resolve.outputs.image_tag_base }}" \
"${refs[@]}"

- name: Verify merged manifest SBOM attestation
env:
IMAGE_REF: ghcr.io/nvidia/openshell/${{ inputs.component }}:${{ needs.resolve.outputs.image_tag_base }}
REQUIRE_CARGO: ${{ inputs.auditable }}
run: |
args=("${IMAGE_REF}")
if [[ "${REQUIRE_CARGO}" == "true" ]]; then
args+=(--require-cargo)
fi
tasks/scripts/verify-image-sbom.sh "${args[@]}"
2 changes: 2 additions & 0 deletions .github/workflows/rust-native-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 24 additions & 12 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions docs/security/verifying-images.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Note>
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.
</Note>
15 changes: 10 additions & 5 deletions tasks/scripts/docker-build-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[@]}"} \
.
53 changes: 53 additions & 0 deletions tasks/scripts/verify-image-sbom.sh
Original file line number Diff line number Diff line change
@@ -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 <image-ref> [--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}"
Loading