Skip to content

[CI] Migrate kyverno ClusterPolicy to the new CEL-based engines #9336

Description

@moukoublen

Kyverno is used in CI clusters and was upgraded to 1.17.1

Release Notes: https://kyverno.io/blog/2026/02/02/announcing-kyverno-release-1.17/
Migration Guide: https://kyverno.io/docs/guides/migration-to-cel/

Kyverno 1.17 officially marks ClusterPolicy and CleanupPolicy as Deprecated. While they remain functional in this release, the clock has started on their removal to make way for the more performant, standardized CEL-based engines.

Summary

Kyverno 1.17 deprecated ClusterPolicy and CleanupPolicy in favor of new CEL-based policy engines. These deprecated resources will receive only critical fixes in v1.18 (Apr 2026) and v1.19 (Jul 2026), and are planned for removal in v1.20 (Oct 2026).

We currently use Kyverno v1.17.1 (see hack/deployer/runner/kyverno/install/README.md) with 5 ClusterPolicy resources to enforce security in our E2E CI pipelines. All of these need to be migrated to the new ValidatingPolicy kind under policies.kyverno.io/v1.

Affected Files

File Description
hack/deployer/runner/kyverno/install/policies.yaml 4 ClusterPolicy resources (privileged containers, run-as-non-root, hostPath, SA token automount)
hack/deployer/runner/kyverno/install/gke-policies.yaml 1 ClusterPolicy resource (disallow GKE built-in StorageClasses)
hack/deployer/runner/kyverno/kyverno.go Go code that embeds and applies the policy manifests
hack/deployer/runner/kyverno/install/README.md Documentation

Policies to Migrate

1. disallow-privileged-containers (policies.yaml)

  • Type: Pattern-based validation
  • What it does: Ensures securityContext.privileged is unset or false on all container types.
  • Excludes: default, *-system, local-path-storage namespaces; Pods with labels common.k8s.elastic.co/type in [beat, agent] or app.kubernetes.io/name in [max-map-count-setter].
  • CEL equivalent: Use object.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged) || c.securityContext.privileged == false) (and similar for initContainers/ephemeralContainers).

2. require-run-as-non-root-user (policies.yaml)

  • Type: Pattern-based validation
  • What it does: Ensures runAsUser is unset or > 0 on pod securityContext and all container types.
  • Excludes: *-system, local-path-storage namespaces; Pods named manage-agent-hostpath-permissions-*; same label selectors as above.
  • CEL equivalent: Use !has(object.spec.securityContext) || !has(object.spec.securityContext.runAsUser) || object.spec.securityContext.runAsUser > 0 (and per-container).

3. disallow-host-path (policies.yaml)

  • Type: Pattern-based validation
  • What it does: Ensures no hostPath volumes exist.
  • Excludes: default, *-system, local-path-storage namespaces; Pods named manage-agent-hostpath-permissions-*; Pods with label common.k8s.elastic.co/type in [beat, agent].
  • CEL equivalent: Use !has(object.spec.volumes) || object.spec.volumes.all(v, !has(v.hostPath)).

4. restrict-automount-sa-token (policies.yaml)

  • Type: Pattern-based validation
  • What it does: Ensures automountServiceAccountToken is false.
  • Excludes: Same as disallow-host-path plus label exclusions for max-map-count-setter.
  • CEL equivalent: Use object.spec.automountServiceAccountToken == false.

5. disallow-gke-storageclasses (gke-policies.yaml)

  • Type: Deny with conditions (uses Kyverno JMESPath variables)
  • What it does: Blocks PVCs and StatefulSets from using built-in GKE StorageClasses (premium-rwo, standard, standard-rwo).
  • Migration note: This policy uses deny with conditions and Kyverno variable substitution ({{request.object.spec.storageClassName}}). When converting to CEL, the deny logic must be inverted into a positive assertion.
  • CEL equivalent (PVC rule): !['premium-rwo', 'standard', 'standard-rwo'].exists(sc, object.spec.?storageClassName.orValue('') == sc)
  • CEL equivalent (StatefulSet rule): object.spec.volumeClaimTemplates.all(vct, !['premium-rwo', 'standard', 'standard-rwo'].exists(sc, vct.spec.?storageClassName.orValue('') == sc))

Migration Steps

Step 1: Convert policies.yaml to CEL-based ValidatingPolicy resources

For each ClusterPolicy, create a corresponding ValidatingPolicy:

Key field mappings:

Old (ClusterPolicy) New (ValidatingPolicy)
apiVersion: kyverno.io/v1 apiVersion: policies.kyverno.io/v1
kind: ClusterPolicy kind: ValidatingPolicy
spec.validationFailureAction: enforce spec.validationActions: [Deny]
spec.rules[].match.any[].resources.kinds spec.matchConstraints.resourceRules[].resources + apiGroups/apiVersions/operations
spec.rules[].exclude spec.matchConstraints.excludeResourceRules and/or spec.matchConditions
spec.rules[].validate.pattern spec.validations[].expression (CEL)
spec.rules[].validate.deny.conditions spec.validations[].expression (CEL, inverted logic)
spec.rules[].validate.message spec.validations[].message

Handling label-selector exclusions: The current policies use matchExpressions with NotIn to exclude certain label values. In the CEL model, these should become matchConditions using CEL expressions, e.g.:

