Image signing and SBOM
A software bill of materials (SBOM) is a complete inventory of the components, libraries, and dependencies contained in a piece of software. SBOMs are a core building block for supply-chain security: they let you audit what ships inside an image, match it against vulnerability databases, and satisfy compliance requirements such as those described in the US Executive Order on cybersecurity.
OpsMill signs every released Infrahub container image and publishes a signed SBOM alongside it. The sections below cover how both are produced, how to verify that an image was built by an OpsMill release workflow, and how to retrieve and verify the SBOM for the image you deploy.
How OpsMill generates SBOMs​
SBOMs are produced automatically by the release pipeline whenever an Infrahub image is published. You do not need to generate them yourself — they are attached to the published image and are ready to retrieve.
The pipeline performs the following steps for each release:
- Scan the image: Syft scans the published multi-architecture image and catalogs every package it contains.
- Emit two formats: Syft writes the inventory in both SPDX (
infrahub-sbom.spdx.json) and CycloneDX (infrahub-sbom.cdx.json) JSON. Both are industry-standard formats; most security tooling accepts at least one of them. - Sign as attestations: Cosign attaches each SBOM to the image digest as a signed attestation. Signing is keyless — it uses Sigstore and the release workflow's OpenID Connect identity rather than a long-lived private key, so the attestation is cryptographically tied to the build that produced the image.
- Sign the image: Cosign signs the image digest itself with the same keyless identity. You can verify the image on its own, without retrieving its SBOM.
Because the SBOMs are attested against the image digest, they are attached to the image: pulling the image and retrieving its SBOM always returns the inventory for exactly the bits you are running.
Prerequisites​
To verify an image and retrieve its SBOM you need:
- Cosign v3 or later installed locally. Cosign v2 is not compatible — it emits a different attestation envelope and the verification commands below will not work with it.
jqto extract the SBOM document from the attestation payload.- Access to the registry that hosts the image you deploy, for example
registry.opsmill.io/opsmill/infrahub.
Set the image reference and its signing identity as variables so the commands below can be reused. The community and enterprise images are built from different repositories, so each has its own signing identity.
For the community image:
export IMAGE="registry.opsmill.io/opsmill/infrahub:<version>"
export IDENTITY="^https://github\.com/opsmill/infrahub/\.github/workflows/ci-docker-image\.yml@refs/tags/infrahub-v"
For the enterprise image:
export IMAGE="registry.opsmill.io/opsmill/infrahub-enterprise:<version>"
export IDENTITY="^https://github\.com/opsmill/infrahub-private/\.github/workflows/ci-docker-image\.yml@refs/tags/infrahub-enterprise-v"
Replace <version> with the version you deploy, and verify that reference rather than latest. latest points to a different image after each release, so verifying it does not confirm the image you are running. The released versions are listed on the releases page. Both the signature and the attestations are attached to the image digest, so what you verify matches the version you run.
These patterns match release images, which are signed at the release tag that built them. Every other image is signed at the ref its build ran from, and that ref depends on how the build was triggered. A rolling tag can carry either identity, because more than one workflow writes it: stable is published both by a release and by the nightly build, so which identity it carries depends on which ran last. That is a further reason to verify the version you deploy rather than a rolling tag.
To verify an image that is not a release, run the command with the release pattern first. Verification fails and reports the identity it found, which is the value to pin. Replace the whole ref portion of the pattern, @refs/tags/infrahub-v for the community image or @refs/tags/infrahub-enterprise-v for the enterprise one, with the ref from that output.
Verifying the image signature​
cosign verify confirms that the image was built and signed by an OpsMill release workflow. Run it before you retrieve the SBOM.
cosign verify \
--certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE"
Cosign lists the checks it performed:
Verification for registry.opsmill.io/opsmill/infrahub:<version> --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The code-signing certificate was verified using trusted certificate authority certificates
It then prints the signed payloads as JSON. With cosign v3 and no additional flags, those payloads are the SBOM attestations, listed by the types https://spdx.dev/Document and https://cyclonedx.org/bom. The command confirms the signature, the signing identity and the transparency-log entry, which is what the checks above report. It does not return the SBOM documents themselves. Use cosign verify-attestation below to retrieve those.
If the identity does not match, verification fails with no matching CertificateIdentity found, and the error reports both the pattern it expected and the identity it found. Each pattern names one repository and one tag prefix, so the community pattern rejects an enterprise image and the enterprise pattern rejects a community one.
Verifying and retrieving the SBOM​
Use cosign verify-attestation to confirm the signature is valid and return the SBOM in a single step. Because signing is keyless, you must specify the signing identity and the OIDC issuer.
The jq selector (.payload // .dsseEnvelope.payload) below is format-agnostic: it reads the payload field from the legacy cosign output and falls back to dsseEnvelope.payload for the newer DSSE bundle format that cosign v3 emits.
SPDX​
cosign verify-attestation \
--type spdxjson \
--certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE"
If verification succeeds, cosign prints the signed attestations to standard output. Extract the SPDX document itself with jq:
cosign verify-attestation \
--type spdxjson \
--certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE" \
| jq -r '(.payload // .dsseEnvelope.payload)' | base64 -d | jq '.predicate' > infrahub-sbom.spdx.json
CycloneDX​
The CycloneDX SBOM is retrieved the same way, using the cyclonedx attestation type:
cosign verify-attestation \
--type cyclonedx \
--certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE" \
| jq -r '(.payload // .dsseEnvelope.payload)' | base64 -d | jq '.predicate' > infrahub-sbom.cdx.json
The --certificate-identity-regexp value must match the workflow identity that signed the image. The $IDENTITY patterns in Prerequisites pin verification to the release workflow and tag prefix of a single image. A broader pattern such as ^https://github.com/opsmill/.* also passes, but it accepts any workflow in the opsmill GitHub organization and is a weaker trust policy.
The subject.name field inside an attestation payload is the registry backend hostname, 9r2s1098.c1.gra9.container-registry.ovh.net, rather than registry.opsmill.io. Both names serve the same registry. The attestation is bound to the image digest, which is what cosign checks, so the hostname in that field does not affect verification.
Download without verifying​
To pull the attestation without checking the signature — for example, in an environment that cannot reach the Sigstore transparency log — use cosign download attestation and filter by predicate type:
# SPDX
cosign download attestation "$IMAGE" \
| jq -r 'select((.payload // .dsseEnvelope.payload) | @base64d | fromjson | .predicateType == "https://spdx.dev/Document") | (.payload // .dsseEnvelope.payload)' \
| base64 -d | jq '.predicate' > infrahub-sbom.spdx.json
# CycloneDX
cosign download attestation "$IMAGE" \
| jq -r 'select((.payload // .dsseEnvelope.payload) | @base64d | fromjson | .predicateType == "https://cyclonedx.org/bom") | (.payload // .dsseEnvelope.payload)' \
| base64 -d | jq '.predicate' > infrahub-sbom.cdx.json
cosign download attestation does not verify the signature. Prefer cosign verify-attestation whenever your environment can reach Sigstore so you confirm the SBOM was produced by a trusted OpsMill build.
Inspecting the SBOM​
Once retrieved, the SBOM is a standard JSON document you can feed into your existing tooling. For example, scan it for known vulnerabilities with Grype:
grype sbom:infrahub-sbom.spdx.json
Or list the cataloged packages directly with jq:
jq -r '.packages[].name' infrahub-sbom.spdx.json | sort -u
Reference​
| Artifact | Format | Filename | Standard |
|---|---|---|---|
| SPDX SBOM | SPDX JSON | infrahub-sbom.spdx.json | spdx.dev |
| CycloneDX SBOM | CycloneDX JSON | infrahub-sbom.cdx.json | cyclonedx.org |
Both SBOMs are attached to every published image digest as Cosign attestations and cover the same image; choose whichever format your security tooling consumes.