Skip to main content
TruStacks
← All posts

Don't trust our supply chain. Verify it

TruStacks6 min read

To verify a vendor's container image signature, run cosign verify with two strings the vendor publishes: the certificate identity of the release workflow allowed to sign, and the OIDC issuer that vouched for it. A green result means that workflow signed that exact image digest and nothing has changed since. Here is the TruStacks command, and the output we got running it.

To verify a vendor's container image, run cosign verify with two strings the vendor publishes: the certificate identity of the release workflow allowed to sign, and the OIDC issuer that vouched for it. A green result means that workflow signed that exact image digest, and nothing has changed since. For TruStacks, the command and our own output are below.

That is the whole post, really. The rest is the output, and what it does and does not prove.

The reason it exists: we ask you to run our runner inside your cluster, reading your repositories and opening pull requests against your platform repo. That is a real trust ask. A vendor making it owes you a way to check them that does not route through their marketing department — including this one.

The command

To check the signature you need cosign and nothing else. No account, no registry login, no key from us.

cosign verify \
  --certificate-identity-regexp \
    'https://github.com/TruStacks/trustacks-mvp/.github/workflows/publish-images.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/trustacks/runner:latest

There is no --key flag because there is no key. The runner, control-plane, and UI images and the constitution policy bundle are signed with Sigstore keyless signing: at release time, GitHub Actions proves to Sigstore's certificate authority which workflow is running, the authority issues a short-lived certificate naming that workflow, and the signature goes into the public Rekor transparency log. You are not checking a key we could lose. You are checking an identity.

The two strings say exactly which identity is allowed. The issuer is GitHub Actions. The identity is one workflow file, run from a semver release tag — the $ at the end of the regexp means a build from a branch, or from any other workflow, does not match.

What we got

We ran that command on 6 October 2026 with cosign v3.0.6 and an empty Docker config, so no stored registry credentials could help it along. The checks print first:

Verification for ghcr.io/trustacks/runner:latest --
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

Then the signature payload, as JSON. Trimmed here, in two places: the same certificate fields also appear under their raw OID numbers, and the transparency-log bundle runs to several kilobytes of base64. Both are cut; nothing else is.

[{
  "critical": {
    "identity": { "docker-reference": "ghcr.io/trustacks/runner" },
    "image": { "docker-manifest-digest": "sha256:d2364dd1feb7760b51928fbf41ef11511c59ce3517338722a4ea69067692ba9b" },
    "type": "cosign container image signature"
  },
  "optional": {
    "...": "[trimmed: the same fields under their raw OID numbers]",
    "Bundle": { "...": "[trimmed: signed entry timestamp and Rekor payload]" },
    "Issuer": "https://token.actions.githubusercontent.com",
    "Subject": "https://github.com/TruStacks/trustacks-mvp/.github/workflows/publish-images.yml@refs/tags/v0.2.13",
    "githubWorkflowName": "Publish images to GHCR",
    "githubWorkflowRef": "refs/tags/v0.2.13",
    "githubWorkflowRepository": "TruStacks/trustacks-mvp",
    "githubWorkflowSha": "39ca316de66b5a52b958955b2b4e0bb1adaed3bc",
    "githubWorkflowTrigger": "push"
  }
}]

Read the optional block as a sentence: the publish-images.yml workflow, triggered by a push of tag v0.2.13 at commit 39ca316, signed digest d2364dd…. The certificate authority attested to that, not us.

The same command, with the image name swapped, verified control-plane:latest and ui:latest on the same day. The policy bundle uses its own workflow:

cosign verify \
  --certificate-identity-regexp \
    'https://github.com/TruStacks/trustacks-mvp/.github/workflows/publish-policy.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/trustacks/policy/constitution:latest

That came back green too, signed by publish-policy.yml at v0.2.13. The two identities are not interchangeable: run the runner image against the policy workflow's regexp and cosign refuses it, which is the point of naming the workflow rather than the organization.

:latest only moves on a stable release. Pre-release tags such as 0.2.14-rc22 verify against the same regexp, and in an admission policy you should pin a digest rather than a tag anyway.

Check that the signature covers what you are about to run

A signature over a digest is only useful if it is the digest you pull. Ask the registry directly:

