Kubernetes Image Policy Enforcement: Cosign, Notation, and Admission Webhooks

Kubernetes Image Policy Enforcement: Cosign, Notation, and Admission Webhooks

Problem

Without image policy enforcement, any container image from any registry can run in a Kubernetes cluster. A developer can deploy an image from Docker Hub that was built on someone’s laptop. A compromised CI pipeline can push a malicious image to your registry. An attacker who gains deployment access can run a cryptominer image. The cluster has no mechanism to distinguish a trusted, scanned, signed image from a malicious one.

This creates several concrete risks:

  • No provenance verification. There is no proof that an image was built by your CI pipeline, from your source code, with your build configuration. Anyone who can push to the registry can inject arbitrary code.
  • Tag mutability allows silent replacement. Image tags like v1.2.3 can be overwritten. An attacker who compromises registry write access can replace a legitimate image with a backdoored one using the same tag. Pods pulling that tag get the malicious image.
  • Unscanned images enter production. Without admission control, images that failed vulnerability scanning or were never scanned can be deployed.
  • Registry sprawl increases attack surface. Teams pulling from Docker Hub, GitHub Container Registry, Quay, and internal registries create an uncontrolled supply chain with no central verification.

This article covers signing images with cosign and Notation, verifying signatures at admission time with Kyverno and Gatekeeper, restricting allowed registries, and implementing break-glass procedures for emergencies.

Target systems: Kubernetes 1.29+ with Kyverno 1.12+ or Gatekeeper 3.16+. cosign from Sigstore, or Notation from CNCF Notary Project.

Threat Model

  • Adversary: Attacker who can push images to the container registry (via compromised CI credentials), or an insider deploying unauthorized images.
  • Access level: Write access to the container registry, or RBAC permissions to create/update Deployments in one or more namespaces.
  • Objective: Run malicious code in the cluster by deploying a backdoored image, an image with known vulnerabilities, or an image from an untrusted source.
  • Blast radius: Without image policy enforcement, a single compromised registry credential or deployment permission allows arbitrary code execution in the cluster. With enforcement, the attacker must also compromise the signing key or the admission controller to run unauthorized images.

Configuration

Step 1: Sign Images with Cosign

Cosign from the Sigstore project signs container images using either a static key pair or keyless signing via OIDC (identity-based, no key management).

Key-based signing (for private environments):

# Generate a cosign key pair
cosign generate-key-pair
# Creates cosign.key (private, keep secret) and cosign.pub (public, distribute)

# Sign an image after CI build
cosign sign --key cosign.key \
  registry.example.com/web-app:v1.4.2

# Verify the signature
cosign verify --key cosign.pub \
  registry.example.com/web-app:v1.4.2

Keyless signing (using CI identity via Sigstore):

# In a GitHub Actions workflow:
# The OIDC token from GitHub proves the build identity
cosign sign \
  --oidc-issuer=https://token.actions.githubusercontent.com \
  registry.example.com/web-app:v1.4.2

# Verify with identity constraints (no key needed)
cosign verify \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  --certificate-identity=https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main \
  registry.example.com/web-app:v1.4.2

Example CI pipeline (GitHub Actions):

# .github/workflows/build-sign.yaml
name: Build and Sign
on:
  push:
    branches: [main]

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: sigstore/cosign-installer@v3

      - name: Build and push
        run: |
          docker build -t registry.example.com/web-app:${{ github.sha }} .
          docker push registry.example.com/web-app:${{ github.sha }}

      - name: Sign image
        run: |
          cosign sign --yes \
            registry.example.com/web-app:${{ github.sha }}

Step 2: Sign Images with Notation (Alternative)

Notation is the CNCF Notary Project signing tool, using standard X.509 certificates:

# Install notation
curl -Lo notation.tar.gz \
  "https://github.com/notaryproject/notation/releases/download/v1.2.0/notation_1.2.0_linux_amd64.tar.gz"
tar xzf notation.tar.gz -C /usr/local/bin notation

# Add a signing key (from a certificate)
notation key add "ci-signing-key" \
  --plugin "com.example.kms" \
  --id "arn:aws:kms:us-east-1:123456789:key/abcd-1234"

# Sign the image
notation sign \
  --key "ci-signing-key" \
  registry.example.com/web-app:v1.4.2

