Skip to content

Sergei Ozeranskii

Runner-image supply chain: how to verify the image your CI runs on

A runner image is the most privileged artifact in CI. Your code runs inside it: build steps, tests, dependencies pulled from registries, often Docker-in-Docker. If something is slipped into the image — a compromised toolchain, a swapped binary, an extra package — it touches every build that runs on it. So “what exactly is in the image and who built it” isn’t box-ticking hygiene; it’s part of the threat model.

“Trust us” doesn’t cut it here. Trust in the image has to be verifiable: an outsider must be able to independently confirm that the image was built from the stated source, passed a vulnerability scan, and wasn’t tampered with along the way. Let’s break down what such a chain is made of — using our own runner-images (opens in new tab). The repository is public precisely so you can see what’s inside.

An image trust chain: build from a pinned source → trivy CVE gate → cosign keyless signature plus SBOM and SLSA provenance attestations → publish to the registry by digest → admission admits only a signed image → the job runs.
Every link is bound to one digest; the signature is applied only after a green CVE gate.

What we’re actually defending

A supply-chain attack is when malware gets in not through a bug in your code, but through something you trust: a base image, a dependency, the build tool itself. A runner image is an ideal target: it executes other people’s code and carries a whole toolchain (compilers, runtimes, CLIs, browsers). Compromise it once and you reach every build of everyone who runs on it.

The defense rests on three questions, each of which must have a verifiable answer:

  • Integrity — is the image built from exactly what’s stated, with no floating versions?
  • Vulnerabilities — what is known about the CVEs inside, and what was done about them?
  • Provenance — who built this specific image, from which source and in what environment, and hasn’t it been swapped?

Pins: integrity of the chain

A floating tag like :latest means “whatever is under that name today.” For an image that runs other people’s code, that’s unacceptable: you can neither reproduce the build nor prove that tomorrow the same tag won’t hold different contents. So there isn’t a single floating link in the chain:

  • the base is pinned by sha256: digest, not by tag;
  • the toolset (the runner, language runtimes and toolcaches, CLIs, browsers with drivers) is pinned to exact versions, with downloads verified by SHA256/512 or via key-verified apt repositories;
  • every GitHub Action in the pipeline is pinned by commit SHA, not a tag like @v4;
  • published tags are immutablevYYYYMMDD and sha-<commit>, no :latest; the consumer (our ARC scale-set) pins the image as tag@sha256:.

Two forces keep the pins current: Renovate proposes PRs to bump the base digest, action SHAs and toolchain versions as upstream ships fixes, and a weekly rebuild pulls apt patches into the layers. Pins aren’t “freeze and forget” — they’re “change deliberately, one at a time.”

The CVE gate: trivy as a condition of publishing

Every push to main is scanned by A vulnerability scanner: checks an image for known CVEs in OS packages and dependencies. In CI it acts as a gate that blocks publishing on fixable HIGH/CRITICAL findings. against the published digest with --severity HIGH,CRITICAL. The logic is simple and strict: a fixable HIGH/CRITICAL fails the build — the image is not signed, and an unsigned image is not admitted to run by the consumer’s admission policy. In other words, a valid signature by itself means the image passed the CVE gate.

A few deliberate choices keep the gate honest rather than cosmetic:

  • The gate is OS-scoped (--pkg-types os) — the apt/base layer this project patches directly (apt security updates plus a base-digest bump on the weekly rebuild). CVEs inside third-party tools’ bundled dependencies are handled by keeping those tool versions current (Renovate), not by an unmaintainable per-dependency ignore list — the same posture GitHub-hosted images take.
  • --ignore-unfixed — CVEs with no available fix don’t block the build (nothing to fix), but are picked up automatically once upstream ships a patch. That’s exactly what the weekly rebuild is for.
  • No silent suppression. Exceptions live in .trivyignore.yaml (opens in new tab), and each carries an id (the CVE), a statement (the rationale) and an expired_at — the date after which trivy sees the CVE again, forcing a re-triage. Widening the severity filter to hide a problem is not allowed.

Signing and provenance: Sigstore

After a green gate, the image is signed and gets statements about its origin. All of this is An open stack for keyless artifact signing: Fulcio issues a short-lived certificate bound to an OIDC identity, and Rekor is a public, immutable transparency log of signatures., an open keyless-signing stack.

  • The signature is A Sigstore tool for signing and verifying container images and artifacts. In keyless mode the signature is bound to an OIDC identity and needs no long-lived keys. keyless. Fulcio issues a short-lived certificate bound to the build workflow’s GitHub OIDC identity, and the signing event is recorded in the public Rekor transparency log. There are no long-lived private keys to steal.
  • Two Software Bill of Materials — a machine-readable inventory of every component and dependency inside an artifact (image): what is in it and at which versions. Formats: SPDX or CycloneDX.s. An SPDX one from buildkit (sbom: true), embedded in the image’s OCI index as an attestation manifest — it has no signature of its own; its integrity is covered by the image signature (cosign signs the index digest that references it). And a package-level CycloneDX one from syft, attached to the digest as a cosign A signed, machine-readable statement about an artifact (e.g. an SBOM or provenance), bound to its digest and independently verifiable. with a TSA timestamp (RFC 3161 (opens in new tab) — too large for the Rekor transparency log).
  • Supply-chain Levels for Software Artifacts — an industry standard of supply-chain security levels; the levels describe how strongly an artifact's origin is proven. A cryptographically signed statement of an artifact's origin: what built it, from which source and in what environment. In SLSA terms, a build provenance attestation. — a build attestation (actions/attest-build-provenance): what built the image, from which source and in what environment.

The key property: the signature, provenance and SBOM are all bound to the same sha256: digest. What’s verified isn’t “some image with this tag” but this exact byte-for-byte artifact.

How to verify it yourself

This is the whole point of verifiability — anyone can run the commands below, with no access to our infrastructure. Work against the immutable @sha256: digest, not a floating tag.

The signature (needs cosign (opens in new tab)):

IMAGE=ghcr.io/tempusbuild/runner-ubuntu-24.04
cosign verify \
  --certificate-identity-regexp '^https://github\.com/tempusbuild/runner-images/\.github/workflows/build\.yml@refs/heads/main$' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  "${IMAGE}@${DIGEST}"

--certificate-identity-regexp binds the signature to the build.yml workflow on main of this repository, and --certificate-oidc-issuer binds it to GitHub OIDC. Any other identity or issuer is not our build.

SLSA provenance is verified with the gh CLI:

gh attestation verify "oci://${IMAGE}@${DIGEST}" --owner tempusbuild

The SPDX SBOM is embedded in the image’s OCI index — read it with buildkit tooling (docker buildx imagetools inspect "${IMAGE}@${DIGEST}" --format '{{json .SBOM}}'); its integrity comes from the image signature above (gh attestation verify covers provenance, not SPDX). The CycloneDX SBOM is stored as an OCI referrer (not a legacy .att tag) — a naive .att lookup comes back empty, so it needs a referrers-aware command:

cosign verify-attestation \
  --type cyclonedx \
  --insecure-ignore-tlog \
  --use-signed-timestamps \
  --certificate-identity-regexp '^https://github\.com/tempusbuild/runner-images/\.github/workflows/build\.yml@refs/heads/main$' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  "${IMAGE}@${DIGEST}"

--insecure-ignore-tlog only waives the Rekor lookup — identity, signature and the TSA timestamp are still verified. How to pin the digest is in SECURITY.md (opens in new tab).

Keeping it from going stale

Signing an image once isn’t enough — the chain has to be maintained:

  • A weekly rebuild (weekly-rebuild) pulls in fresh apt patches, re-signs the image and refreshes the attestations. The base pin and versions stay the same — only what upstream shipped changes.
  • OpenSSF Scorecard continuously scores the repository’s own posture (branch protection, pins, token permissions), and CodeQL checks the workflows themselves as code.
  • Reports and badges are public: check status and Scorecard are visible right in the repository (opens in new tab).

How it works with us

The tempus.build runner image is public (ghcr.io/tempusbuild/runner-ubuntu-24.04 (opens in new tab), Apache-2.0). You can pull it, inspect it, build it yourself and — most importantly — independently verify the signature, SBOM and provenance by digest. On our side, admission only lets a signed image that passed the CVE gate run; anything unsigned doesn’t start.

Supply chain is a layer orthogonal to runtime isolation. Verifiable provenance answers “what image are we running,” while the hardware boundary answers “what that image can do to the host and its neighbours” — that’s covered in the Kata isolation deep-dive. The full isolation picture and links to the image are on the Security & isolation page.

Honest caveats

  • A signature proves origin and integrity, not the absence of vulnerabilities. It means “we built this from the stated source and it passed the CVE gate,” not “there isn’t a single bug in here.”
  • An OS-scoped CVE gate doesn’t remove vulnerabilities inside the tools themselves — those are handled by keeping versions current, which is a continuous process, not a one-off check.
  • Verification needs tooling (cosign, gh) and working by digest. In return it doesn’t require trusting us — only open Sigstore, GitHub OIDC and cryptography.

← back to the blog