Back to Distributed Systems & Kubernetes Series

Kyverno Track Part 3: Verify Images & Supply Chain

June 6, 2026 Wasil Zafar 35 min read

Container images are the deployment unit of Kubernetes. Without verification, a compromised image can enter your cluster undetected. Kyverno's verifyImages rules enforce that every image is cryptographically signed and attested before admission — blocking unsigned, tampered, or non-compliant images at the gate.

Table of Contents

  1. Supply Chain Security Concepts
  2. verifyImages Rule
  3. Attestation Verification
  4. Notary v2 / ORAS Signatures
  5. Integration with CI/CD
  6. Exercises
  7. Key Takeaways

Supply Chain Security Concepts

Software supply chain security ensures that the code you write is the same code that runs in production — unmodified, from a trusted source, with known dependencies.

  • Image Signature — A cryptographic proof that the image was built by a trusted entity. Created with cosign sign or Notary v2.
  • Attestation — A signed statement about an image (not the image itself). Examples: "this image passed a vulnerability scan," "this image has an SBOM."
  • SBOM (Software Bill of Materials) — A machine-readable inventory of all packages/dependencies in the image.
  • Provenance — Metadata proving where and how the image was built (build system, source repo, commit SHA).
  • Sigstore — An ecosystem of tools: cosign (signing), Rekor (transparency log), Fulcio (keyless certificates).
Defense in Depth: Signing images in CI/CD (supply side) + verifying images in Kubernetes admission (demand side) = complete coverage. Even if an attacker pushes a malicious image to your registry, Kyverno rejects it because it lacks a valid signature.

verifyImages Rule

Kyverno's verifyImages rule type checks image signatures at admission time. If the signature is missing or invalid, the pod is rejected.

Cosign Key-Based Verification

# First, generate a cosign key pair (done once)
cosign generate-key-pair

# Sign an image after building it in CI
cosign sign --key cosign.key ghcr.io/myorg/myapp:v1.2.3

# Verify manually (optional — Kyverno does this automatically)
cosign verify --key cosign.pub ghcr.io/myorg/myapp:v1.2.3
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
  annotations:
    policies.kyverno.io/title: Verify Image Signatures
    policies.kyverno.io/category: Supply Chain Security
    policies.kyverno.io/severity: high
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExxxxxxxxxxxxxx
                      xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                      -----END PUBLIC KEY-----

When a pod references ghcr.io/myorg/myapp:v1.2.3, Kyverno fetches the cosign signature from the registry and verifies it against the public key. If verification fails, the pod is rejected.

Cosign Keyless Verification (Fulcio + OIDC)

Keyless signing uses short-lived certificates from Fulcio, tied to an OIDC identity (e.g., GitHub Actions identity). No long-lived keys to manage.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-keyless-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-github-actions-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - count: 1
              entries:
                - keyless:
                    subject: "https://github.com/myorg/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: "https://rekor.sigstore.dev"
Keyless Benefits: No key management, no key rotation, no secrets to store. The signing identity is the CI/CD workflow itself (verified via OIDC). The transparency log (Rekor) provides an immutable audit trail.

Attestation Verification

Beyond "who signed it," attestations answer "what was verified about it." Kyverno can check that specific attestations exist and contain expected values.

Verify SBOM Attestation Exists

# Generate and attach SBOM in CI
syft ghcr.io/myorg/myapp:v1.2.3 -o spdx-json > sbom.spdx.json

# Attest the SBOM (signs and attaches to image)
cosign attest --key cosign.key \
  --predicate sbom.spdx.json \
  --type spdxjson \
  ghcr.io/myorg/myapp:v1.2.3
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-sbom-attestation
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-sbom-exists
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestations:
            - type: https://spdx.dev/Document
              attestors:
                - entries:
                    - keys:
                        publicKeys: |-
                          -----BEGIN PUBLIC KEY-----
                          MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExxxxxx
                          -----END PUBLIC KEY-----
              conditions:
                - all:
                    # Verify SBOM has content (spdxVersion field exists)
                    - key: "{{ spdxVersion }}"
                      operator: Equals
                      value: "SPDX-2.3"

Verify Vulnerability Scan Passed

# Run vulnerability scan and create attestation in CI
trivy image --format cosign-vuln ghcr.io/myorg/myapp:v1.2.3 > vuln-report.json

