← Back to blog
SecurityKyvernoKubernetesCI/CD

Supply Chain Security: Image Signing with Cosign and Kyverno Admission Policies

· 7 min read

Your container registry is a trust boundary. Anyone with push access — a developer, a compromised CI runner, a leaked token — can place an arbitrary image there. Without verification at deploy time, that image lands in your production cluster. This is the supply chain attack surface, and it is wider than most teams realize.

The fix is straightforward in concept: sign images after building them, then refuse to run anything unsigned. This post covers the exact tooling and configuration to make that work: Cosign for signing in CI/CD, and Kyverno for enforcing signatures at Kubernetes admission time.

If you have been following our series, this builds directly on the patterns from our ArgoCD production setup, the Kubernetes security hardening checklist, and the CI/CD pipeline architecture in our end-to-end GitOps pipeline post.

graph LR
    A[Source Code] --> B[CI Build - Kaniko]
    B --> C[Sign - Cosign]
    C --> D[Push to Registry]
    D --> E[ArgoCD Deploy]
    E --> F{Kyverno Verify}
    F -->|Valid| G[Pod Admitted]
    F -->|Invalid| H[Pod Rejected]

The Problem: Unsigned Images Are Implicit Trust

Consider a typical pipeline: GitLab CI builds a container image with Kaniko, pushes it to a registry, and ArgoCD deploys it to Kubernetes. At no point does anyone verify that the image running in production was actually produced by your CI pipeline. The cluster trusts the registry. The registry trusts anyone with credentials.

Attack vectors include:

  • Compromised developer credentials — an attacker pushes a backdoored image to your registry under an existing tag.
  • CI pipeline manipulation — a malicious merge request modifies the build step to inject code before the image is built.
  • Registry compromise — the registry itself is breached and image layers are tampered with.

Image signing closes this gap. A cryptographic signature ties an image digest to a private key that only your CI pipeline controls. The cluster then verifies that signature before admitting any workload.

Cosign: Signing Images in CI/CD

Cosign is part of the Sigstore project. It signs OCI artifacts (container images) and stores the signatures alongside the image in the same registry — no separate infrastructure required.

Generate a Key Pair

cosign generate-key-pair

This produces cosign.key (private, keep secret) and cosign.pub (public, distribute to clusters). Store the private key as a CI/CD variable. The public key goes into your Kyverno policy.

Sign an Image

cosign sign --key cosign.key registry.example.com/myapp:v1.2.3@sha256:abc123...

Cosign attaches the signature to the registry as a separate tag. Verification only needs the public key and the image reference.

Full GitLab CI Pipeline: Build and Sign

Here is a complete .gitlab-ci.yml that builds an image with Kaniko and signs it with Cosign. The signing step runs only after the build succeeds and only on the default branch.

stages:
  - build
  - sign

variables:
  IMAGE: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}

build:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:v1.23.2-debug
    entrypoint: [""]
  script:
    - >
      /kaniko/executor
      --context "${CI_PROJECT_DIR}"
      --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
      --destination "${IMAGE}"
      --digest-file /tmp/image-digest
  artifacts:
    paths:
      - /tmp/image-digest
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

sign:
  stage: sign
  image: bitnami/cosign:2.4.1
  dependencies:
    - build
  before_script:
    - echo "${COSIGN_PRIVATE_KEY}" > /tmp/cosign.key
  script:
    - DIGEST=$(cat /tmp/image-digest)
    - >
      cosign sign
      --key /tmp/cosign.key
      --yes
      "${CI_REGISTRY_IMAGE}@${DIGEST}"
  after_script:
    - rm -f /tmp/cosign.key
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Key details:

  • Kaniko writes the image digest to a file. The sign stage uses that digest — never a mutable tag — to ensure you sign exactly what was built.
  • COSIGN_PRIVATE_KEY is a masked, protected CI/CD variable containing the private key.
  • COSIGN_PASSWORD (if your key is passphrase-protected) should also be a masked variable.
  • The --yes flag confirms the upload of the signature to the registry without interactive prompts.

