Skip to content
Yuvraj 🧢
Github - yindiaGithub - tqindiaContact

Kyverno Generate Policies: Automate Namespace Resources the Right Way

— kubernetes, kyverno, policy, security — 7 min read

Kyverno is a policy engine built for Kubernetes. It runs as an admission controller that validates and mutates incoming resources, and it can also generate new resources in response to events like a namespace being created. Generate rules are how you turn "every new namespace must have a default NetworkPolicy, a ResourceQuota, and an image-pull Secret" into something the cluster enforces for you. This post explains how generate rules work, how to keep generated resources in sync, and the RBAC that trips most people up.

The examples target the current Kyverno v1 policy API (kyverno.io/v1). If you are following an older tutorial, note that the match syntax and the RBAC model changed; this post uses the current form.

TL;DR

  • A generate rule creates a resource when a trigger event matches, most commonly a new namespace.
  • The source can be inline (data) or a clone of an existing resource (clone). They are mutually exclusive.
  • synchronize: true makes the background controller keep the generated resource reconciled: edits are reverted, and deleting it recreates it.
  • Generation is done by the kyverno-background-controller ServiceAccount. To generate resource types it does not manage by default, you grant it permissions with an aggregation label.
  • To generate RBAC (RoleBindings), the controller must already hold the role you are granting, or Kubernetes blocks it as privilege escalation.

The problem generate rules solve

New namespaces usually need a standard set of supporting resources: a NetworkPolicy so pods are not open by default, a ResourceQuota, an image-pull Secret, maybe a RoleBinding. Doing this by hand is error-prone, and doing it with a bash script that loops over namespaces drifts the moment someone creates a namespace outside the script.

Generate rules move that logic into the cluster. You declare the desired resource once in a policy, and Kyverno creates it whenever a matching trigger appears, and optionally keeps it in sync forever.

How a generate rule works

A generate rule has three parts: a match block that defines the trigger, a generate block that defines what to create and where, and a source that is either inline data or a clone of an existing resource.

new Namespace created
│ matches policy `match.any`
â–¼
Kyverno admission controller creates an UpdateRequest
│
â–¼
kyverno-background-controller reconciles it
│
â–¼
downstream resource created in the new namespace
│ synchronize: true
â–¼
controller keeps it reconciled (reverts edits, recreates on delete)

Caption: a namespace trigger flows through an UpdateRequest to the background controller, which creates and optionally syncs the resource.

The intermediate object is a UpdateRequest. You can inspect generation status with kubectl get updaterequests -A, which is the first place to look when a resource does not appear.

Walkthrough: generate from inline data

The inline data source defines the resource directly in the policy. This example creates a ConfigMap in every new namespace, excluding the system ones. Note the current match.any syntax and that the generate block includes apiVersion and kind.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: default-configmap
spec:
rules:
- name: gen-configmap
match:
any:
- resources:
kinds:
- Namespace
exclude:
any:
- resources:
namespaces: [kube-system, kube-public, kube-node-lease, kyverno]
generate:
apiVersion: v1
kind: ConfigMap
name: default-config
namespace: "{{request.object.metadata.name}}"
synchronize: true
data:
data:
APP_ENV: production

Notice namespace: "{{request.object.metadata.name}}" targets the namespace that triggered the rule, and synchronize: true means later edits to the ConfigMap are reverted by Kyverno. The exclude block keeps the policy off system namespaces, which you almost always want.

Walkthrough: clone an existing resource

When the source already exists in the cluster, use clone instead of data. A common case is copying an image-pull Secret into every namespace so pods can pull private images. With synchronize: true, changes to the source Secret propagate to every copy.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: sync-image-pull-secret
spec:
rules:
- name: sync-regcred
match:
any:
- resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: Secret
name: regcred
namespace: "{{request.object.metadata.name}}"
synchronize: true
clone:
namespace: default
name: regcred

Notice clone names the source namespace and resource. Kyverno copies it as-is and cannot template its contents, which is the trade-off versus data. To copy several resources at once, use cloneList with a label selector.

What synchronize actually does

This is the part the old documentation got wrong, and it matters. With synchronize: true, the background controller reconciles the generated resource. It does not block edits with a webhook; it reverts them. The behaviour differs slightly between a data source and a clone source.