# Attest the vulnerability scan results
cosign attest --key cosign.key \
  --predicate vuln-report.json \
  --type vuln \
  ghcr.io/myorg/myapp:v1.2.3
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-vuln-scan
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-no-critical-vulns
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestations:
            - type: https://cosign.sigstore.dev/attestation/vuln/v1
              attestors:
                - entries:
                    - keys:
                        publicKeys: |-
                          -----BEGIN PUBLIC KEY-----
                          MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExxxxxx
                          -----END PUBLIC KEY-----
              conditions:
                - all:
                    # No critical vulnerabilities allowed
                    - key: "{{ scanner.result.summary.criticalCount }}"
                      operator: Equals
                      value: 0

Notary v2 / ORAS Signatures

Notary v2 (also called Notation) is an alternative signing framework that stores signatures as OCI artifacts alongside images. Kyverno supports Notary v2 verification natively.

# Sign with Notation (Notary v2)
notation sign ghcr.io/myorg/myapp:v1.2.3 \
  --key my-signing-key \
  --signature-format cose

# Verify
notation verify ghcr.io/myorg/myapp:v1.2.3 \
  --trust-policy trustpolicy.json
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-notation-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-notary-v2
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          type: Notary
          attestors:
            - count: 1
              entries:
                - certificates:
                    cert: |-
                      -----BEGIN CERTIFICATE-----
                      MIIBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                      -----END CERTIFICATE-----
                    certChain: |-
                      -----BEGIN CERTIFICATE-----
                      MIICxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                      -----END CERTIFICATE-----

Integration with CI/CD

The signing happens in your CI/CD pipeline. The verification happens in Kubernetes. Here's a typical GitHub Actions workflow that signs and attests:

# .github/workflows/build-sign.yaml
name: Build, Sign, and Attest
on:
  push:
    tags: ['v*']

permissions:
  contents: read
  packages: write
  id-token: write  # Required for keyless signing

jobs:
  build-and-sign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build and push image
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/myorg/myapp:${{ github.ref_name }}

      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign image (keyless)
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          cosign sign --yes ghcr.io/myorg/myapp:${{ github.ref_name }}

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: ghcr.io/myorg/myapp:${{ github.ref_name }}
          output-file: sbom.spdx.json

      - name: Attest SBOM
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          cosign attest --yes \
            --predicate sbom.spdx.json \
            --type spdxjson \
            ghcr.io/myorg/myapp:${{ github.ref_name }}

      - name: Run Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/myorg/myapp:${{ github.ref_name }}
          format: cosign-vuln
          output: vuln-report.json

      - name: Attest vulnerability scan
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          cosign attest --yes \
            --predicate vuln-report.json \
            --type vuln \
            ghcr.io/myorg/myapp:${{ github.ref_name }}
End-to-End Flow: CI builds image → cosign signs → syft generates SBOM → cosign attests SBOM → trivy scans → cosign attests scan results → image pushed to registry. At deploy time, Kyverno verifies signature + attestations → pod admitted (or rejected).

Exercises

Exercise 1: Generate a cosign key pair locally. Build a container image, push to a registry, and sign it with cosign sign. Then write a Kyverno ClusterPolicy with verifyImages that requires your public key. Deploy a pod using your signed image (should succeed) and an unsigned image (should fail).
Exercise 2: Extend Exercise 1 by adding an SBOM attestation requirement. Generate an SBOM with syft, attest it with cosign attest, and add an attestation check in your Kyverno policy. Verify that pods are rejected when the SBOM attestation is missing.
Exercise 3: Configure a keyless signing policy that requires images to be signed by your GitHub Actions workflow. Use the keyless attestor with subject matching your GitHub org and issuer set to GitHub's OIDC provider. Push a manually signed image (wrong identity) and verify it's rejected by Kyverno.

Key Takeaways

  • verifyImages enforces cryptographic signatures at admission — unsigned images are rejected
  • Key-based signing uses a static public/private key pair; keyless uses OIDC identity + Fulcio certificates
  • Attestations verify properties about images: SBOM existence, vulnerability scan results, provenance
  • Notary v2 is supported as an alternative to cosign for organizations using that ecosystem
  • The pattern is: sign in CI → verify in cluster — complete coverage with zero manual steps
  • Combine with Trivy (vulnerability scanning) for a comprehensive supply chain security posture