Skip to content
Stribog

Supply Chain

All writing

Supply Chain Security: SBOM, Sigstore and Admission Control

Kubernetes supply chain security end to end: SBOMs with Syft, SLSA L3 provenance, Sigstore keyless signing, and Kyverno admission control that actually blocks.

Stribog18 min readUpdated 6 Aug 2026

The 2026 Black Duck OSSRA report is not comforting reading. Open source vulnerabilities have roughly doubled over the prior cycle, driven in part by the explosion in AI-generated code that pulls in transitive dependencies without a second thought. The attack surface of the average containerized workload now extends through hundreds of third-party packages, their upstream suppliers, the build pipelines that assembled them, and the registries that stored the result. Any one of those links can be compromised — and historically, the most sophisticated attacks have not touched your application code at all. They have touched the packaging layer upstream of it.

The industry's response has been to mandate Software Bills of Materials. NIS2 requires supply chain security as a hard audit control for essential entities. DORA makes ICT third-party risk a first-class regulatory obligation. The OpenSSF, backed by US federal cybersecurity priorities, has been pushing SBOM generation and artifact signing as the supply chain baseline. All of this is correct. It is also woefully incomplete. Generating an SBOM and dropping it in an artifact store is not a security control. It is a compliance artifact — useful evidence for an auditor, useless at stopping a malicious image from reaching production.

This article builds the complete trust chain that actually closes: source commit → CI build → SLSA L3 provenance attestation → Cosign keyless signing → Rekor transparency log → Kubernetes admission enforcement → runtime drift detection. Real commands, real YAML, and the gaps most implementations leave open. The goal: a container not built from your source, by your CI, in the last known-good state, cannot reach production — a cryptographic guarantee at the admission gate. That guarantee is strongest when you control the forge and CI — Forgejo, Woodpecker, and Zot end to end. On GitHub, self-hosted runners with ARC own the execution layer where signing and SBOM generation happen.

Why Your SBOM Is a JFrog Artifact, Not a Security Control

The phrase "we have SBOMs" has become the supply chain equivalent of "we have a firewall." Both statements are true of almost every breached organization. An SBOM — a structured list of the components in a software artifact — is valuable exactly as far as the consumption pipeline that reads it. Without that pipeline, it is a JSON file sitting in a registry, consulted by nobody, triggering nothing.

The consumption question has three parts. First: who scans the SBOM for known CVEs, and how often? New vulnerabilities are published continuously. An SBOM generated at build time is stale by the next morning. A one-time scan at build time catches what was known then — it does nothing about the CVE-2026-XXXXX published forty-eight hours after deployment. Second: what happens when a critical CVE matches a transitive dependency? If the answer is "an email goes to someone's inbox," your mean time to remediation is measured in weeks, not hours. Third: how does the SBOM connect to admission enforcement? If a vulnerable image is blocked only on the next deployment and the running pod is untouched, you have a detection, not a control.

The open-source-as-method discipline here means treating your supply chain pipeline exactly as you would treat any other critical system: with continuous observability, automated remediation loops, and audit-grade evidence by design — including retained denial logs for the audit window, not only the signed artifacts that ship — not a batch job that runs once a quarter before an audit.

The Complete Trust Chain: Build Provenance, Artifact Signing, Admission Enforcement

The complete trust chain. Every artifact produced at each stage has a cryptographic link to the previous one. The admission controller is the enforcement point — it verifies the Cosign signature and SLSA provenance before allowing a pod to run.

The pipeline has six stages, each producing an artifact the next verifies. Source commit triggers a CI build that yields a container image plus two attestations: SLSA provenance (who built this, from what source, with what inputs) and a CycloneDX SBOM (what is inside). Both are signed with Cosign in keyless mode — no private key, no HSM. The signing event is logged to Rekor with an inclusion proof. The signed image, Cosign signature, SLSA provenance, and SBOM attestation land in the OCI registry together. At deploy time, Kyverno or the Sigstore Policy Controller verifies the signature and SLSA provenance against Rekor and the expected OIDC issuer, then admits or denies the pod. No shared secret: the trust anchor is the OIDC identity of the CI runner.

