Skip to content

Loading

← All projects

Supply Chain Security · 2026

Container Provenance

End-to-end GitOps container provenance pipeline using Sigstore + K8s admission webhooks. Stops npm/registry-style supply-chain attacks via keyless signing - stolen tokens can't publish trusted artifacts without CI OIDC identity.

  • Sigstore
  • Cosign
  • Kubernetes
  • ArgoCD
  • GitHub Actions
  • Helm
  • Golang
  • Lab

The problem

If a CI pipeline is compromised, the attacker can push a malicious image to the same registry path your cluster trusts. The cluster has no way to know - it pulls the image by tag, and the tag points wherever the attacker says.

This is a different class of problem from "the cluster is misconfigured." The cluster is doing exactly what it was told. The trust boundary was upstream, and nothing inside the cluster reaches across it.

The approach

I wanted three guarantees, in order:

  1. Every image gets signed in CI, with no long-lived private key anywhere.
  2. The cluster admission controller refuses to schedule pods backed by unsigned images.
  3. Every signing event is auditable in a transparency log I don't run.

Sigstore covers (1) and (3) directly. Kyverno on the cluster side handles (2) by hooking into the admission webhook.

Architecture

┌─────────────┐   keyless OIDC   ┌────────┐   write   ┌───────┐
│  GitHub CI  │  ─────────────▶  │ Fulcio │ ────────▶ │ Rekor │
└─────┬───────┘                  └────────┘           └───────┘
      │ docker push                                       ▲
      ▼                                                   │ verify
┌─────────────┐                                  ┌────────┴──────────┐
│   Registry  │ ◀── pull during admission ──── │ Kyverno (in-cluster) │
└─────────────┘                                  └───────────────────┘
                                                         │ allow / deny
                                                         ▼
                                                  ┌─────────────┐
                                                  │ kube-apiserver
                                                  └─────────────┘

A pod request hits the API server. Kyverno runs at the validating admission webhook, pulls the image's signature reference, asks Rekor "did this signature happen?", and compares the signing identity to the policy. A mismatch is a hard reject.

Implementation highlights

CI signs the image

permissions:
  id-token: write   # cosign keyless
- run: cosign sign --yes ghcr.io/me/app:${{ github.sha }}

The id-token permission is the magic. GitHub mints a short-lived OIDC token, Fulcio uses it to issue a certificate tied to the workflow identity, and the signature lands in Rekor.

Cluster enforces the policy

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-signature
      match: { resources: { kinds: ["Pod"] } }
      verifyImages:
        - imageReferences: ["ghcr.io/me/*"]
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/me/app/.github/workflows/release.yml@*"
                    issuer: "https://token.actions.githubusercontent.com"

Enforce means a non-matching pod is rejected, not just audited. The keyless attestor pins the policy to a specific workflow file at a specific repository - a different repo signing under the same identity won't satisfy it.

Results

  • Demo: unsigned image → admission rejected with a clear kubectl describe reason. Signed image (after re-tagging) → pod scheduled.
  • Audit trail: every signing event landed in the public Rekor log and was retrievable by image digest.
  • CI overhead: ~3 seconds added to the release job. Negligible.

What I learned

The conceptual win was understanding that "trust" in supply-chain security isn't a property of an image; it's a property of a chain of custody that can be replayed and verified. The image is the artefact; the signature is the receipt; Rekor is the notary. Each part is verifiable on its own, and together they tell you exactly who, what, when.

The production-grade extension list would include attestations (SBOMs, provenance per SLSA), per-repo verification keys, and a policy versioning strategy. But "verified at admission" alone moves the threat model.