Bootstrapping Kubernetes namespaces with Kyverno

If you manage a Kubernetes cluster with multiple applications or tenants, you likely understand the repetitive task of configuring each namespace with the appropriate defaults. This includes setting up network isolation, image pull credentials, and custom certificates—essential but monotonous tasks. Kyverno is not only a policy enforcement engine but also a tool to automate the clean and declarative bootstrapping of these resources.

What is Kyverno?

Kyverno is a Kubernetes-native policy engine designed for security and governance. It's often compared to Open Policy Agent (OPA), but unlike OPA, Kyverno uses YAML and understands Kubernetes resources out of the box. While OPA requires learning Rego, a purpose-built policy language, Kyverno works directly with familiar Kubernetes manifests.

Most people primarily use it to enforce cluster-wide or namespace-level rules, such as blocking privileged containers or enforcing label conventions.

Why bootstrap namespaces?

In most clusters, each namespace needs to be initialized with a few baseline resources: network policies to enforce isolation, image pull secrets for private registries, shared config like certificate authorities, and resource quotas to prevent overuse. Doing this by hand—or wiring it into every Helm chart—is error-prone and hard to maintain consistently.

Kyverno solves this by making the defaults automatic, versioned, and self-healing through declarative policies.

Kyverno as a bootstrapper

Beyond enforcement, Kyverno is also great for bootstrapping namespaces with sensible defaults. It doesn't just create resources—it keeps them in sync with a source template. That means Kyverno can apply updates to your standard NetworkPolicy, default certificate bundles, or other shared configs.

At the core of this is Kyverno's generate rule. It can create resources when a namespace is created or if a resource is missing. If someone changes the resource later, Kyverno puts it back in line. Combined with match and exclude rules, you can automate the setup of secrets, configmaps, policies, and more.

⚠️ Be cautious when using synchronize: true—Kyverno will enforce the generated resource to match the source, which can overwrite manual changes in downstream namespaces.

Let's look at four real-world examples:


1. Enforce network isolation with a default NetworkPolicy

Every namespace should have a baseline NetworkPolicy that isolates traffic and only allows DNS resolution via kube-dns. This ensures that applications start isolated, and teams must explicitly declare exceptions.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: default-deny
spec:
  rules:
    - name: add-default-network-policy
      match:
        resources:
          kinds:
            - Namespace
      generate:
        kind: NetworkPolicy
        apiVersion: networking.k8s.io/v1
        name: default-deny
        namespace: ""
        synchronize: true
        generateExisting: true
        data:
          metadata:
            name: default-deny
          spec:
            podSelector: {}
            policyTypes:
              - Ingress
              - Egress
            ingress:
              - to:
                  - podSelector: {}  # allow traffic to any pod in same namespace
            egress:
              - to:
                  - podSelector: {}  # allow traffic to any pod in same namespace
              - to:
                  - namespaceSelector: # allow traffic to kube-dns in kube-system
                      matchLabels:
                        kubernetes.io/metadata.name: kube-system
                    podSelector:
                      matchLabels:
                        k8s-app: kube-dns
                ports:
                  - protocol: UDP
                    port: 53

This rule secures every new namespace, starting with tight ingress & egress controls. When applied, you'll see a default-deny policy in each namespace, allowing only DNS traffic to kube-dns in the kube-system namespace:

$ kubectl create ns test-namespace
namespace/test-namespace created
$ kubectl get networkpolicy -n test-namespace
NAME           POD-SELECTOR   AGE
default-deny   <none>         3s

2. Automatically generate image pull secrets

If you use a private container registry, every application namespace likely needs credentials to pull images. Instead of copying secrets manually or relying on manual steps in Helm charts, inject them using Kyverno.

Kyverno assumes there's an image-pull-secret in the kyverno namespace, you can create this secret with:

$ kubectl create secret docker-registry image-pull-secret -n kyverno --docker-username=foo --docker-password=password

By default, Kyverno isn't allowed to interact with secrets, so you need to permit it to do so. You can do this by creating these ClusterRole resources. This works with aggregated roles, adding them to the existing kyverno:admission and kyverno:background roles.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno:admission:secrets
  labels:
    rbac.kyverno.io/aggregate-to-admission-controller: "true"
rules:
  - apiGroups:
      - ""
    resources:
      - secrets
    verbs:
      - list
      - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno:background:secrets
  labels:
    rbac.kyverno.io/aggregate-to-background-controller: "true"
rules:
  - apiGroups:
      - ""
    resources:
      - secrets
    verbs:
      - list
      - get
      - create
      - update
      - delete
      - patch

Then, use Kyverno to copy the created secret into every new namespace.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: copy-image-pull-secret
spec:
  rules:
    - name: generate-image-pull-secret
      match:
        resources:
          kinds:
            - Namespace
      generate:
        generateExisting: true
        kind: Secret
        apiVersion: v1
        name: image-pull-secret
        namespace: ""
        synchronize: true
        clone:
          namespace: kyverno
          name: image-pull-secret

This example copies the existing secret into every new namespace. You could add exclude conditions to skip dev namespaces or add annotations if needed.


3. Inject a custom CA certificate

Some applications must trust internal services with a custom certificate authority (CA). You can use Kyverno to inject a CA bundle secret into each namespace so apps can mount or use it as needed.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-ca-secret
spec:
  rules:
    - name: copy-ca-secret
      match:
        resources:
          kinds:
            - Namespace
      generate:
        generateExisting: true
        kind: Secret
        apiVersion: v1
        name: custom-ca
        namespace: ""
        synchronize: true
        clone:
          namespace: kyverno
          name: custom-ca

This example will copy the existing custom-ca secret from the kyverno namespace into every new and existing namespace.


4. Set ResourceQuota templates per namespace

You might want to apply baseline ResourceQuota objects to each namespace to prevent noisy neighbor issues in a multi-tenant cluster.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: default-resource-quota
spec:
  rules:
    - name: generate-default-quota
      match:
        resources:
          kinds:
            - Namespace
      generate:
        generateExisting: true
        kind: ResourceQuota
        apiVersion: v1
        name: default-quota
        namespace: ""
        synchronize: true
        data:
          metadata:
            name: default-quota
          spec:
            hard:
              requests.cpu: "1"
              requests.memory: 1Gi
              limits.cpu: "2"
              limits.memory: 2Gi

This example ensures new namespaces don't get to consume unbounded resources by default.


A note on trade-offs

Using Kyverno to bootstrap namespaces brings consistency, but it also means you're applying cluster-wide defaults—broad strokes that can limit flexibility later. For example, this article's default NetworkPolicy allows all traffic within a namespace. It's a valid starting point but prevents more granular segmentation later since Kyverno will keep restoring the broader rule. The same applies to ResourceQuota: if you need to scale an app beyond the limits, adjusting the quota might affect all namespaces unless scoped more carefully.

To avoid these limitations:

  • Use labels to scope policies. You can apply defaults only to namespaces with a specific label, like env=prod or bootstrap=default, allowing opt-outs or overrides elsewhere.
  • Split policies by environment or team to reduce blast radius. For example, use different policies for shared namespaces vs. team-specific ones.
  • Use exclude blocks to prevent policies from applying to specific namespaces (e.g., namespace != critical-apps).

Wrap-up

Using Kyverno for bootstrapping is a form of policy-as-code that brings consistency to your cluster setup. Whether dealing with app isolation, access credentials, TLS configuration, or resource limits, Kyverno makes the defaults repeatable, auditable, and self-healing.

Kyverno has a repository with many more policies ready to use or draw inspiration from.