Generating SBOMs at Scale: Syft, Trivy, and What to Do with the Output

syft (Anchore) and trivy (Aqua Security) are the two production-grade SBOM generators in the CNCF ecosystem. syft produces richer package metadata for multi-language images and integrates cleanly with grype for CVE matching. trivy bundles SBOM generation with its own scanner in a single binary, which simplifies pipeline configuration. Both support CycloneDX 1.5 and SPDX 2.3. For most pipelines, generate CycloneDX — it is the format Dependency-Track and most admission-time SBOM policies understand.

bash
# Generate a CycloneDX SBOM for a container image using syft
# Run after 'docker build' or 'ko build' produces the image
IMAGE="ghcr.io/your-org/your-service:${GIT_SHA}"

syft "${IMAGE}" \
  --output cyclonedx-json=sbom.cdx.json \
  --select-catalogers "+cargo-auditable-binary-cataloger" \
  --source-name your-service \
  --source-version "${GIT_SHA}"

# Keyless cosign attest is GitHub Actions–only (needs the Actions OIDC ambient
# credential). Production path: build-and-attest.yml in the next section
# (login → build → cosign attest --type cyclonedx). Do not paste keyless
# attest into a laptop shell expecting the same identity to work.
# cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "${IMAGE}"

# After CI has written the attestation, verify from any machine with registry read:
cosign verify-attestation --type cyclonedx \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --certificate-identity-regexp "^https://github.com/your-org/your-repo/.github/workflows/build-and-attest.yml@refs/heads/main$" \
  "${IMAGE}"