Kyverno: Kubernetes-Native Policy Enforcement

Kyverno is a policy engine designed specifically for Kubernetes. Unlike OPA/Gatekeeper, which requires learning Rego, Kyverno policies are written as Kubernetes resources in plain YAML. It operates as an admission webhook — it intercepts API requests before they are persisted and can validate, mutate, or generate resources.

Install it with Helm:

helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace \
  --set replicaCount=3

Running three replicas is important for availability. If Kyverno is down and configured in Fail mode, no pods can be scheduled.

The Policy: Only Allow Signed Images

This ClusterPolicy verifies that every container image in every pod is signed with your specific public key:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  background: false
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----
          mutateDigest: true
          verifyDigest: true
          required: true

What each field does:

  • validationFailureAction: Enforce — unsigned images are rejected, not just logged. Use Audit first to test without breaking anything.
  • imageReferences — scope which images require signatures. You probably do not want to verify upstream images like nginx or redis (unless you re-sign them).
  • mutateDigest: true — Kyverno rewrites the image reference to use the digest, preventing tag mutation attacks after verification.
  • verifyDigest: true — ensures the digest in the signature matches the actual image.

What Happens When an Unsigned Image Is Deployed

When someone tries to create a pod with an unsigned image, Kubernetes returns a clear rejection:

Error from server: admission webhook "mutate.kyverno.svc-fail" denied the request:

resource Pod/default/myapp was blocked due to the following policies:

verify-image-signatures:
  verify-cosign-signature: |-
    image verification failed for registry.example.com/myapp:latest:
    signature not found

The pod is never created. The image never runs. This is enforcement at the API level — it does not matter whether the deployment comes from kubectl apply, a Helm release, or an ArgoCD sync.

Combining with ArgoCD: Full Supply Chain Integrity

When you combine image signing with the ArgoCD GitOps workflow, you get a complete chain of trust:

  1. Git holds the source of truth (application code and Kubernetes manifests).
  2. GitLab CI builds the image and signs it with a private key that only CI possesses.
  3. Git (again) holds the updated image digest in the deployment manifests — committed via CI automation.
  4. ArgoCD detects the manifest change and syncs it to the cluster.
  5. Kyverno verifies the image signature before the pod is admitted.

Every link in this chain is verifiable. A compromised registry cannot inject unsigned images. A rogue kubectl apply cannot bypass the policy. If ArgoCD tries to deploy an image that was not signed by your pipeline, the sync fails and ArgoCD reports the error.

This is what real supply chain security looks like — not a checkbox, but a cryptographic guarantee enforced at every boundary.

Practical Considerations

Start in Audit mode. Set validationFailureAction: Audit and monitor Kyverno’s policy reports before switching to Enforce. This lets you catch images that legitimately need exemptions (init containers, sidecar injectors, etc.).

Exempt system namespaces. Kyverno supports namespace exclusions. You do not want to block kube-system pods:

spec:
  rules:
    - name: verify-cosign-signature
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
                - kyverno

Rotate keys. Cosign supports multiple public keys in a policy. When rotating, add the new public key before removing the old one, and re-sign recent images with the new key.

Use keyless signing for open source. Cosign supports keyless mode via Fulcio and Rekor (the rest of Sigstore). For internal workloads, key-based signing is simpler and does not depend on external infrastructure.

Conclusion

Supply chain security is not optional. The combination of Cosign and Kyverno gives you a practical, production-ready solution: sign in CI, verify at admission, reject everything else. Combined with GitOps through ArgoCD and the hardening practices from our security checklist, you have a cluster where every running workload is traceable back to a verified build.

If you want help implementing image signing across your pipelines or deploying Kyverno policies at scale, get in touch. This is exactly the kind of security infrastructure we build for our clients at robto.