refactor(hub): one Kyverno policy per template in templates/kyverno

Move the image-verification policy to templates/kyverno/, one
ClusterPolicy per file (hub-api-signed-images.yaml), with the shared
parts (Kyverno check, names, labels, policy-wide settings and the UUG.AI
signer) in templates/kyverno/_helpers.tpl.

imageVerification.images becomes imageVerification.policies.<name>,
each with its own enabled switch and optional validationFailureAction,
failurePolicy and mutateDigest overrides. Schema and README updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cédric Verstraeten
2026-11-10 19:50:43 +00:00
co-authored by Claude Opus 5.5
parent 49d6226a5d
commit dcfcf28050
6 changed files with 198 additions and 109 deletions
+26 -10
View File
@@ -339,10 +339,22 @@ only when a compatible image release is selected; no migration is required.
Starting with chart **`0.179.0`**, the chart verifies that UUG.AI images are
signed by UUG.AI. When [Kyverno](https://kyverno.io) is installed in the cluster,
it creates one `ClusterPolicy` per release, named
`<namespace>-<release>-signed-images` and scoped to the release namespace.
Without Kyverno (no `kyverno.io/v1` API), nothing is created and the chart
behaves as before. Set `imageVerification.enabled: false` to opt out.
it creates Kyverno `ClusterPolicies` scoped to the release namespace, named
`<namespace>-<release>-<policy>`. Without Kyverno (no `kyverno.io/v1` API),
nothing is created and the chart behaves as before. Set
`imageVerification.enabled: false` to opt out.
Every policy is its own template in `templates/kyverno/`, with its own entry
under `imageVerification.policies`:
| Template | Values | Checks |
|---|---|---|
| `hub-api-signed-images.yaml` | `imageVerification.policies.hubApi` | `ghcr.io/uug-ai/hub-api` signed for `uug-ai/hub-api` |
`templates/kyverno/_helpers.tpl` holds only the shared parts (the Kyverno
check, names, labels, policy-wide settings and the UUG.AI signer). To change one
policy, edit its file; to add an image, copy a policy file, change the image and
repository, and add a `policies.<name>` entry in `values.yaml`.
Every image is released with a keyless [cosign](https://docs.sigstore.dev)
signature recorded in the public Rekor transparency log. The policy accepts an
@@ -355,21 +367,25 @@ image only when its signature was:
The release workflow is shared by all UUG.AI repositories, so this repository
claim is what stops an image of one repository passing as another.
Today only `hub-api` is listed in `imageVerification.images`; other images are
added as their releases start signing. `hub-api` releases are signed from
Today only `hub-api` has a policy; other images get one as their releases start
signing. `hub-api` releases are signed from
`v1.11.39` onwards (and `v1.11.37`); in `Enforce` mode an unsigned version such
as `v1.11.38` is rejected.
The default is **`Audit`**: unsigned images are reported in Kyverno
PolicyReports (for example in Policy Reporter) but still run, and
`failurePolicy: Ignore` keeps pods starting if Kyverno is unavailable. To block
unsigned images once the reports are clean:
unsigned images once the reports are clean, for every policy or per policy:
```yaml
imageVerification:
validationFailureAction: Enforce
failurePolicy: Fail # only with a highly available Kyverno
mutateDigest: true # pin verified tags to their digest
validationFailureAction: Enforce # default for every policy
failurePolicy: Fail # only with a highly available Kyverno
mutateDigest: true # pin verified tags to their digest
policies:
hubApi:
enabled: true
# validationFailureAction: Audit # per-policy override
```
If you mirror the images to another registry, set `imageVerification.registry`
@@ -1,72 +0,0 @@
{{- /*
Kyverno policy that verifies the keyless cosign signatures of UUG.AI images.
Rendered only when imageVerification.enabled is true and the cluster serves the
kyverno.io/v1 API (Kyverno installed), so clusters without Kyverno are unaffected.
Each release gets its own ClusterPolicy, scoped to the release namespace.
*/ -}}
{{- $iv := .Values.imageVerification | default dict -}}
{{- if and $iv.enabled (.Capabilities.APIVersions.Has "kyverno.io/v1") }}
{{- if not $iv.images }}
{{- fail "imageVerification.images must list at least one image when imageVerification.enabled is true" }}
{{- end }}
{{- if not (has $iv.validationFailureAction (list "Audit" "Enforce")) }}
{{- fail "imageVerification.validationFailureAction must be Audit or Enforce" }}
{{- end }}
{{- if not (has $iv.failurePolicy (list "Ignore" "Fail")) }}
{{- fail "imageVerification.failurePolicy must be Ignore or Fail" }}
{{- end }}
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: {{ printf "%s-%s-signed-images" .Release.Namespace .Release.Name | trunc 63 | trimSuffix "-" }}
labels:
app.kubernetes.io/name: hub
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: image-verification
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
annotations:
policies.kyverno.io/title: Kerberos Hub images must be signed by UUG.AI
policies.kyverno.io/category: Supply chain
policies.kyverno.io/severity: high
policies.kyverno.io/subject: Pod
policies.kyverno.io/description: >-
Verifies that UUG.AI images in namespace {{ .Release.Namespace }} carry a
keyless cosign signature made by the UUG.AI release workflow for their own
GitHub repository.
spec:
validationFailureAction: {{ $iv.validationFailureAction }}
background: true
failurePolicy: {{ $iv.failurePolicy }}
webhookTimeoutSeconds: {{ $iv.webhookTimeoutSeconds | default 30 }}
rules:
{{- range $iv.images }}
{{- $repository := .githubRepository | default .name }}
- name: {{ printf "verify-%s" .name | trunc 63 | trimSuffix "-" }}
match:
any:
- resources:
kinds:
- Pod
namespaces:
- {{ $.Release.Namespace | quote }}
verifyImages:
- imageReferences:
- {{ printf "%s/%s:*" $iv.registry .name | quote }}
- {{ printf "%s/%s@*" $iv.registry .name | quote }}
required: true
mutateDigest: {{ $iv.mutateDigest }}
verifyDigest: {{ $iv.mutateDigest }}
attestors:
- entries:
- keyless:
issuer: {{ $iv.issuer | quote }}
subjectRegExp: {{ $iv.subjectRegExp | squote }}
additionalExtensions:
# The signer identity is shared by every UUG.AI repository;
# the repository claim binds the signature to this image's repo.
githubWorkflowRepository: {{ printf "%s/%s" $iv.githubOrganization $repository | quote }}
rekor:
url: {{ $iv.rekorUrl | quote }}
{{- end }}
{{- end }}
+78
View File
@@ -0,0 +1,78 @@
{{- /*
Shared building blocks for the Kyverno policies in this folder.
Each policy lives in its own file (one ClusterPolicy per file); these helpers
hold only what every policy repeats.
*/ -}}
{{- /*
hub.kyverno.enabled: "true" when policy <name> should be rendered: image
verification is enabled, the policy itself is enabled, and the cluster serves
kyverno.io/v1 (Kyverno installed).
Usage: {{- if eq (include "hub.kyverno.enabled" (dict "root" . "policy" "hubApi")) "true" }}
*/ -}}
{{- define "hub.kyverno.enabled" -}}
{{- $iv := .root.Values.imageVerification | default dict -}}
{{- $policy := index ($iv.policies | default dict) .policy | default dict -}}
{{- if and $iv.enabled $policy.enabled (.root.Capabilities.APIVersions.Has "kyverno.io/v1") -}}
true
{{- end -}}
{{- end -}}
{{- /*
hub.kyverno.name: ClusterPolicy name, unique per release: <namespace>-<release>-<suffix>.
Usage: {{ include "hub.kyverno.name" (dict "root" . "suffix" "hub-api-signed-images") }}
*/ -}}
{{- define "hub.kyverno.name" -}}
{{- printf "%s-%s-%s" .root.Release.Namespace .root.Release.Name .suffix | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- /*
hub.kyverno.labels: common labels for every policy.
*/ -}}
{{- define "hub.kyverno.labels" -}}
app.kubernetes.io/name: hub
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: kyverno-policy
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
{{- end -}}
{{- /*
hub.kyverno.spec: policy-wide settings; a policy's own values override the
imageVerification defaults (validationFailureAction, failurePolicy).
*/ -}}
{{- define "hub.kyverno.spec" -}}
{{- $iv := .root.Values.imageVerification -}}
{{- $policy := index $iv.policies .policy -}}
{{- $action := $policy.validationFailureAction | default $iv.validationFailureAction -}}
{{- $failure := $policy.failurePolicy | default $iv.failurePolicy -}}
{{- if not (has $action (list "Audit" "Enforce")) -}}
{{- fail (printf "imageVerification.policies.%s.validationFailureAction must be Audit or Enforce" .policy) -}}
{{- end -}}
{{- if not (has $failure (list "Ignore" "Fail")) -}}
{{- fail (printf "imageVerification.policies.%s.failurePolicy must be Ignore or Fail" .policy) -}}
{{- end -}}
validationFailureAction: {{ $action }}
background: true
failurePolicy: {{ $failure }}
webhookTimeoutSeconds: {{ $iv.webhookTimeoutSeconds | default 30 }}
{{- end -}}
{{- /*
hub.kyverno.signedBy: keyless cosign attestor for an image released by the
UUG.AI release workflow for GitHub repository <githubOrganization>/<repository>.
The signer identity is shared by every UUG.AI repository; the repository claim
binds the signature to this image's repository.
Usage: {{- include "hub.kyverno.signedBy" (dict "root" . "repository" "hub-api") | nindent 12 }}
*/ -}}
{{- define "hub.kyverno.signedBy" -}}
{{- $iv := .root.Values.imageVerification -}}
- entries:
- keyless:
issuer: {{ $iv.issuer | quote }}
subjectRegExp: {{ $iv.subjectRegExp | squote }}
additionalExtensions:
githubWorkflowRepository: {{ printf "%s/%s" $iv.githubOrganization .repository | quote }}
rekor:
url: {{ $iv.rekorUrl | quote }}
{{- end -}}
@@ -0,0 +1,46 @@
{{- /*
hub-api images must be signed by the UUG.AI release workflow for uug-ai/hub-api.
Values: imageVerification.policies.hubApi (defaults from imageVerification).
To add another image, copy this file, change the image and repository, and add
a policies.<name> entry in values.yaml.
*/ -}}
{{- if eq (include "hub.kyverno.enabled" (dict "root" . "policy" "hubApi")) "true" }}
{{- $iv := .Values.imageVerification }}
{{- $policy := $iv.policies.hubApi }}
{{- $mutateDigest := ternary $policy.mutateDigest $iv.mutateDigest (hasKey $policy "mutateDigest") }}
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: {{ include "hub.kyverno.name" (dict "root" . "suffix" "hub-api-signed-images") }}
labels:
{{- include "hub.kyverno.labels" . | nindent 4 }}
annotations:
policies.kyverno.io/title: hub-api images must be signed by UUG.AI
policies.kyverno.io/category: Supply chain
policies.kyverno.io/severity: high
policies.kyverno.io/subject: Pod
policies.kyverno.io/description: >-
Verifies that hub-api images in namespace {{ .Release.Namespace }} carry a
keyless cosign signature made by the UUG.AI release workflow for the
uug-ai/hub-api repository.
spec:
{{- include "hub.kyverno.spec" (dict "root" . "policy" "hubApi") | nindent 2 }}
rules:
- name: verify-hub-api
match:
any:
- resources:
kinds:
- Pod
namespaces:
- {{ .Release.Namespace | quote }}
verifyImages:
- imageReferences:
- {{ printf "%s/hub-api:*" $iv.registry | quote }}
- {{ printf "%s/hub-api@*" $iv.registry | quote }}
required: true
mutateDigest: {{ $mutateDigest }}
verifyDigest: {{ $mutateDigest }}
attestors:
{{- include "hub.kyverno.signedBy" (dict "root" . "repository" "hub-api") | nindent 12 }}
{{- end }}
+30 -17
View File
@@ -25,11 +25,11 @@
"imageVerification": {
"type": "object",
"additionalProperties": false,
"description": "Kyverno ClusterPolicy that verifies the keyless cosign signatures of UUG.AI images. Rendered only when the cluster serves kyverno.io/v1.",
"description": "Kyverno ClusterPolicies (templates/kyverno/) that verify the keyless cosign signatures of UUG.AI images. Rendered only when the cluster serves kyverno.io/v1.",
"properties": {
"enabled": {
"type": "boolean",
"description": "Create the policy when Kyverno is installed."
"description": "Create the policies when Kyverno is installed."
},
"validationFailureAction": {
"type": "string",
@@ -37,7 +37,7 @@
"Audit",
"Enforce"
],
"description": "Audit reports violations; Enforce rejects pods with unsigned images."
"description": "Default for every policy: Audit reports violations; Enforce rejects pods with unsigned images."
},
"failurePolicy": {
"type": "string",
@@ -45,7 +45,7 @@
"Ignore",
"Fail"
],
"description": "Webhook behaviour when Kyverno is unavailable."
"description": "Default webhook behaviour when Kyverno is unavailable."
},
"webhookTimeoutSeconds": {
"type": "integer",
@@ -54,7 +54,7 @@
},
"mutateDigest": {
"type": "boolean",
"description": "Rewrite verified tags to digests and require digests."
"description": "Default: rewrite verified tags to digests and require digests."
},
"registry": {
"type": "string",
@@ -78,23 +78,36 @@
"type": "string",
"minLength": 1
},
"images": {
"type": "array",
"minItems": 1,
"items": {
"policies": {
"type": "object",
"description": "One entry per policy template in templates/kyverno/.",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
"enabled": {
"type": "boolean",
"description": "Render this policy (templates/kyverno/<file>)."
},
"githubRepository": {
"validationFailureAction": {
"type": "string",
"pattern": "^[A-Za-z0-9._-]+$"
"enum": [
"Audit",
"Enforce"
],
"description": "Overrides imageVerification.validationFailureAction for this policy."
},
"failurePolicy": {
"type": "string",
"enum": [
"Ignore",
"Fail"
],
"description": "Overrides imageVerification.failurePolicy for this policy."
},
"mutateDigest": {
"type": "boolean",
"description": "Overrides imageVerification.mutateDigest for this policy."
}
}
}
+18 -10
View File
@@ -28,13 +28,16 @@ mode: "all"
###########################################################################
# Image signature verification (Kyverno)
###########################################################################
# Creates a Kyverno ClusterPolicy, scoped to the release namespace, that checks
# the keyless cosign signatures of UUG.AI images: signed by GitHub Actions, by
# the UUG.AI release workflow, for the image's own uug-ai GitHub repository.
# Rendered only when Kyverno is installed (the cluster serves kyverno.io/v1);
# without Kyverno nothing is created.
# Kyverno ClusterPolicies, scoped to the release namespace, that check the
# keyless cosign signatures of UUG.AI images: signed by GitHub Actions, by the
# UUG.AI release workflow, for the image's own uug-ai GitHub repository.
# Each policy is its own template in templates/kyverno/. They are rendered only
# when Kyverno is installed (the cluster serves kyverno.io/v1); without Kyverno
# nothing is created.
imageVerification:
enabled: true
# Defaults for every policy; a policy under `policies` may override
# validationFailureAction, failurePolicy and mutateDigest.
# Audit reports unsigned images (PolicyReports) without blocking them;
# Enforce rejects pods with an unsigned or wrongly signed image.
validationFailureAction: Audit
@@ -53,11 +56,16 @@ imageVerification:
# Signer: the shared UUG.AI release workflow, at a branch/tag ref or a commit.
subjectRegExp: '^https://github\.com/uug-ai/workflows/\.github/workflows/release-create\.yml@(refs/.+|[0-9a-f]{40})$'
rekorUrl: https://rekor.sigstore.dev
# Images to verify, by repository name under `registry`. githubRepository
# defaults to the name. Only images whose releases are signed belong here;
# more are added as their release pipelines start signing.
images:
- name: hub-api
# One entry per policy template in templates/kyverno/. Only images whose
# releases are signed have a policy; more are added as their release
# pipelines start signing.
policies:
# templates/kyverno/hub-api-signed-images.yaml
hubApi:
enabled: true
# validationFailureAction: Enforce
# failurePolicy: Fail
# mutateDigest: true
###########################################################################
# Global configuration
###########################################################################