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 signor 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).
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"
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 }}
Exercises
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).
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.
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
verifyImagesenforces 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