# Verify the signature
notation verify \
  registry.example.com/web-app:v1.4.2

Step 3: Enforce Signatures with Kyverno

Kyverno is a Kubernetes-native policy engine that runs as an admission webhook. It can verify cosign and Notation signatures before allowing image deployment.

# Install Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace

Policy: require cosign signature (key-based):

# require-image-signature.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-image-signature
spec:
  validationFailureAction: Enforce
  background: true
  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-----

Policy: require keyless signature with identity verification:

# require-keyless-signature.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-keyless-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-keyless-cosign
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keyless:
                    issuer: "https://token.actions.githubusercontent.com"
                    subject: "https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main"
                    rekor:
                      url: "https://rekor.sigstore.dev"

Step 4: Enforce with Gatekeeper (Alternative)

Gatekeeper uses OPA Rego policies with external data for cosign verification:

# registry-allowlist.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedregistries
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRegistries
      validation:
        openAPIV3Schema:
          type: object
          properties:
            registries:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedregistries

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not registry_allowed(container.image)
          msg := sprintf("Image '%v' is from a disallowed registry. Allowed: %v",
            [container.image, input.parameters.registries])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not registry_allowed(container.image)
          msg := sprintf("Init container image '%v' is from a disallowed registry. Allowed: %v",
            [container.image, input.parameters.registries])
        }

        registry_allowed(image) {
          startswith(image, input.parameters.registries[_])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
  name: allowed-registries
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    registries:
      - "registry.example.com/"
      - "registry.k8s.io/"
      - "docker.io/library/"

Step 5: Registry Allowlisting

Combine signing verification with registry restrictions so only images from approved registries, with valid signatures, can run:

# combined-image-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: image-policy
spec:
  validationFailureAction: Enforce
  rules:
    # Rule 1: Only allow approved registries
    - name: restrict-registries
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Images must be from approved registries."
        pattern:
          spec:
            containers:
              - image: "registry.example.com/* | registry.k8s.io/*"
            =(initContainers):
              - image: "registry.example.com/* | registry.k8s.io/*"

    # Rule 2: Require digest pinning (no mutable tags)
    - name: require-image-digest
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Images must use a digest (@sha256:...), not a tag."
        pattern:
          spec:
            containers:
              - image: "*@sha256:*"
            =(initContainers):
              - image: "*@sha256:*"

    # Rule 3: Require signature
    - name: require-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----

Step 6: Emergency Break-Glass Procedure

When a critical security patch must be deployed immediately and the signing infrastructure is unavailable, you need a time-limited exception:

# break-glass-exception.yaml
apiVersion: kyverno.io/v2beta1
kind: PolicyException
metadata:
  name: emergency-deploy-2026-04-22
  namespace: production
  annotations:
    break-glass/requester: "oncall-engineer@example.com"
    break-glass/reason: "CVE-2026-XXXX hotfix, signing infra down"
    break-glass/expires: "2026-04-23T00:00:00Z"
spec:
  exceptions:
    - policyName: require-image-signature
      ruleNames:
        - verify-cosign-signature
  match:
    any:
      - resources:
          kinds:
            - Pod
          namespaces:
            - production
          names:
            - "hotfix-*"
# Apply the exception
kubectl apply -f break-glass-exception.yaml

# Deploy the unsigned hotfix image
kubectl set image deployment/web-app \
  web=registry.example.com/web-app:hotfix-cve-2026 \
  -n production

# After signing infra is restored, sign the image and remove the exception
cosign sign --key cosign.key registry.example.com/web-app:hotfix-cve-2026
kubectl delete -f break-glass-exception.yaml

Create a CronJob that cleans up expired exceptions:

# cleanup-expired-exceptions.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-break-glass
  namespace: kyverno
spec:
  schedule: "0 * * * *"  # Every hour
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: break-glass-cleanup
          containers:
            - name: cleanup
              image: bitnami/kubectl:1.30
              command:
                - /bin/sh
                - -c
                - |
                  NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
                  kubectl get policyexceptions -A -o json | \
                    jq -r ".items[] | select(.metadata.annotations[\"break-glass/expires\"] < \"$NOW\") | \
                    \"\(.metadata.namespace) \(.metadata.name)\"" | \
                    while read ns name; do
                      kubectl delete policyexception "$name" -n "$ns"
                      echo "Deleted expired exception: $ns/$name"
                    done
          restartPolicy: OnFailure

Expected Behaviour

After implementing image policy enforcement:

  • All images deployed to the cluster must come from approved registries
  • Images without a valid cosign or Notation signature are rejected at admission time
  • Kyverno/Gatekeeper returns a clear error message identifying which policy the image violates
  • Images pinned by digest cannot be silently replaced by overwriting a tag
  • Break-glass exceptions allow emergency deployments with time-limited scope
  • Expired break-glass exceptions are automatically cleaned up

Trade-offs

Control Impact Risk Mitigation
Signature verification at admission Every pod creation adds 100-500ms for signature check Slower deployments; potential timeout on large-scale rollouts Cache verification results. Use Kyverno background scanning for existing resources
Digest pinning requirement Developers must update digests instead of tags; no more image: app:latest Developer friction; increased merge conflicts on digest changes Integrate digest resolution into CI. Use tools like kbld or Flux image automation to resolve tags to digests
Registry allowlisting Blocks images from unapproved sources, including debugging tools Engineers cannot quickly pull debug images during incidents Include a curated set of debug images in the approved registry. Maintain an internal mirror of common tools
Break-glass procedure Bypasses signing requirement for emergencies If overused or if exceptions are not cleaned up, the policy becomes ineffective Require manager approval for break-glass. Alert on every PolicyException creation. Auto-expire exceptions
Keyless signing dependency on Sigstore infrastructure Signature verification requires connectivity to Rekor transparency log If Sigstore is unreachable, all deployments fail verification Run a private Sigstore instance (Rekor + Fulcio) for air-gapped or high-availability requirements

Failure Modes

Failure Symptom Detection Recovery
Signing key compromised Attacker can sign arbitrary images that pass verification No immediate detection from the cluster side; requires monitoring signing activity in CI logs Rotate the signing key. Update the Kyverno/Gatekeeper policy with the new public key. Re-sign all images with the new key. Revoke the old key in Rekor
Kyverno webhook unavailable All pod creations fail if failurePolicy is Fail; all pass unchecked if failurePolicy is Ignore Pod creation errors referencing webhook timeout; or sudden absence of policy violations in logs Check Kyverno pod health. The default failurePolicy should be Fail to prevent bypass. Restore Kyverno before deploying new workloads
Signature verification timeout Pod creation blocked with webhook timeout errors; deployments stall Deployment events show admission webhook timeout; Kyverno logs show registry connectivity issues Check network connectivity from Kyverno pods to the container registry and Rekor. Increase webhook timeout if needed
Registry mirror out of sync Images exist in the source registry but not in the approved mirror ImagePullBackOff with “manifest not found” errors Sync the mirror. Configure automatic mirroring for all images used by the cluster
Break-glass exception not cleaned up Unsigned images can be deployed indefinitely in the excepted namespace Audit PolicyException resources; alert on exceptions older than 24 hours Deploy the cleanup CronJob. Manually delete stale exceptions

When to Consider a Managed Alternative

Transition point: Running your own signing infrastructure (key management, transparency logs, admission webhooks) is a significant operational commitment. The signing key is a high-value secret that must be protected, rotated, and backed up. If your team deploys fewer than 20 distinct images, the overhead of a full Sigstore pipeline may exceed the value.

Recommended providers:

  • Snyk (#48): Container security platform that integrates image scanning and policy enforcement. Provides a managed admission controller that blocks images with critical vulnerabilities, reducing the need for custom Kyverno/Gatekeeper policies.
  • Sigstore (#97): The open source signing and verification ecosystem. Use the public instance (Rekor, Fulcio) for keyless signing in public CI systems. For private environments, deploy a private Sigstore stack or use a managed service.

What you still control: The signing policy (which identities are trusted), the registry allowlist, the break-glass procedure, and the admission controller configuration. Managed scanning services complement but do not replace signature verification.

Premium content pack: Kyverno policy pack for image verification, including policies for cosign signature verification, digest pinning, registry allowlisting, and break-glass exception templates. Includes a GitHub Actions workflow for automated image signing.