docker buildx imagetools inspect ghcr.io/trustacks/runner:latest \
  --format '{{ json .Manifest.Digest }}'
"sha256:d2364dd1feb7760b51928fbf41ef11511c59ce3517338722a4ea69067692ba9b"

Same digest as docker-manifest-digest above. That digest is a multi-platform index: it names the linux/amd64 and linux/arm64 images, and two attestation manifests alongside them, each by its own digest. Change a byte in any of them and the index digest changes, and the signature no longer matches.

The SBOM and the build record are inside that index

Those attestation manifests are where the software bill of materials and the build provenance live. They are attached by the build itself, not added later, and you read them from the registry without asking us for anything:

docker buildx imagetools inspect ghcr.io/trustacks/runner:latest \
  --format '{{ json .SBOM }}' \
  | jq '."linux/amd64".SPDX | {spdxVersion, creators: .creationInfo.creators, packages: (.packages | length)}'
{
  "spdxVersion": "SPDX-2.3",
  "creators": [
    "Organization: Anchore, Inc",
    "Tool: syft-v1.42.3",
    "Tool: buildkit-v0.31.2"
  ],
  "packages": 773
}

Drop the jq filter and you get the full SPDX document for both platforms, ready for whatever scanner your team already runs. The control-plane and UI images carry one too.

The provenance says where the build came from:

docker buildx imagetools inspect ghcr.io/trustacks/runner:latest \
  --format '{{ json .Provenance }}' \
  | jq '."linux/amd64".SLSA.runDetails.metadata.buildkit_metadata.vcs'
{
  "localdir:context": ".",
  "localdir:dockerfile": "runner",
  "revision": "39ca316de66b5a52b958955b2b4e0bb1adaed3bc",
  "source": "https://github.com/TruStacks/trustacks-mvp"
}

revision is 39ca316, the same commit as githubWorkflowSha in the signing certificate. Two independent records — one written by the build, one issued by the certificate authority — agree on what was built.

Note that these are BuildKit attestations, not cosign attestations, so cosign download sbom and cosign verify-attestation will not find them. docker buildx imagetools inspect is the right tool.

What this proves, and what it does not

It proves the image you pull was signed by our release workflow, from a release tag, at a specific commit; that the signing event is in a public transparency log; and that nothing in the signed index, including the SBOM and provenance, has changed since.

It does not prove our code is free of vulnerabilities. A signature establishes who built something and that it was not altered. It says nothing about whether it was built well. That is what the SBOM is for — feed it to your own scanner and judge for yourself.

It does not let you read the workflow. The repository the certificate names is private, so you cannot browse that commit or the workflow file behind it. What you can check is that the signature was issued to that workflow, by a certificate authority that is not us.

It does not carry a SLSA level. The provenance is there and you have just read it. We have not published a SLSA build level, and we will not until we can stand behind the number.

What a failure looks like

The most common one is self-inflicted. The identity regexp is case-sensitive, and the certificate records the organization as TruStacks. Type it in lowercase and you get:

Error: no matching signatures: none of the expected identities matched what was in the certificate, got subjects [https://github.com/TruStacks/trustacks-mvp/.github/workflows/publish-images.yml@refs/tags/v0.2.13] with issuer https://token.actions.githubusercontent.com

Fix the casing and run it again. The supply-chain verification reference covers the other causes, and the same two strings for an admission controller.

If it fails for you

If you run these commands with the correct strings and do not get a green result, do not run the image, and tell us through our contact form. It is the route our security.txt publishes for security reports. A failed verification is a finding we want, not a support ticket.

Agents propose. Policy decides. Humans approve. And you check the people selling you that.

Every command and every output on this page was run on 6 October 2026, with cosign v3.0.6 and no registry credentials. Digests and tags will move with future releases; the commands will not. The same commands live on /product/security, and the facts page records when each supply-chain claim was last verified.

  • supply chain security
  • Sigstore
  • cosign
  • SBOM
  • AI delivery governance

See it open a pull request.

The crew reads a real repository and proposes a policy-checked pull request you review yourself. Hosted Beta signup is open, capped at 100 teams. Create your workspace at app.trustacks.com, and the hosted quickstart walks you to your first governed pull request.