From 060087487c8c2a8bde36f6d886a178274d8ae46f Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 12:09:22 +0200 Subject: [PATCH 1/2] feat(kyverno): migrate disallow-latest-tag to ValidatingPolicy Rewrite the policy as a ValidatingPolicy with the same Audit action, Pod CREATE scope, background evaluation and namespace exclusions, covering regular, init and ephemeral containers. Collapse its two rule entries in the failure-action baseline to one policy entry and add ephemeral container fixtures to its CLI test. Fixes #5115 Co-Authored-By: Claude Opus 5.5 --- .../best-practices/disallow-latest-tag.yaml | 118 ++++++------------ tests/disallow-latest-tag/kyverno-test.yaml | 35 +----- tests/disallow-latest-tag/resources.yaml | 26 ++++ tests/policy-failure-actions.json | 3 +- 4 files changed, 72 insertions(+), 110 deletions(-) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml index 1703f635d9..8f57765cb8 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml @@ -3,22 +3,22 @@ # tag points at TODAY) and make rollbacks/incident forensics guesswork. # Requires every container image to carry a tag, and forbids the ':latest' # tag specifically. A tag+digest reference (img:1.2.3@sha256:…) passes both -# rules — the digest is what actually pins it. +# checks — the digest is what actually pins it. # # Audit ONLY for now: third-party chart images can't be enumerated # statically, and an Enforce policy that trips on one would wedge its # HelmRelease mid-upgrade (the enforce-flux-best-practices failure mode). -# Flip rule failureAction to Enforce once prod PolicyReports +# Switch validationActions to Deny once prod PolicyReports # (`kubectl get polr -A | grep disallow-latest-tag`) show zero violations. # # Namespace exclusions mirror validate-pod-security: infrastructure # namespaces that run third-party system charts by design. -apiVersion: kyverno.io/v1 -kind: ClusterPolicy +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy metadata: name: disallow-latest-tag annotations: - policies.kyverno.io/minversion: 1.13.0 + policies.kyverno.io/minversion: 1.17.0 policies.kyverno.io/title: Disallow Latest Tag policies.kyverno.io/category: Best Practices policies.kyverno.io/subject: Pod @@ -27,77 +27,37 @@ metadata: mutable ':latest' tag, so what runs is reproducible and the signature-verification layers gate immutable references. spec: - background: true - rules: - - name: require-image-tag - match: - any: - - resources: - kinds: - - Pod - operations: - - CREATE - exclude: - any: - - resources: - namespaces: - - kube-system - - kube-public - - kube-node-lease - - flux-system - - longhorn-system - - local-path-storage - - kubescape - - kubevirt - - cdi - - observability - - velero - validate: - failureAction: Audit - message: >- - An explicit image tag is required — a tagless reference silently - resolves to the mutable ':latest'. - pattern: - spec: - =(ephemeralContainers): - - image: "*:*" - =(initContainers): - - image: "*:*" - containers: - - image: "*:*" - - name: validate-image-tag - match: - any: - - resources: - kinds: - - Pod - operations: - - CREATE - exclude: - any: - - resources: - namespaces: - - kube-system - - kube-public - - kube-node-lease - - flux-system - - longhorn-system - - local-path-storage - - kubescape - - kubevirt - - cdi - - observability - - velero - validate: - failureAction: Audit - message: >- - The mutable ':latest' tag is not allowed — pin a version tag so - rollbacks and signature verification reference immutable images. - pattern: - spec: - =(ephemeralContainers): - - image: "!*:latest" - =(initContainers): - - image: "!*:latest" - containers: - - image: "!*:latest" + validationActions: + - Audit + evaluation: + background: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + matchConditions: + - name: not-infrastructure-namespace + expression: >- + !(object.metadata.?namespace.orValue('') in [ + 'kube-system', 'kube-public', 'kube-node-lease', 'flux-system', + 'longhorn-system', 'local-path-storage', 'kubescape', 'kubevirt', + 'cdi', 'observability', 'velero']) + variables: + # Every container kind the Pod declares: regular, init and ephemeral. + - name: images + expression: >- + object.spec.containers.map(c, c.image) + + object.spec.?initContainers.orValue([]).map(c, c.image) + + object.spec.?ephemeralContainers.orValue([]).map(c, c.image) + validations: + - expression: "variables.images.all(i, i.contains(':'))" + message: >- + An explicit image tag is required — a tagless reference silently + resolves to the mutable ':latest'. + - expression: "variables.images.all(i, !i.endsWith(':latest'))" + message: >- + The mutable ':latest' tag is not allowed — pin a version tag so + rollbacks and signature verification reference immutable images. diff --git a/tests/disallow-latest-tag/kyverno-test.yaml b/tests/disallow-latest-tag/kyverno-test.yaml index ce93f771fe..bcf5c80c2f 100644 --- a/tests/disallow-latest-tag/kyverno-test.yaml +++ b/tests/disallow-latest-tag/kyverno-test.yaml @@ -1,7 +1,7 @@ -# Pins the ClusterPolicy's behaviour before its ValidatingPolicy rewrite (#4996). -# A versioned tag and a tag+digest pass both rules; ':latest' fails only the -# latest-tag rule; a tagless image fails only the tag-required rule; the same -# tagless image in an excluded infrastructure namespace is out of scope. +# Pins the policy's behaviour across its ValidatingPolicy rewrite (#4996). +# A versioned tag and a tag+digest pass; ':latest' fails, as does a tagless +# image, in a regular, init or ephemeral container; the same tagless image in an +# excluded infrastructure namespace is out of scope. apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: @@ -12,45 +12,22 @@ resources: - resources.yaml results: - policy: disallow-latest-tag - rule: require-image-tag resources: - default/versioned-tag - default/tag-and-digest - - default/latest-tag - - default/latest-tag-init-container kind: Pod result: pass - policy: disallow-latest-tag - rule: require-image-tag - resources: - - default/tagless - - default/tagless-init-container - kind: Pod - result: fail - - policy: disallow-latest-tag - rule: validate-image-tag resources: - - default/versioned-tag - - default/tag-and-digest - default/tagless - default/tagless-init-container - kind: Pod - result: pass - - policy: disallow-latest-tag - rule: validate-image-tag - resources: - default/latest-tag - default/latest-tag-init-container + - default/latest-tag-ephemeral-container + - default/tagless-ephemeral-container kind: Pod result: fail - policy: disallow-latest-tag - rule: require-image-tag - resources: - - kube-system/tagless - kind: Pod - result: skip - - policy: disallow-latest-tag - rule: validate-image-tag resources: - kube-system/tagless kind: Pod diff --git a/tests/disallow-latest-tag/resources.yaml b/tests/disallow-latest-tag/resources.yaml index 9c2dfdd405..33f858aa47 100644 --- a/tests/disallow-latest-tag/resources.yaml +++ b/tests/disallow-latest-tag/resources.yaml @@ -73,3 +73,29 @@ spec: containers: - name: app image: ghcr.io/example/app +--- +apiVersion: v1 +kind: Pod +metadata: + name: latest-tag-ephemeral-container + namespace: default +spec: + ephemeralContainers: + - name: debug + image: ghcr.io/example/debug:latest + containers: + - name: app + image: ghcr.io/example/app:1.2.3 +--- +apiVersion: v1 +kind: Pod +metadata: + name: tagless-ephemeral-container + namespace: default +spec: + ephemeralContainers: + - name: debug + image: ghcr.io/example/debug + containers: + - name: app + image: ghcr.io/example/app:1.2.3 diff --git a/tests/policy-failure-actions.json b/tests/policy-failure-actions.json index e759140470..c3715885fe 100644 --- a/tests/policy-failure-actions.json +++ b/tests/policy-failure-actions.json @@ -5,8 +5,7 @@ "require-baseline-workers-for-hcloud-volumes/require-baseline-worker-affinity": "Enforce", "require-baseline-workers-for-hcloud-volumes/report-claim-lookup-failure": "Audit", "audit-privileged-rbac/privileged-rules": "Enforce", - "disallow-latest-tag/require-image-tag": "Audit", - "disallow-latest-tag/validate-image-tag": "Audit", + "disallow-latest-tag": "Audit", "restrict-github-team-external-identity/teammemberships-external-name-matches-declared-member": "Enforce", "restrict-github-team-external-identity/teamrepositories-external-name-matches-declared-grant": "Enforce", "restrict-github-team-external-identity/teams-external-name-is-an-approved-identity": "Enforce", From 871d3a4e8cfcc1f3787d53bb37d7bbe1c5b23908 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 12:27:07 +0200 Subject: [PATCH 2/2] fix(kyverno): do not read a registry port as an image tag A tagless image from a registry with a port (host:5000/app) carried a colon and passed the tag check. Look for the tag in the last path segment, and keep passing digest references, which pin the image. Refs #5115 Co-Authored-By: Claude Opus 5.5 --- .../best-practices/disallow-latest-tag.yaml | 6 +++- tests/disallow-latest-tag/kyverno-test.yaml | 6 +++- tests/disallow-latest-tag/resources.yaml | 30 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml index 8f57765cb8..e9a386d8e9 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/disallow-latest-tag.yaml @@ -53,7 +53,11 @@ spec: object.spec.?initContainers.orValue([]).map(c, c.image) + object.spec.?ephemeralContainers.orValue([]).map(c, c.image) validations: - - expression: "variables.images.all(i, i.contains(':'))" + # The tag lives in the last path segment: a registry port (host:5000/app) + # is not a tag. A digest (app@sha256:…) pins the image, so it also passes. + - expression: >- + variables.images.all(i, i.contains('@') || + i.substring(i.lastIndexOf('/') + 1).contains(':')) message: >- An explicit image tag is required — a tagless reference silently resolves to the mutable ':latest'. diff --git a/tests/disallow-latest-tag/kyverno-test.yaml b/tests/disallow-latest-tag/kyverno-test.yaml index bcf5c80c2f..4471d462b2 100644 --- a/tests/disallow-latest-tag/kyverno-test.yaml +++ b/tests/disallow-latest-tag/kyverno-test.yaml @@ -1,5 +1,6 @@ # Pins the policy's behaviour across its ValidatingPolicy rewrite (#4996). -# A versioned tag and a tag+digest pass; ':latest' fails, as does a tagless +# A versioned tag, a tag+digest and a digest alone pass, with or without a +# registry port; a registry port is not a tag; ':latest' fails, as does a tagless # image, in a regular, init or ephemeral container; the same tagless image in an # excluded infrastructure namespace is out of scope. apiVersion: cli.kyverno.io/v1alpha1 @@ -15,6 +16,8 @@ results: resources: - default/versioned-tag - default/tag-and-digest + - default/digest-only + - default/registry-port-versioned-tag kind: Pod result: pass - policy: disallow-latest-tag @@ -25,6 +28,7 @@ results: - default/latest-tag-init-container - default/latest-tag-ephemeral-container - default/tagless-ephemeral-container + - default/registry-port-tagless kind: Pod result: fail - policy: disallow-latest-tag diff --git a/tests/disallow-latest-tag/resources.yaml b/tests/disallow-latest-tag/resources.yaml index 33f858aa47..a4b4822408 100644 --- a/tests/disallow-latest-tag/resources.yaml +++ b/tests/disallow-latest-tag/resources.yaml @@ -99,3 +99,33 @@ spec: containers: - name: app image: ghcr.io/example/app:1.2.3 +--- +apiVersion: v1 +kind: Pod +metadata: + name: registry-port-tagless + namespace: default +spec: + containers: + - name: app + image: registry.example.com:5000/example/app +--- +apiVersion: v1 +kind: Pod +metadata: + name: registry-port-versioned-tag + namespace: default +spec: + containers: + - name: app + image: registry.example.com:5000/example/app:1.2.3 +--- +apiVersion: v1 +kind: Pod +metadata: + name: digest-only + namespace: default +spec: + containers: + - name: app + image: ghcr.io/example/app@sha256:0000000000000000000000000000000000000000000000000000000000000000