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 authenticatedhelmCLI v3.x or laterkubectlconfigured with access to your control planeyqinstalled for YAML processingAppropriate 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.enabledenabled 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.io, policies.kyverno.io, reports.kyverno.io, wgpolicyk8s.io):
ClusterRole | Label | Purpose |
|---|---|---|
|
| Lets |
|
| Lets control plane users create and modify Kyverno policies |
|
| 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_EOFBuild 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.yamlIf 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'