File: 11-2026-08-12-starting-provenancepipeline.md

Starting ProvenancePipeline: a container tag is not an identity

Published: 2026-08-12 // Root Access: Granted

Starting ProvenancePipeline: a container tag is not an identity

I can type an image name into a Kubernetes manifest and the cluster will try to run it.

That is convenient. It is not much of a trust decision.

A reference such as app:latest does not tell the cluster who built the image, which commit produced it, which workflow ran, whether the contents were scanned, or whether somebody moved the tag after review.

ProvenancePipeline is the last technical project in TrustStack. Its job is to replace that implicit trust with evidence and then make the cluster enforce the result.

The project finishes only when KateClusters accepts the intended image and rejects one that cannot prove its origin.

Keeping the application small

The workload is a small Go service built with the standard library.

I am deliberately avoiding a large application framework. Every dependency adds useful context in a normal software project, but here it would also add packages to the SBOM, findings to the scanner, and unrelated debugging work.

The application needs enough behavior for tests, a health endpoint, and a real container build. It does not need to become the interesting part of the repository.

The Dockerfile will use two stages:

  1. A builder image compiles the binary.

  2. A distroless runtime image receives only the resulting executable and required runtime files.

The final process will run as a non-root user. Base images will be pinned by digest rather than selected only by tag.

Digest pinning creates maintenance work. A pinned base does not receive an update automatically when a tag moves. That is acceptable as long as the project has a deliberate update process. Silent change is the problem I am trying to remove.

The questions the pipeline must answer

I want each artifact to carry enough evidence to answer four separate questions.

First: what is inside the image?

Syft will generate SBOMs in SPDX and CycloneDX formats. The SBOM is a component inventory, not a security score. It gives scanners and reviewers a stable description of the packages associated with a specific image.

Second: does the current vulnerability policy allow this artifact?

Grype will scan the image or its SBOM. The initial gate fails on CRITICAL findings. HIGH and MEDIUM remain visible but do not block this version of the release.

That threshold is a project choice, not a universal definition of acceptable risk. I would rather state it clearly than pretend the scanner has made the decision for me.

Third: who signed the image?

Cosign will use keyless signing. GitHub Actions receives an OIDC token, the Sigstore flow issues a short-lived certificate, and the image digest is signed without a private key stored in the repository.

Fourth: where and how was the artifact built?

SLSA provenance will record the repository, workflow, commit, builder context, and output digest. This proves origin information about the artifact. It does not prove that the source code was safe or that the build job could never be compromised.

The four answers overlap, but none replaces the others.

Keyless does not mean trustless

Removing a stored signing key changes the trust anchor. It does not remove one.

The expected certificate identity will name the exact GitHub release workflow on the main branch of the repository. The expected issuer will be GitHub Actions.

Both values matter.

If I check only the issuer, I could accept an artifact signed by another GitHub workflow. If I check only a similar identity string without the expected issuer, I am not validating the complete certificate context.

The intended verification should be narrow enough that a different branch, repository, or workflow does not qualify by accident.

I will record the final identity from a real signature rather than assuming I remembered the certificate format correctly.

Why verification needs a separate job

The job that builds and signs the image already knows what it intended to create.

That makes it a poor independent witness.

A later job on a clean runner will pull the published artifact and verify:

  1. The image digest.

  2. The signature.

  3. The certificate identity.

  4. The certificate issuer.

  5. The SBOM attestation.

  6. The provenance attestation.

The verification job will also run a deliberately incorrect check. I want the correct identity to pass and an incorrect identity to fail.

A command that returns success for both cases has not created evidence. It has created a green box.

SLSA level without wishful accounting

I expect this design to reach SLSA Build Level 2.

The workflow produces authenticated provenance tied to the artifact, but the same broader job context that builds the image also participates in creating that provenance. I am not using a separately isolated builder that would justify a Level 3 claim.

Calling it Level 2 may look less ambitious in a badge. It is more useful in a technical explanation because it names the remaining trust assumption.

If I later move to an appropriate isolated builder, that can become a measured improvement instead of a retroactive reinterpretation.

CI is only half of the project

It would be easy to stop after Cosign verifies the image.

The registry would contain a signed artifact, the workflow would display green checks, and Kubernetes would still run any unsigned image a manifest referenced.

The enforcement step belongs in KateClusters.

Kyverno will receive an image verification policy. The policy will operate in enforcement mode and fail closed if verification cannot be completed. It will validate the identity and issuer expected from the release workflow.

The positive test is a deployment using the legitimate signed image.

The negative test is an unsigned image, or an image signed by the wrong identity, being denied before a pod starts.

This creates an operational concern: admission now depends on obtaining and verifying the signature material. Registry availability, certificate verification, policy matching, and failure behavior all become part of the deployment path.

That is why the failure policy needs to be an explicit choice rather than a default I discover during an outage.

My build order

I am dividing the project into four passes.

The first pass is the application and container. It includes tests, reproducible build settings, the multi-stage Dockerfile, digest-pinned bases, and non-root execution.

The second pass is artifact inspection. It includes both SBOM formats, vulnerability scanning, stored reports, and a clear blocking threshold.

The third pass is trust metadata. It includes keyless signing, the transparency record, SBOM attestation, provenance, independent verification, and the negative control.

The fourth pass is admission. It installs the policy into KateClusters and exercises the allowed and denied deployments.

I will not write the final Kyverno policy around a guessed certificate identity. The real signature comes first, then verification, then enforcement.

What I will count as finished

ProvenancePipeline is complete when I can start from a commit and trace one exact image digest through build, inventory, scan, signature, provenance, registry, independent verification, and Kubernetes admission.

The evidence must also show the boundary:

  1. The expected artifact passes.

  2. An incorrect identity fails verification.

  3. An unsigned image fails admission.

  4. The valid image still runs under the restrictions already present in KateClusters.

This is not a promise that the software contains no vulnerability. It is a controlled answer to a smaller question:

Was this exact artifact produced through the path I decided to trust?

Today, the cluster cannot answer that.

In two weeks, it should refuse to guess.