Syft SBOM generation (any environment). Keyless cosign attest is GitHub Actions–only — use the build-and-attest workflow in the next section; verify-attestation can run anywhere after CI has written the attestation. The --select-catalogers flag enables cargo-auditable-binary-cataloger for Rust binaries that embed dependency metadata via cargo-auditable; Go binaries are covered by the separate go-module-binary-cataloger (already part of syft's default image catalogers).

In GitHub Actions, keyless cosign attest --type cyclonedx stores a signed SBOM attestation on the image digest — same registry namespace, linked by digest, without a separate artifact system that can drift. That path needs the Actions OIDC ambient credential; the production sequence is the build-and-attest workflow in the next section. After CI has attested, retrieve and verify the SBOM with cosign verify-attestation --type cyclonedx from any machine with registry read access (prefer attestations over deprecated attach-sbom tag attachments). For a local demo without OIDC, use a key pair (cosign generate-key-pair / cosign attest --key) and pin verification to that public key instead of the Actions identity.

The second tool in the consumption loop is grype, which takes a Syft SBOM and matches it against vulnerability databases (NVD, GitHub Advisory, OSV). Run grype sbom:./sbom.cdx.json in CI to fail the build on critical findings, and re-scan on a schedule against attested SBOMs (cosign verify-attestation --type cyclonedx, then pipe the predicate into grype) so post-build CVEs still surface. This is the loop-close most teams miss: building clean does not mean staying clean.

SLSA Levels in Practice: Reaching Level 3 with slsa-github-generator in an Afternoon

SLSA level comparison. L2 is achievable with a single GitHub Actions step (actions/attest-build-provenance). L3 — via slsa-github-generator — hardens isolation so repo owners, calling workflows, and other tenants cannot forge provenance; build-platform control-plane compromise remains out of scope for Build L3.

SLSA (Supply chain Levels for Software Artifacts) is an OpenSSF framework that defines a graduated set of guarantees about build provenance. The levels are not arbitrary — each one is designed to close a specific class of attack. SLSA L1 establishes that the build is scripted (no manual steps that could inject artifacts). SLSA L2 adds the requirement that the build runs on a hosted build service and that the provenance is signed by the build service itself, not by the developer — meaning a compromised developer workstation cannot forge the provenance. SLSA L3 adds isolation and ephemerality so a build cannot influence its own provenance signing key material or another build's — provenance is non-forgeable by the repository owner, the calling workflow, or another tenant; compromise of the build platform control plane remains outside what Build L3 covers.

For internal services shipping to your own Kubernetes clusters, L3 is achievable in one workflow addition — and this pipeline demonstrates it. The slsa-github-generator project's generator_container_slsa3.yml reusable workflow produces genuine SLSA L3 provenance: it runs in an isolated, ephemeral environment that the repository owner cannot tamper with, making the provenance non-forgeable by the repository owner, the calling workflow, or another tenant; compromise of the build platform control plane remains outside what Build L3 covers. GitHub's own actions/attest-build-provenance action (generally available as of late 2024) is the simpler path: it produces L2 provenance signed by GitHub's OIDC issuer — a meaningful baseline that closes the developer-workstation compromise class and requires no additional setup beyond a single workflow step. Choose L3 (slsa-github-generator) for regulated, externally-distributed, or high-assurance artifacts; L2 (actions/attest-build-provenance) for internal services where an afternoon's setup budget is the constraint.

yaml
# .github/workflows/build-and-attest.yml
# Produces SLSA L3 provenance via the slsa-github-generator reusable workflow
# Requires: contents:write and id-token:write permissions at the workflow level

name: Build, Attest, Sign

on:
  push:
    branches: [main]

permissions:
  contents: write
  id-token: write      # Required for Sigstore OIDC signing
  packages: write      # Token scope for GHCR; still need docker/login-action below
  attestations: write  # Required for GitHub artifact attestations

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          # Outputs the digest as a step output
          outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
          format: cyclonedx-json
          output-file: sbom.cdx.json
          upload-artifact: true

      - uses: sigstore/cosign-installer@v3

      - name: Sign image
        run: cosign sign --yes "ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}"

      - name: Attest SBOM
        run: cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}"

  provenance:
    needs: [build]
    permissions:
      actions: read
      id-token: write
      packages: write
    # slsa-github-generator reusable workflow — produces L3 signed provenance
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
    with:
      image: ghcr.io/${{ github.repository }}
      digest: ${{ needs.build.outputs.digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}
GitHub Actions workflow: GHCR login, build, Cosign sign, SBOM attest, and SLSA L3 provenance via slsa-github-generator. packages:write alone is not enough — docker/login-action must run before push and before cosign writes signatures or attestations. The generator runs isolated from your repo so the owner and calling workflow cannot forge provenance; build-platform control-plane compromise remains outside Build L3.

Registry authentication is easy to omit and fatal when you do. packages:write authorizes the GITHUB_TOKEN for GHCR, but the runner still needs docker/login-action (or an equivalent docker login) before docker/build-push-action and before cosign sign / cosign attest write signatures and CycloneDX attestations as OCI referrers. Without that login, push and cosign fail with unauthorized — and the Kyverno policy in the next section has nothing to verify, so Enforce mode denies every Pod that matches the rule.

Sigstore, Cosign, and the Transparency Log: Keyless Signing Without the Key Management Tax

The traditional objection to artifact signing was operational: protect a private key, rotate it, trust everyone who can touch it, and revoke-and-re-sign if it leaks. Sigstore keyless signing removes that tax. The trust anchor is the OIDC identity of the signing process — in GitHub Actions, the workflow's OIDC token bound to the run, repository, branch, and commit SHA. Cosign exchanges it with Fulcio for a short-lived signing certificate (about ten minutes), signs the artifact, and logs the event to Rekor. The certificate expires after use; there is no long-lived key to leak. The same path covers artifacts beyond containers: Wasm modules as OCI artifacts for SpinKube or wasmCloud sign with cosign identically, though component-level SBOM provenance for those modules is thinner than for a full image.

bash
# Keyless signing with cosign in a GitHub Actions environment
# COSIGN_EXPERIMENTAL is no longer required as of cosign 2.x
IMAGE_DIGEST="ghcr.io/your-org/your-service@sha256:abc123..."

# Sign the image — cosign auto-detects GitHub OIDC in Actions environment
cosign sign \
  --yes \
  "${IMAGE_DIGEST}"

# Verify signature from outside CI — use the expected OIDC issuer + subject
cosign verify \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --certificate-identity-regexp "^https://github.com/your-org/your-repo/.github/workflows/build-and-attest.yml@refs/heads/main$" \
  "${IMAGE_DIGEST}" \
  | jq '.[0] | {issuer: .optional.Issuer, subject: .optional.Subject, repo: .optional["github-workflow-repository"]}'

# Verify SLSA provenance attestation
cosign verify-attestation \
  --type slsaprovenance \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --certificate-identity "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0" \
  "${IMAGE_DIGEST}" \
  | jq -r '.payload' | base64 -d | jq '.predicate | {builder, buildType, invocation}'
Cosign keyless signing and verification. The --certificate-identity-regexp flag pins verification to a specific workflow path — this is the critical control that prevents a signature from a compromised or forked workflow from passing verification. Be as specific as your policy allows.

Rekor, the Sigstore transparency log, plays a role analogous to Certificate Transparency in the TLS ecosystem. Every signing event is logged as an immutable entry. Verifiers can check that the log inclusion proof is valid — confirming that the signature was recorded at a specific time and has not been tampered with since. More importantly, Rekor enables monitoring: you can subscribe to entries matching your OIDC identity and get alerted if anything is signed with your workflow's identity that you did not authorize. This is the audit trail that closes the circle between signing and detection of unauthorized signing.

The question is not whether your images are signed. The question is whether you will notice when something is signed with your identity that you did not authorize — and whether your admission controller will reject it before it runs.

Enforcing at the Gate: Sigstore Policy Controller vs. Kyverno Image Verification

Two mature options exist for Kubernetes admission-time image verification: the Sigstore Policy Controller (an OpenSSF project) and Kyverno's verifyImages rule. Both are production-grade; the choice is architectural. If you already run Kyverno for general policy enforcement — which you should, as detailed in our capabilities documentation — add image verification to your existing Kyverno cluster policy rather than operating a second admission webhook. If you are greenfield and supply chain verification is your only admission requirement, the Sigstore Policy Controller is a lighter installation.

yaml
# Kyverno ClusterPolicy: enforce signed images with SLSA provenance
# Requires Kyverno 1.13+ with Sigstore verification enabled (verifyImages.failureAction)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
  annotations:
    policies.kyverno.io/title: Require Signed Container Images
    policies.kyverno.io/description: >-
      Blocks pods whose containers reference images that are not signed by
      the expected GitHub Actions OIDC issuer on the main branch. Verification
      includes the Cosign keyless signature and the SLSA L3 provenance attestation.
spec:
  background: false  # Admission-time only — do not scan running pods
  rules:
    - name: verify-image-signature
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [production, staging]
      verifyImages:
        - imageReferences:
            - "ghcr.io/your-org/*"
          failureAction: Enforce
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/your-org/*/.github/workflows/build-and-attest.yml@refs/heads/main"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev
          attestations:
            - type: https://slsa.dev/provenance/v0.2
              attestors:
                - entries:
                    - keyless:
                        subject: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0"
                        issuer: "https://token.actions.githubusercontent.com"
              conditions:
                - all:
                    - key: "{{ invocation.configSource.uri }}"
                      operator: Equals
                      value: "git+https://github.com/your-org/your-repo@refs/heads/main"
                    - key: "{{ builder.id }}"
                      operator: Equals
                      value: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0"
Kyverno ClusterPolicy enforcing signature and SLSA L3 provenance. The policy blocks any pod in the production or staging namespaces whose image was not signed by the expected GitHub Actions workflow on main. The attestation conditions add a second layer: the SLSA v0.2 provenance must pin the source URI to main and the expected generator builder.id — blocking a compromised feature branch from slipping an image through.

The failureAction: Enforce mode is non-negotiable in production. Audit mode logs violations without blocking — it is useful for a migration period, but an admission policy that does not block is not an admission policy. Set a calendar reminder: if your supply chain enforcement is in Audit mode, it has a deadline to flip to Enforce. The thesis behind treating infrastructure security as an engineering practice rather than a compliance posture is exactly this: controls that observe but do not prevent are detection, not protection.

The Drift Problem: Runtime Verification After Images Are Already Running

The SBOM consumption loop. Grype and Dependency-Track run continuously against signed SBOM attestations on each image. A new critical CVE triggers a rebuild, which produces a new signed image. The Kyverno policy then blocks the old digest on the next deployment. KubeVigil's supply-chain checks extend this with cluster-level SBOM auditing.

Admission enforcement is necessary but not sufficient. A pod that passed admission six weeks ago may now be running an image with a known-critical vulnerability discovered yesterday. The signed image was valid when admitted — it remains signed, and Kyverno will not retroactively evict the running pod. This is the drift problem: your running fleet diverges from your current security posture as new vulnerabilities are disclosed.

Runtime drift detection operates on two distinct layers. The first is vulnerability drift: continuously re-scanning the SBOMs of running images against updated CVE databases. This is where grype and Dependency-Track play their role — not just at build time, but on a schedule against digests currently running in the cluster. Collect the live set with kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.status.containerStatuses[*].imageID}{"\n"}{end}' | sort -u (resolved digests, not PodSpec tags). Those imageID values are often runtime-prefixed — for example docker-pullable://ghcr.io/your-org/your-service@sha256:… — and cosign will not accept that string as-is. Strip the scheme so you pass a clean name@sha256:… reference; if the residual is only a bare sha256:… (common with some containerd reports), recombine it with the registry path from .status.containerStatuses[*].image. Then verify the CycloneDX attestation, base64-decode the DSSE payload, extract the in-toto predicate (the SBOM document), and feed it to Grype on stdin or as a file. Or ingest the same attested SBOMs into Dependency-Track. Avoid grype sbom:$(…) (the sbom: scheme expects a filesystem path such as grype sbom:./file.json, not a shell substitution of image names; pipe the SBOM document on stdin instead) and deprecated cosign download sbom.

bash
# Re-scan a running Pod's image from its attested CycloneDX SBOM
# status.containerStatuses[*].imageID is often runtime-prefixed:
#   docker-pullable://ghcr.io/your-org/your-service@sha256:abc...
RAW_ID="docker-pullable://ghcr.io/your-org/your-service@sha256:abc123..."
IMAGE="${RAW_ID#*://}"   # → ghcr.io/your-org/your-service@sha256:abc123...

# Verify attestation (take first match), decode DSSE payload → in-toto predicate (the SBOM), scan with Grype.
# Grype accepts an SBOM on stdin when piped (Anchore docs); file form also works.
cosign verify-attestation --type cyclonedx \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --certificate-identity-regexp "^https://github.com/your-org/your-repo/.github/workflows/build-and-attest.yml@refs/heads/main$" \
  "${IMAGE}" \
  | jq -s -r '.[0].payload' | base64 -d | jq '.predicate' \
  | grype

# Equivalent file path form:
# ... | jq '.predicate' > /tmp/sbom.cdx.json && grype sbom:/tmp/sbom.cdx.json
End-to-end vulnerability re-scan of a live image digest. Strip the runtime imageID prefix, verify the Cosign CycloneDX attestation, base64-decode the payload, extract .predicate, then grype on stdin or sbom:<file>.

The second layer is binary drift: detecting when the contents of a running container differ from the image it was launched from. This is the class of attack that follows a container escape or a supply chain compromise that targets the container runtime rather than the image build. KubeVigil's supply-chain checks include image digest verification against running pods — confirming that what Kubernetes reports as the running image digest matches the expected digest from the registry, and flagging any divergence for investigation. Falco's container drift rules cover the file-system mutation case: detecting writes to the container root filesystem that would indicate post-admission tampering.

Wiring It to NIS2 and DORA Supply Chain Audit Evidence

NIS2 Article 21 requires essential entities to implement measures addressing supply chain security, specifically the security relationship between each entity and its direct suppliers. DORA Article 28 requires financial entities to manage ICT third-party risk, including software components sourced from external parties. Both regulations require *demonstrable* controls — not a policy document, but evidence that the control was active and functioning at audit time. Signing stages in the pipeline above produce cryptographically verifiable artifacts with timestamps and Rekor log entries; admission denials do not — they need retained API or webhook logs, which is why the evidence package below treats build artifacts and Enforce records differently. The same inventory answers a voluntary framework too: NIST CSF 2.0's GV.SC asks that suppliers be "known and prioritized by criticality", and an SBOM is what turns a hand-listed register into a derived one. A third regime reaches the same artifacts from a different direction: the EU Cyber Resilience Act makes an SBOM and a published advisory obligations of whoever *supplies* the product, not of whoever runs it — so the same pipeline output answers to two different obligated parties.

The audit evidence package for a supply chain control under NIS2/DORA should contain: the SBOM for each production image (signed CycloneDX attestations, retrievable by digest), the Cosign signature and verification log (from Rekor), the SLSA provenance attestation (asserting which source commit and which CI workflow produced the image), the Kyverno admission policy in force during the period, and a durable record of Enforce denials — Kubernetes API audit logs and/or Kyverno admission-webhook logs exported to a SIEM or object store for the full audit window (PolicyReports only reflect currently existing admitted resources, not blocked requests; cluster Events default to a ~1h TTL and are not a period denial ledger of which images were blocked). That package is a verifiable chain from source to running workload for artifacts that shipped — but only when denial logging is retained for the audit window; without exported API or webhook logs, Enforce is operationally real and still hard to prove after the fact.

  • SBOM inventory: CycloneDX JSON per image digest, as signed Cosign attestations, retrievable with cosign verify-attestation --type cyclonedx. Satisfies the "know what you are running" requirement.
  • Vulnerability scan results: Grype JSON reports per image, timestamped, stored in your artifact system. Demonstrates continuous monitoring against current CVE databases.
  • Signing provenance: Rekor log entries for every signed image, retrievable by log index or inclusion proof. Provides non-repudiation — the signing event cannot be back-dated or forged.
  • SLSA attestations: Provenance predicates asserting build origin, source digest, and builder identity. Satisfies the build integrity requirement and closes the attribution gap between source and artifact.
  • Admission enforcement record: Kubernetes API audit logs and/or Kyverno admission-webhook logs retained for the audit window — the durable record of which image references Enforce blocked (configure audit Request/RequestResponse level so the Pod object and webhook message are kept). Optionally long-retention kyverno_policy_results counters (the policy/rule-execution series in the Kyverno metrics reference; confirm the exact series name on your Kyverno version's /metrics endpoint — Prometheus client libraries may expose a _total suffix) as operational volume signal, not a per-image ledger. PolicyReport/ClusterPolicyReport only as the current-state record of admitted resources. Cluster Events (default TTL ~1h) are triage aids, not audit-period evidence.
  • Drift detection alerts: Grype scheduled scan results and any CVE alerts triggered against running images. Demonstrates continuous compliance, not point-in-time.

The audit-grade rigor principle is exactly this: controls produce evidence by design, not as an afterthought before the audit. If you have to reconstruct evidence after the fact, the control was not audit-grade — it was a process someone remembered to follow, which means someone else will eventually forget.

The Pipeline Is a System, Not a Checklist

The components — syft, cosign, slsa-github-generator, grype, Kyverno — are mature and free. What most teams miss is systems thinking: wiring them into one control rather than isolated projects. An SBOM that never reaches a CVE scanner. Signing that never reaches admission. Alerts that go nowhere. Those are the failure modes in practice.

The open-source-as-method discipline means treating this pipeline as you would any production system: version-controlled configuration, tested policy changes, monitored operation, and alert paths that reach on-call engineers rather than ticketing queues that drain on a quarterly schedule. The supply chain is as much a production system as the application it delivers. Operate it accordingly.

The pipeline — build → attest → sign → log → enforce → detect — is not aspirational. Tooling is stable; overhead is low once wired. The cost of skipping it is concrete: NIS2/DORA exposure, expanding CVE surface from AI-generated dependency graphs, and the cost of an incident a signed admission policy would have blocked at the gate. Build it, operate it, and trust it — the admission controller is the one gate that will not fail silently.

§FAQ/Common questions

Frequently asked

What is the difference between SLSA Level 1, 2, and 3, and which should I target?

SLSA L1 requires a scripted build with generated (but unauthenticated) provenance. SLSA L2 adds the requirement that provenance is signed by the build service itself, making it non-forgeable by the developer — achievable with GitHub's `actions/attest-build-provenance` in a single step. SLSA L3 requires an isolated, ephemeral build environment so the repository owner, calling workflow, or another tenant cannot forge provenance — this is what `slsa-github-generator`'s `generator_container_slsa3.yml` provides; build-platform control-plane compromise remains outside Build L3. The pipeline in this article achieves L3. For most regulated and externally-distributed artifacts, L3 is the appropriate target; `actions/attest-build-provenance` (L2) is the right starting point when the goal is a quick baseline with minimal setup overhead.

Does Sigstore keyless signing require storing a private key anywhere?

No. Cosign keyless signing uses short-lived certificates issued by Sigstore's Fulcio CA in exchange for a valid OIDC token from your CI environment (GitHub Actions, GitLab CI, Tekton, etc.). The certificate is valid for ten minutes and is used only for a single signing operation. There is no long-lived private key to protect, rotate, or revoke. The trust anchor is the OIDC issuer URL and the identity claim (workflow path, repository, branch ref) — pin these in your admission policy for the strongest verification guarantees.

Should I use the Sigstore Policy Controller or Kyverno for image verification?

If you already run Kyverno for policy enforcement — which is the recommended baseline for any serious Kubernetes security posture — add image verification via Kyverno's verifyImages rule rather than operating a second admission webhook. Kyverno 1.13+ is required for the sample policy in this article — it uses per-rule `verifyImages[].failureAction` (the 1.13 replacement for deprecated `spec.validationFailureAction`) plus Cosign keyless verification and SLSA L3 provenance attestation conditions. The Sigstore Policy Controller is a good choice for greenfield clusters where supply chain verification is the only admission requirement; it is lighter to install but provides a narrower policy surface.

What happens to pods already running when a critical CVE is found in a dependency?

Admission enforcement is point-in-time — it verifies images at pod creation, not continuously during the pod's lifetime. Running pods are not automatically evicted when a new CVE is discovered. The remediation path is: Grype's scheduled scan detects the CVE in the SBOM of the running image, fires an alert, triggers a rebuild workflow producing a new signed image, and the new image is deployed via your normal rollout process. If you version-pin your admission policy to require images newer than a specific date, the old image will be blocked on the next restart or re-deployment. Runtime tools like Falco and KubeVigil provide continuous detection of behavioral anomalies in running pods while the rebuild is in progress.

Is generating an SBOM sufficient for NIS2 supply chain compliance?

No. An SBOM satisfies the inventory requirement but not the monitoring or control requirements. NIS2 Article 21 requires measures that include both knowing what you are running (SBOM) and demonstrating that your supply chain is controlled and monitored. Demonstrable controls require: continuous CVE scanning against stored SBOMs, artifact signing with non-repudiable provenance, admission enforcement that blocks unsigned or unverified images, and audit evidence of each control's operation over time. The SBOM is the foundation, not the completion.

How do I handle images from third-party base registries that I do not sign?

You have three options. First, mirror and re-sign: pull the upstream image, run your own SBOM generation and CVE scan against it, then re-sign it with your workflow identity and push to your internal registry. Admission policies then require your signature on all images, which forces mirroring for any third-party image. Second, allow-list by digest: pin trusted upstream images by SHA256 digest in your admission policy and update the digest when you verify a new upstream version. Third, namespace-scope your signing policy: enforce signing only in production and staging namespaces, allowing development namespaces to pull upstream images directly. Option one is the most secure; option two is the most practical for teams that depend on frequently-updated upstream images.

Kubernetes supply chain security SBOM Sigstore 2026SLSA level 3 GitHub Actions slsa-github-generatorSigstore cosign container signing productionKubernetes admission control image signingsoftware supply chain security pipelineSBOM generation Syft Trivy

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.