diff --git a/charts/hub/README.md b/charts/hub/README.md index 48c16cd..3c10a3f 100644 --- a/charts/hub/README.md +++ b/charts/hub/README.md @@ -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 -`--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 +`--`. 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.` 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` diff --git a/charts/hub/templates/image-verification-policy.yaml b/charts/hub/templates/image-verification-policy.yaml deleted file mode 100644 index 1d22b5d..0000000 --- a/charts/hub/templates/image-verification-policy.yaml +++ /dev/null @@ -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 }} diff --git a/charts/hub/templates/kyverno/_helpers.tpl b/charts/hub/templates/kyverno/_helpers.tpl new file mode 100644 index 0000000..3466e0b --- /dev/null +++ b/charts/hub/templates/kyverno/_helpers.tpl @@ -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 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: --. +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 /. +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 -}} diff --git a/charts/hub/templates/kyverno/hub-api-signed-images.yaml b/charts/hub/templates/kyverno/hub-api-signed-images.yaml new file mode 100644 index 0000000..f791005 --- /dev/null +++ b/charts/hub/templates/kyverno/hub-api-signed-images.yaml @@ -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. 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 }} diff --git a/charts/hub/values.schema.json b/charts/hub/values.schema.json index d009450..7a4682a 100644 --- a/charts/hub/values.schema.json +++ b/charts/hub/values.schema.json @@ -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/)." }, - "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." } } } diff --git a/charts/hub/values.yaml b/charts/hub/values.yaml index d12bd7e..a8adb8f 100644 --- a/charts/hub/values.yaml +++ b/charts/hub/values.yaml @@ -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 ###########################################################################