Sign and SBOM every release
Signing the image and generating an SBOM are the supply-chain steps everyone agrees to and then forgets under deadline. Make them steps in the release workflow, with a verify that won't let an unsigned artifact through.
blog/sign-and-sbomTwo supply-chain steps show up in every security review and disappear in every crunch: sign the artifact, and produce a software bill of materials for it. Nobody disagrees with them. They just live in a runbook nobody opens, so the release that goes out at 6pm on a Friday is the one that's unsigned with no SBOM — exactly the release you'll wish you could attest to three weeks later.
The reliable way to make a step happen is to make it part of the workflow that already runs, with a check at the end that fails if the step didn't. Here's cosign and syft wrapped as ordinary release steps, plus a verify.
The workflow
name: supply-chain
# Wrap the supply-chain steps you keep forgetting: generate an SBOM,
# sign the image, then verify the signature before you call it shipped.
defaults:
image: ghcr.io/acme/api:1.4.0
tasks:
# `orchstep run attest`
attest:
steps:
- name: sbom
func: shell
do: echo "syft {{ vars.image }} -o spdx-json=sbom.json"
- name: sign
func: shell
do: echo "cosign sign --yes {{ vars.image }}"
retry:
max_attempts: 3
interval: "1s"
backoff_rate: 2.0
- name: verify
func: shell
do: echo "cosign verify {{ vars.image }}"
- name: gate
func: assert
args:
condition: '{{ ne vars.image "" }}'
message: "an image reference is required to sign"Swap the echo for the real tools and nothing else changes: syft writes the bill of materials, cosign sign signs the image, and cosign verify checks the signature you just made — each pointed at the same image reference.
Why each piece is here
SBOM first. The sbom step runs syft against the exact image reference you're about to ship. Generating it as part of the release — not in a quarterly audit — means the bill of materials matches the artifact, because they're produced from the same variable in the same run.
Sign with a retry. Signing talks to a registry and a transparency log, and those flake. The retry: block gives cosign sign three attempts with backoff so a transient network blip doesn't fail an otherwise-good release:
retry:
max_attempts: 3
interval: "1s"
backoff_rate: 2.0Verify what you signed. The verify step runs cosign verify immediately after signing. This catches the embarrassing case where the sign step "succeeded" but the signature isn't actually retrievable — you find out in the same pipeline, not when a downstream admission controller rejects the image in prod.
Gate on the inputs. The assert step makes the obvious mistake impossible: signing nothing. It refuses to run the task without an image reference, so an empty --var can't produce a "successful" release that signed an empty string.
- name: gate
func: assert
args:
condition: '{{ ne vars.image "" }}'
message: "an image reference is required to sign"Point it at any build
The image is a variable, so the same task attests whatever your build produced:
orchstep run attest --var image=ghcr.io/acme/api:2.0.0And you can preview the exact commands — fully resolved, real tags and all — before they touch a registry:
orchstep run attest --dry-runWhat you gained
| Concern | Runbook step | OrchStep |
|---|---|---|
| Generate an SBOM | "remember to run syft" | a sbom step every release |
| Sign the image | manual cosign sign | a sign step with retry |
| Flaky registry | re-run by hand | retry: { max_attempts: 3 } |
| Confirm the signature | hope it took | a verify step |
| Sign nothing by mistake | possible | assert on the image ref |
This doesn't replace your signing tooling — cosign and syft still do the cryptography. It makes "we sign and SBOM every release" true by construction instead of true when someone remembers.
Where to go next
- Functions: shell — wrap any CLI tool as a step
- Functions: assert — fail fast on a missing input
- Error Handling — retry, catch, and finally
Install with curl -fsSL https://orchstep.dev/install.sh | sh, then orchstep run attest --dry-run to see the resolved sign-and-verify plan.
curl -fsSL https://orchstep.dev/install.sh | sh