← THE INDEX  ·  DETECTION ENG

CI/CD Supply Chain Security

Proves the artifact, not just the source: keyless Sigstore signing, SBOM attestation, and a Kyverno policy that enforces both at cluster admission.

CI/CD Supply Chain Security

The problem

The secure CI/CD pipeline proves the source is clean. This repo proves the artifact is: that the container image a cluster is about to run was built by this pipeline, hasn't been tampered with since, and ships with a verifiable bill of materials. Without artifact signing, a clean source scan and a build step are not a complete chain of custody: anything that can push to your registry can substitute an image between build and deploy.

The mechanism is Sigstore. The pipeline signs every image keylessly with Cosign using the workflow's OIDC identity, attaches an SPDX SBOM as a signed attestation, and a Kyverno policy refuses to admit anything to the cluster that can't produce both.

Keyless signing

There is no private key to manage or leak. Cosign requests a short-lived certificate from Fulcio bound to the GitHub Actions OIDC identity, signs the image digest, and logs the signature to the Rekor transparency log. Verification checks the certificate identity and issuer rather than a key you have to rotate, store securely, and recover if lost.

Everything operates on the image digest, never a mutable tag. The thing that gets signed is exactly the thing that was built. A tag can be moved after signing; a digest can't.

policy/kyverno-verify-images.yaml: ClusterPolicy enforcing signature + SBOM at admission
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-signed-images
spec:
  validationFailureAction: Enforce
  failurePolicy: Fail
  background: false
  rules:
    - name: check-cosign-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "ghcr.io/<org>/*"
          attestors:
            - count: 1
              entries:
                - keyless:
                    subject: "https://github.com/<org>/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev
          mutateDigest: true
          required: true
    - name: require-sbom-attestation
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "ghcr.io/<org>/*"
          attestations:
            - type: https://spdx.dev/Document
              attestors:
                - count: 1
                  entries:
                    - keyless:
                        subject: "https://github.com/<org>/*"
                        issuer: "https://token.actions.githubusercontent.com"

The pipeline stages

Four jobs run sequentially after build:

  1. scan: syft generates an SPDX SBOM; grype scans the image against it and fails the build on any high or critical advisory
  2. sign: Cosign signs the image digest keylessly using the workflow's OIDC identity; the signature is logged to Rekor
  3. attest: Cosign attaches the SPDX SBOM as a signed attestation on the image
  4. verify: Cosign verifies both the signature and the attestation in-pipeline before the run is marked successful

The Kyverno policy then enforces the same checks at admission: a signed image from the pipeline is admitted; an arbitrary nginx:latest is rejected. policy/test/pods.yaml has one of each for kyverno apply to test against, which is what the admission-policy-check workflow runs on every PR that touches the policy file.

Why keyless is the right default for CI: there is no private key to store, rotate, or recover. The trust anchor is the GitHub OIDC identity that already exists. The workflow asks for id-token: write and Cosign handles the rest. The transparency log means every signature is auditable after the fact.