How to package Kyverno as a controller for Spaces ≤1.14 (addon in 1.15+)

Last updated: May 14, 2026

This guide explains how to package Kyverno as an Upbound controller and deploy it to Upbound Spaces control planes.

Prerequisites

  • Upbound CLI (up) installed and authenticated

  • helm CLI v3.x or later

  • kubectl configured with access to your control plane

  • yq installed for YAML processing

  • Appropriate permissions to push packages to your Upbound organization registry

  • Spaces v1.12.0 or later

  • UXP v1.19.0 or later

  • Feature flag features.enabled.alpha.upboundControllers.enabled enabled on Spaces

Understanding the RBAC approach

Spaces control planes are virtual Kubernetes clusters — standard ClusterRoles like cluster-admin do not exist. Crossplane has its own aggregated RBAC system. To grant upbound-controller-manager access to Kyverno's API groups, you create ClusterRoles with rbac.crossplane.io/aggregate-to-* labels. Kubernetes automatically merges these into Crossplane's built-in crossplane ClusterRole, which upbound-controller-manager already holds.

Kyverno's CRDs ship with the Helm chart (unlike Gatekeeper, which generates constraint CRDs dynamically at runtime), so all CRDs are extracted and included in the package.

Package structure

Source files live in kyverno/; the actual package is built inside controller-package/ so up xpkg build never sees non-package files like the RBAC source or scripts.

kyverno/
├── kyverno-cp-rbac.yaml          # Aggregate RBAC source (copied into Helm chart at build time)
├── setup.sh                      # Build script
└── controller-package/           # Created by setup.sh; xpkg build runs here
    ├── crossplane.yaml           # Generated by setup.sh
    ├── crds/                     # Extracted CRDs from the Helm chart
    │   ├── clusterpolicies.kyverno.io.yaml
    │   └── ...
    └── helm/
        └── chart.tgz             # Repackaged Helm chart (includes RBAC under templates/upbound/)

Aggregate RBAC manifest

kyverno-cp-rbac.yaml contains three ClusterRoles covering Kyverno's four API groups (kyverno.iopolicies.kyverno.ioreports.kyverno.iowgpolicyk8s.io):

ClusterRole

Label

Purpose

kyverno:aggregate-to-crossplane

aggregate-to-crossplane: "true"

Lets upbound-controller-manager manage all Kyverno resources

kyverno:aggregate-to-edit

aggregate-to-admin/edit: "true"

Lets control plane users create and modify Kyverno policies

kyverno:aggregate-to-view

aggregate-to-view: "true"

Lets control plane users read Kyverno resources

This manifest is not applied separately — it is embedded inside the Helm chart under templates/upbound/ so it ships with the release.

Packaging script

setup.sh in the kyverno/ directory. It creates a controller-package/ subdirectory and runs up xpkg build from there, keeping the RBAC source and other non-package files out of the build context.

#!/bin/bash
set -euo pipefail

export CHART_NAME=kyverno
export CHART_VERSION=3.4.2   # update as needed

export RELEASE_NAME=kyverno
export RELEASE_NAMESPACE=kyverno-system

mkdir -p controller-package
cd controller-package

# Pull the Helm chart
helm pull oci://ghcr.io/kyverno/charts/$CHART_NAME --version $CHART_VERSION

mkdir -p helm crds
mv $CHART_NAME-$CHART_VERSION.tgz helm/chart.tgz

# Extract CRDs
helm template $RELEASE_NAME helm/chart.tgz -n $RELEASE_NAMESPACE --include-crds \
  | yq e 'select(.kind == "CustomResourceDefinition")' - \
  | yq -s '("crds/" + .metadata.name + ".yaml")' -

# Inject RBAC into chart, repackage
tar -xzf helm/chart.tgz
mkdir -p $CHART_NAME/templates/upbound
cp ../kyverno-cp-rbac.yaml $CHART_NAME/templates/upbound/kyverno-cp-rbac.yaml
helm package $CHART_NAME -d helm
rm -rf $CHART_NAMErm -f helm/chart.tgz
mv helm/$CHART_NAME-$CHART_VERSION.tgz helm/chart.tgz

# Generate crossplane.yamlcat <<EOF > crossplane.yaml
apiVersion: meta.pkg.upbound.io/v1alpha1
kind: Controller
metadata:
  name: kyverno
spec:
  packagingType: Helm
  helm:
    releaseName: $RELEASE_NAME
    releaseNamespace: $RELEASE_NAMESPACE
EOF

up xpkg build

XPKG_FILE=$(ls *.xpkg | head -1)

export CONTROLLER_NAME=controller-kyverno
export CONTROLLER_VERSION=v1.0.0
export UPBOUND_REGISTRY=xpkg.upbound.io
export UPBOUND_ACCOUNT=$(up profile current | jq -r '.profile.organization')

up xpkg push $UPBOUND_REGISTRY/$UPBOUND_ACCOUNT/$CONTROLLER_NAME:$CONTROLLER_VERSION -f $XPKG_FILE

cat <<DEPLOY_EOF > ../deploy-kyverno.yaml
apiVersion: pkg.upbound.io/v1alpha1
kind: Controller
metadata:
  name: kyverno
spec:
  package: $UPBOUND_REGISTRY/$UPBOUND_ACCOUNT/$CONTROLLER_NAME:$CONTROLLER_VERSION
DEPLOY_EOF

Build and deploy

chmod +x setup.sh
./setup.sh

Switch to your control plane context and apply:

# Get the control plane kubeconfig
up ctp kubeconfig get <ctp-name> --namespace <namespace> -f kubeconfig.yaml

kubectl --kubeconfig kubeconfig.yaml apply -f deploy-kyverno.yaml

# Verify
kubectl --kubeconfig kubeconfig.yaml get controllers.pkg
kubectl --kubeconfig kubeconfig.yaml -n kyverno-system get pods

Using Kyverno on the control plane

Once running, apply Kyverno policies directly to the control plane:

# Example: require all Namespaces to have a team labelcat <<EOF | kubectl --kubeconfig kubeconfig.yaml apply -f -
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-team-label
spec:
  validationFailureAction: Audit
  rules:
  - name: check-team-label
    match:
      any:
      - resources:
          kinds: [Namespace]
    validate:
      message: "Namespace must have a 'team' label."
      pattern:
        metadata:
          labels:
            team: "?*"
EOF

kubectl --kubeconfig kubeconfig.yaml get clusterpolicies

Troubleshooting

Controller shows UnhealthyPackageRevision / aggregate RBAC missing

Verify the aggregate RBAC was embedded in the chart before repackaging:

tar -tzf helm/chart.tgz | grep upbound
# expected: kyverno/templates/upbound/kyverno-cp-rbac.yaml

If missing, re-run setup.sh to rebuild and push a new version, then delete and recreate the Controller object so it pulls the updated revision.

CRDs not found

Check the crds/ directory is populated and re-run up xpkg build. The build tool expects CRDs to be present before packaging.

Pods not starting

kubectl --kubeconfig kubeconfig.yaml -n kyverno-system describe pod
kubectl --kubeconfig kubeconfig.yaml -n kyverno-system get events --sort-by='.lastTimestamp'

See also