Skip to main content

Verify image signatures

Inspect 1.36

By default, any container image is admitted to your workload clusters. Add an admission check to require that images are cryptographically signed before pods can run.

Note

Syself's node image has its own integrity story: dm-verity and a published SBOM. This page covers your workload images, not the node image. See for the node side.

Prerequisites#

  • kubectl access to the workload cluster
  • Your images are signed with cosign or another sigstore-compatible tool
  • Kyverno installed if you use Option A (see )

How image-signature checking works#

When a pod is created, an admission controller (a plugin that inspects resources before Kubernetes accepts them) checks the container images and rejects the pod if a signature is missing or invalid.

On Syself, set failurePolicy: Ignore on any admission controller you deploy for this check. This is a recommendation, not something enforces: webhook-guard restricts which API groups and resources a webhook can target, it does not check a webhook's failurePolicy. With failurePolicy: Ignore, if the verifier is down or unreachable, the API server skips it and admits the pod. Image-signature checking is therefore best-effort. Run the verifier highly available to make the check reliable in practice.

Why a native ValidatingAdmissionPolicy does not work here#

A native ValidatingAdmissionPolicy (VAP) runs inside the API server and can hard-block pods without the failurePolicy: Ignore limitation. For many rules, a VAP is the right tool.

Signature verification is the exception. A VAP can inspect the image string: registry, repository, tag, or digest. It cannot fetch a signature from a registry and verify it against a key. That requires network calls and cryptography that a VAP does not support.

Use a VAP to require that images come from a specific registry or use a digest. Use a webhook-based tool (below) to require that images are signed.

Restrict image sources and tags#

Signature verification proves who built an image. It does not stop a pod from pulling nginx:latest from a public registry. Add a second, cheaper check that runs inside the API server with a ValidatingAdmissionPolicy (VAP): require every image to come from an allowed registry or be pinned to a digest, and reject the :latest tag. A VAP only reads the image string, so it needs no network calls and can hard-block without the failurePolicy: Ignore limitation.

yaml
		apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: restrict-image-sources
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: >-
        object.spec.containers.all(c,
          c.image.contains('@sha256:') || c.image.startsWith('registry.example.com/'))
      message: "images must come from registry.example.com or be pinned to a @sha256 digest"
    - expression: "object.spec.containers.all(c, !c.image.endsWith(':latest'))"
      message: "the :latest tag is not allowed; pin a version tag or a digest"
	

Bind it to your own namespaces so it does not touch platform workloads:

yaml
		apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: restrict-image-sources
spec:
  policyName: restrict-image-sources
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: team-a
	

The expressions above cover containers only. Extend them to object.spec.initContainers and object.spec.ephemeralContainers if your pods use them.

Signing is not scanning. A signature proves who built an image, not what is inside it. Scan your workload images for known CVEs as well, in CI before you push and again inside the cluster; is the how. The platform SBOM covers the node image only, never the contents of your workload images, so that side is yours.

Require signatures with a webhook verifier#

Two webhook-based tools can require signatures on this platform. Both run with failurePolicy: Ignore, since Syself does not enforce this setting for you.

Kyverno's verifyImages rule performs cosign (a tool for signing and verifying container images) signature verification. It runs as a webhook. Set failurePolicy: Ignore on it, since Syself does not enforce this setting for you.

See for installation and high-availability configuration.

Example policy (keyless, using GitHub Actions as the signing identity):

yaml
		apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce # block pods that fail the rule when Kyverno is up
  webhookConfiguration:
    failurePolicy: Ignore # recommended so a verifier outage does not block the API server
  rules:
    - name: verify-signature
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [team-a] # scope to your namespaces
      verifyImages:
        - imageReferences: ["registry.example.com/*"]
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/your-org/*"
                    issuer: "https://token.actions.githubusercontent.com"
	

Roll out safely#

Scope the policy to your own namespaces. Do not target kube-system or platform namespaces; Syself manages those images. First set validationFailureAction: Audit. This logs which pods would be blocked without actually blocking them. Once you confirm your signed images pass, switch to Enforce. Because the webhook uses failurePolicy: Ignore, it only protects you while it is running. Keep it running by deploying several replicas and adding a PodDisruptionBudget (a rule that keeps a minimum number of replicas up during node drains). See for a working example.

Verify#

		# A signed image should be admitted:
$ kubectl -n team-a run signed --image=registry.example.com/app@sha256:<signed-digest>
# An unsigned image should be blocked by your policy:
$ kubectl -n team-a run unsigned --image=docker.io/library/busybox   # expect: denied
	

Known limits#

Best-effort under Ignore. If the verifier is down when a pod is created, an unsigned image can be admitted. High-availability mode with alerting on verifier health is the mitigation.

Workload images only. This policy covers images you deploy. The node image integrity story is separate and handled by the platform.