matchConditions:
  - name: exclude-beats-and-agents
    expression: >
      !('common.k8s.elastic.co/type' in object.metadata.labels) ||
      !(['beat', 'agent'].exists(t, object.metadata.labels['common.k8s.elastic.co/type'] == t))

Handling namespace exclusions: Wildcard namespace patterns like *-system need to be converted to CEL string functions:

matchConditions:
  - name: exclude-system-namespaces
    expression: "!object.metadata.namespace.endsWith('-system')"

Handling name-based exclusions: Pod name patterns like manage-agent-hostpath-permissions-* become:

matchConditions:
  - name: exclude-hostpath-permissions-pods
    expression: "!object.metadata.name.startsWith('manage-agent-hostpath-permissions-')"

Step 2: Convert gke-policies.yaml

This policy uses deny conditions with Kyverno variable interpolation. The deny logic must be inverted into positive CEL assertions. The policy has two rules targeting different resource kinds (PVC and StatefulSet) -- these should become two separate ValidatingPolicy resources since each ValidatingPolicy targets a single resource type via matchConstraints.

Step 3: Update kyverno.go

The Go deployment code (hack/deployer/runner/kyverno/kyverno.go) should not need structural changes since it uses kubectl apply -f against the embedded manifests. However:

  • Verify the embedded YAML filenames in the //go:embed directives still match.
  • Ensure Kyverno 1.17+ CRDs for ValidatingPolicy are included in the kyverno.yaml installer manifest (they should be, since we're already on v1.17.1).

Step 4: Update documentation

Update hack/deployer/runner/kyverno/install/README.md to reference the new policy types and link to the CEL migration guide.

Step 5: Test

  • Run the E2E test pipeline with the new policies to verify enforcement behavior is identical.
  • Verify that excluded workloads (beats, agents, max-map-count-setter, system namespaces) are still correctly excluded.
  • Verify that violating workloads are still correctly denied.
  • Use Kyverno CLI to validate policy syntax: kyverno apply validating-policy.yaml --resource test-pod.yaml

Example: Full Conversion of disallow-privileged-containers

Before (ClusterPolicy):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged-containers
spec:
  validationFailureAction: enforce
  background: true
  rules:
    - name: privileged-containers
      exclude:
        any:
          - resources:
              namespaces:
                - 'default'
                - "*-system"
                - "local-path-storage"
      match:
        any:
          - resources:
              kinds:
                - Pod
              selector:
                matchExpressions:
                  - {key: common.k8s.elastic.co/type, operator: NotIn, values: [beat, agent]}
                  - {key: app.kubernetes.io/name, operator: NotIn, values: [max-map-count-setter]}
      validate:
        message: >-
          Privileged mode is disallowed. The fields spec.containers[*].securityContext.privileged
          and spec.initContainers[*].securityContext.privileged must be unset or set to `false`.
        pattern:
          spec:
            =(ephemeralContainers):
              - =(securityContext):
                  =(privileged): "false"
            =(initContainers):
              - =(securityContext):
                  =(privileged): "false"
            containers:
              - =(securityContext):
                  =(privileged): "false"

After (ValidatingPolicy):

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: disallow-privileged-containers
  annotations:
    policies.kyverno.io/title: Disallow Privileged Containers
    policies.kyverno.io/category: Pod Security Standards (Baseline)
    policies.kyverno.io/severity: medium
    policies.kyverno.io/description: >-
      Privileged mode disables most security mechanisms and must not be allowed.
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ['']
        apiVersions: ['v1']
        resources: ['pods']
        operations: [CREATE, UPDATE]
    excludeResourceRules:
      - apiGroups: ['']
        apiVersions: ['v1']
        resources: ['pods']
        resourceNames: []
        namespaces: ['default', 'local-path-storage']
  matchConditions:
    - name: exclude-system-namespaces
      expression: "!object.metadata.namespace.endsWith('-system')"
    - name: exclude-beats-and-agents
      expression: >
        !('common.k8s.elastic.co/type' in object.metadata.?labels.orValue({})) ||
        !(['beat', 'agent'].exists(t, object.metadata.labels['common.k8s.elastic.co/type'] == t))
    - name: exclude-max-map-count-setter
      expression: >
        !('app.kubernetes.io/name' in object.metadata.?labels.orValue({})) ||
        object.metadata.labels['app.kubernetes.io/name'] != 'max-map-count-setter'
  validationActions: [Deny]
  validations:
    - expression: >
        object.spec.containers.all(c,
          !has(c.securityContext) || !has(c.securityContext.privileged) || c.securityContext.privileged == false
        )
        &&
        (!has(object.spec.initContainers) || object.spec.initContainers.all(c,
          !has(c.securityContext) || !has(c.securityContext.privileged) || c.securityContext.privileged == false
        ))
        &&
        (!has(object.spec.ephemeralContainers) || object.spec.ephemeralContainers.all(c,
          !has(c.securityContext) || !has(c.securityContext.privileged) || c.securityContext.privileged == false
        ))
      message: >-
        Privileged mode is disallowed. The fields spec.containers[*].securityContext.privileged
        and spec.initContainers[*].securityContext.privileged must be unset or set to `false`.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    :ciThings related to Continuous Integration, automation and releases>enhancementEnhancement of existing functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions