Producing an audit evidence bundle
An auditor asks: "Show me proof that release v1.4.2 was built from main, ran the SAST scanner, produced an SBOM, didn't use unpinned actions, and was signed by an authorized identity."
The answer should not be "let me forward you some Slack screenshots." It should be a single signed archive: every CI/lock attestation produced by the release pipeline, plus the policy that gated promotion, plus the verification report, plus a manifest telling the auditor which file answers which question. This tutorial walks through producing exactly that.
Everything below composes primitives already exercised in the supply-chain defense tutorial, the five-step GitHub Actions pipeline, the container signing tutorial, and the release promotion gate. No new CI/lock features. Just careful packaging.
What goes in the bundle
| Section | What's in it | Comes from |
|---|---|---|
pipeline/ | The 5 per-step DSSE envelopes plus their products (SARIF, SBOM, binary) | The 5-step pipeline run |
container/ | The docker-build attestation (with docker, oci, sbom predicates), the OCI tarball, the image SBOM | The container signing run |
gate/ | The build attestation that was the input to cilock verify, plus its companion public key | The release-promotion-gate run |
policy/ | The signed policy + the public key needed to verify the policy itself | Whatever you used at promotion time |
verify-report.txt | Captured stdout/stderr of a fresh cilock verify run against the included attestations | Re-run at bundle-creation time |
MANIFEST.md | The auditor-question table from below | Generated |
audit-bundle.attestation.json | A signed attestation whose materials are every file above | cilock run --step audit-bundle -- tar czf ... |
The last row is the trick. By wrapping the tar step with cilock run, the bundle's contents become the materials of a new attestation. Anyone re-deriving the tarball later can hash it against audit-bundle.attestation.json and prove the archive hasn't been edited since.
Step 1: Collect attestations for a release
In a real release pipeline you'd query Archivista by commit SHA. For demo-cilock we pull artifacts from the prior workflow runs:
- name: Resolve source runs
id: resolve
env:
GH_TOKEN: ${{ github.token }}
run: |
PIPELINE_ID=$(gh run list -R ${{ github.repository }} \
--workflow=github-actions-pipeline.yml \
--status=success --limit=1 --json databaseId -q '.[0].databaseId')
CONTAINER_ID=$(gh run list -R ${{ github.repository }} \
--workflow=sign-and-verify-container.yml \
--status=success --limit=1 --json databaseId -q '.[0].databaseId')
GATE_ID=$(gh run list -R ${{ github.repository }} \
--workflow=release-promotion-gate.yml \
--status=success --limit=1 --json databaseId -q '.[0].databaseId')
echo "pipeline-id=$PIPELINE_ID" >> "$GITHUB_OUTPUT"
echo "container-id=$CONTAINER_ID" >> "$GITHUB_OUTPUT"
echo "gate-id=$GATE_ID" >> "$GITHUB_OUTPUT"
- name: Download artifacts from each source run
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p bundle/pipeline bundle/container bundle/gate
gh run download ${{ steps.resolve.outputs.pipeline-id }} \
-R ${{ github.repository }} -n pipeline-attestations -D bundle/pipeline
gh run download ${{ steps.resolve.outputs.container-id }} \
-R ${{ github.repository }} -n build-evidence -D bundle/container
gh run download ${{ steps.resolve.outputs.gate-id }} \
-R ${{ github.repository }} -n build-bundle -D bundle/gategh run download requires permissions: actions: read on the workflow.
Step 2: Re-run verify against the assembled inputs
Don't trust that the gate ran. Re-run cilock verify from inside the bundling job and capture the output.
The key ID comes from cilock keyid show, which prints it exactly as cilock verify computes it: the SHA-256 of the key re-encoded as PKIX PEM, not of the file's bytes. The block stops if the ID is not 64 hex characters, because an empty ID would produce a policy that trusts no key. set -o pipefail keeps a failed cilock verify from being hidden by tee. A GitHub Actions run: step without shell: bash runs bash -e without pipefail, so the job would otherwise pass with a failing report.
set -o pipefail
cd bundle/gate
KEYID=$(cilock keyid show signing.pub | awk '{print $1}')
printf '%s' "$KEYID" | grep -Eq '^[0-9a-f]{64}