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 1703f635d..e9a386d8e 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,41 @@ 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: + # 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'. + - 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 ce93f771f..4471d462b 100644 --- a/tests/disallow-latest-tag/kyverno-test.yaml +++ b/tests/disallow-latest-tag/kyverno-test.yaml @@ -1,7 +1,8 @@ -# 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, 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 kind: Test metadata: @@ -12,45 +13,25 @@ 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 + - default/digest-only + - default/registry-port-versioned-tag 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 + - default/registry-port-tagless 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 9c2dfdd40..a4b482240 100644 --- a/tests/disallow-latest-tag/resources.yaml +++ b/tests/disallow-latest-tag/resources.yaml @@ -73,3 +73,59 @@ 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 +--- +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 diff --git a/tests/policy-failure-actions.json b/tests/policy-failure-actions.json index e75914047..c3715885f 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",