Repository: kyverno/kyverno (2026-04-10T16:19:45Z)

Issue:
[Bug] `matchImageReferences` does not filter `images.containers` in ImageValidatingPolicy — all images are evaluated
### Kyverno Version

1.17.0

### Kubernetes Version

1.33.x

### Kubernetes Platform

AKS

### Kyverno Rule Type

verifyImages

### Description

`matchImageReferences` in `ImageValidatingPolicy` does not filter images from the `images.containers` CEL variable. According to the API documentation in [`kyverno/api`](https://github.com/kyverno/api/blob/main/api/policies.kyverno.io/v1alpha1/imagevalidating_policy.go):

> MatchImageReferences is a list of Glob and CELExpressions to match images.
> Any image that matches one of the rules is considered for validation.
> **Any image that does not match a rule is skipped, even when they are passed as arguments to image verification functions.**

However, in practice, **all images** in a pod are included in `images.containers` and evaluated by the `validations` expression, regardless of whether they match `matchImageReferences`. Non-matching images fail signature verification because they are not signed with the expected key, causing the policy to deny pods that should be unaffected.

### Root Cause

After tracing the source code, the issue is in [`pkg/imageverification/evaluator/policy.go`](https://github.com/kyverno/kyverno/blob/main/pkg/imageverification/evaluator/policy.go) in the `Evaluate` method of `compiledPolicy`.

The `matchImageReferences` filter is only used to control which images get prefetched into the image context (`ictx.AddImages`), but `data[engine.ImagesKey]` (which populates `images.containers` in CEL) is set **before** the filtering and is **never modified**:

```go
// images is set BEFORE matchImageReferences filtering
images, err := engine.ExtractImages(data, c.imageExtractors)
data[engine.ImagesKey] = images  // <-- ALL images, unfiltered

// matchImageReferences only filters what gets added to ictx (registry prefetch)
imgList := []string{}
for _, v := range images {
    for _, img := range v {
        if apply, err := matching.MatchImage(img, c.matchImageReferences...); err != nil {
            return nil, err
        } else if apply {
            imgList = append(imgList, img)
        }
    }
}
if err := ictx.AddImages(ctx, imgList, ...); err != nil {
    return nil, err
}

// validations run against data which contains ALL images, not filtered ones
for i, v := range c.validations {
    out, _, err := v.Program.ContextEval(ctx, data)  // <-- uses unfiltered images
```

As a result, when a validation expression like `images.containers.map(image, verifyImageSignatures(image, [attestors.notary])).all(e, e > 0)` runs, it receives all images including ones that don't match `matchImageReferences`, causing policy validation failures for images that should have been skipped.

### Expected Fix

`data[engine.ImagesKey]` should be filtered by `matchImageReferences` before being passed to validation expressions, so that `images.containers` only contains images that match the policy's glob/CEL patterns.

### Workaround

Add a manual `.filter()` in the CEL validation expression:

```yaml
validations:
  - expression: >-
      images.containers
        .filter(image, image.contains("/myrepo/myapp"))
        .map(image, verifyImageSignatures(image, [attestors.notary]))
        .all(e, e > 0)
    message: "Image must be signed"
```

### Steps to reproduce

1. Create an `ImageValidatingPolicy` with `matchImageReferences` scoped to specific images:

```yaml
apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
  name: verify-image-signatures
spec:
  validationActions: [Deny]
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
        operations: ["CREATE", "UPDATE"]
  matchImageReferences:
    - glob: "*/myrepo/myapp*"
  attestors:
    - name: notary
      notary:
        certs:
          value: |
            -----BEGIN CERTIFICATE-----
            ... (your signing certificate) ...
            -----END CERTIFICATE-----
  validations:
    - expression: >-
        images.containers.map(image,
        verifyImageSignatures(image, [attestors.notary])).all(e, e > 0)
      message: "Image must be signed"
```
2. Deploy a pod with a non-matching image:

```bash
kubectl run test-nginx --image=nginx:latest --dry-run=server
```



### Expected behavior

**Expected**: The pod should be allowed because `nginx:latest` does not match any `matchImageReferences` glob.

**Actual**: The pod is denied with the message "Image must be signed" because `nginx:latest` is included in `images.containers` and fails signature verification.

### Screenshots

_No response_

### Kyverno logs

```Shell

```

### Slack discussion

_No response_

### Troubleshooting

- [x] I have read and followed the documentation AND the [troubleshooting guide](https://kyverno.io/docs/troubleshooting/).
- [x] I have searched other issues in this repository and mine is not recorded.


Discussion:
Yes, I can confirm I noticed the same yesterday, I were trying to migrate from cluster policy to image validating policy, and also posted this in the Slack channel https://kubernetes.slack.com/archives/CLGR9BJU9/p1775804022341289

I'm seeing this in both:

- background scan (properties.process: background scan)
- admission review (properties.process: admission review)

Both controllers seem to be evaluating all pods in all namespaces regardless of the configured globs.

The workaround you mentioned is not good, because it changes the policy from fail to pass, but there should not be a report at all.

We are using v1.7.1, running on EKS.
Hey @herkusas, @DominikPildner, thanks for bringing up the issue ! #15834 should solve it I think. It will be release with `v1.18-rc-1` next week (and backported to `v1.17.2`)



Patch:
diff --git a/pkg/image/verification/evaluator/policy.go b/pkg/image/verification/evaluator/policy.go
index 5471c8cc1bc9..cc384d8e1ed2 100644
--- a/pkg/image/verification/evaluator/policy.go
+++ b/pkg/image/verification/evaluator/policy.go
@@ -120,11 +120,35 @@ func (c *compiledPolicy) Evaluate(ctx context.Context, ictx imagedataloader.Imag
 	} else {
 		data[engine.ObjectKey] = request
 	}
+
 	images, err := engine.ExtractImages(data, c.imageExtractors)
 	if err != nil {
 		return nil, err
 	}
-	data[engine.ImagesKey] = images
+	filteredImages := make(map[string][]string, len(images))
+	imgList := []string{}
+	for category, imgs := range images {
+		filteredImages[category] = []string{} // ensure image.containers is always [] in CEL
+		for _, img := range imgs {
+			if apply, err := matching.MatchImage(img, c.matchImageReferences...); err != nil {
+				return nil, err
+			} else if apply {
+				filteredImages[category] = append(filteredImages[category], img)
+				imgList = append(imgList, img)
+			}
+		}
+	}
+
+	// no image match found, skip
+	if len(c.matchImageReferences) > 0 && len(imgList) == 0 {
+		return nil, nil
+	}
+
+	if err := ictx.AddImages(ctx, imgList, imageverify.GetRemoteOptsFromPolicy(c.creds)...); err != nil {
+		return nil, err
+	}
+
+	data[engine.ImagesKey] = filteredImages
 	data[engine.AttestationsKey] = c.attestationList
 	attestors := lazy.NewMapValue(cel.DynType)
 	for _, attestor := range c.attestors {
@@ -138,19 +162,6 @@ func (c *compiledPolicy) Evaluate(ctx context.Context, ictx imagedataloader.Imag
 	}
 	data[engine.AttestorsKey] = attestors
 
-	imgList := []string{}
-	for _, v := range images {
-		for _, img := range v {
-			if apply, err := matching.MatchImage(img, c.matchImageReferences...); err != nil {
-				return nil, err
-			} else if apply {
-				imgList = append(imgList, img)
-			}
-		}
-	}
-	if err := ictx.AddImages(ctx, imgList, imageverify.GetRemoteOptsFromPolicy(c.creds)...); err != nil {
-		return nil, err
-	}
 	for i, v := range c.validations {
 		out, _, err := v.Program.ContextEval(ctx, data)
 		if err != nil {


Test patch:
diff --git a/pkg/image/verification/evaluator/eval_test.go b/pkg/image/verification/evaluator/eval_test.go
index b7caf8250072..f45a4224930d 100644
--- a/pkg/image/verification/evaluator/eval_test.go
+++ b/pkg/image/verification/evaluator/eval_test.go
@@ -10,6 +10,8 @@ import (
 	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
 )
 
+var nonMatchingImage = "docker.io/library/nginx:latest"
+
 var (
 	obj = func(image string) map[string]any {
 		return map[string]any{
@@ -93,6 +95,12 @@ uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
 	}
 )
 
+func Test_Eval_NonMatchingImage(t *testing.T) {
+	result, err := Evaluate(context.Background(), []*CompiledImageValidatingPolicy{{Policy: ivpol}}, obj(nonMatchingImage), nil, nil, nil)
+	assert.NoError(t, err)
+	assert.Nil(t, result[ivpol.Name])
+}
+
 func Test_Eval(t *testing.T) {
 	result, err := Evaluate(context.Background(), []*CompiledImageValidatingPolicy{{Policy: ivpol}}, obj(signedImage), nil, nil, nil)
 	assert.NoError(t, err)