ActionWith synchronizeWithout synchronize
Edit the generated resourceReverted by the controllerKept
Delete the generated resourceRecreatedStays deleted
Delete the trigger (namespace)Generated resource deletedNo effect
Modify the source (clone) or dataPropagated to copiesNot propagated
Delete the policy (data source)Downstream deleted unless orphanDownstreamOnPolicyDelete: trueRetained
Delete the clone sourceDownstream deletedRetained

Source: Kyverno generate rules. The practical takeaway: synchronize: true gives you desired-state enforcement, not a one-time copy. If you want a copy people can then edit freely, leave it false.

The RBAC that trips people up

Generation is performed by the kyverno-background-controller ServiceAccount. Its default permissions cover common, non-critical resources. Two situations need extra grants.

Generating a resource type the controller does not manage

To generate, say, a Deployment or a custom resource, add a ClusterRole with the background controller's aggregation label. Kyverno's ClusterRoles use aggregation, so a new role with the right label is merged into the controller's permissions automatically (customizing permissions).

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kyverno:generate-networkpolicies
labels:
rbac.kyverno.io/aggregate-to-background-controller: "true"
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["networkpolicies"]
verbs: ["create", "update", "delete", "get", "list", "watch"]

Notice the label rbac.kyverno.io/aggregate-to-background-controller: "true" is what wires this into the controller. Do not edit Kyverno's built-in roles; add a new one so upgrades do not overwrite your change. After applying, confirm aggregation with kubectl get clusterrole kyverno:background-controller -o yaml.

Generating RBAC resources

Generating a RoleBinding is a special case. Kubernetes prevents privilege escalation: to create a binding that grants a role, the creator must already hold that role. So the background controller's ServiceAccount must be bound to the same role you want to grant (generating bindings).

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kyverno:generate-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: admin
subjects:
- kind: ServiceAccount
name: kyverno-background-controller
namespace: kyverno

Notice this binds the background controller to admin so a policy can then generate RoleBindings that grant admin. Without this, Kubernetes rejects the generated binding, and it looks like a Kyverno bug when it is really RBAC working as designed.

Applying to existing namespaces

By default a generate rule only fires for new triggers. To apply it retroactively to namespaces that already exist, set generateExisting: true. It runs once when the policy is installed, then behaves like a normal generate rule.

generate:
generateExisting: true
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
name: default-deny
namespace: "{{request.object.metadata.name}}"
synchronize: true
data:
spec:
podSelector: {}
policyTypes: [Ingress, Egress]

Notice generateExisting: true is what closes the gap when you add Kyverno to a cluster that already has namespaces.

Failure modes and gotchas

  • Nothing generated. Check kubectl get updaterequests -A. A Failed status usually means missing RBAC on the background controller. Kyverno also validates permissions when you install the policy and will warn you.
  • "Privilege escalation" errors on RBAC generation. You did not bind the background controller to the role you are trying to grant. See the RBAC section.
  • Old match syntax. Bare match.resources.kinds is the pre-1.6 form. Use match.any (or match.all) with resources underneath, as shown here.
  • Forgetting apiVersion in generate. The current schema expects apiVersion and kind in the generate block.
  • Expecting a webhook to block edits. Synchronization reverts changes through the controller; it is eventually consistent, not an instant admission denial. Give it a moment.
  • data vs clone deletion semantics differ. Deleting a data-source policy deletes downstreams by default; deleting a clone source deletes downstreams too, but deleting a clone-source policy does not. Check the table before relying on cleanup behaviour.

Summary and next steps

Generate rules let Kyverno provision and maintain the resources every namespace needs: NetworkPolicies, quotas, secrets, and bindings. Use data for templated resources, clone for copies of an existing source, and synchronize: true when you want the cluster to keep enforcing the desired state. The most common failure is RBAC: grant the background controller permission with an aggregation label, and bind it to any role you want to generate bindings for.

Next, see part two of this series for more generate patterns, and the Kyverno policy library for ready-made examples.

References

© 2026 by Yuvraj 🧢. All rights reserved.
Theme by LekoArts