TT Lab
Get started
Learn Learning paths Courses

Policy as Code

A Generation Policy That Automates Tenant Onboarding

Continue in TT Lab

Goal

Write a generation policy so that default security and resource objects appear together the moment a new tenant namespace is created, and also put in place what else is needed for that policy to work in a real cluster.

Why it matters

Handing a new team a namespace usually runs on a checklist. Apply a NetworkPolicy, put a ResourceQuota on it, add a LimitRange, and copy the registry secret. When people do it, someday a step is missed, and if the missed one happens to be the default-deny policy, that namespace alone stays open for months. A generation rule binds this checklist to the namespace-creation event. You learn three things here. First, if you do not narrow the trigger scope with a label, even system namespaces become targets. Second, anything whose content must not be written in a policy file, such as a Secret, is handled by pointing only at the source with clone — because policies go into git. Third, the background controller is installed with minimal permissions, so you must attach permissions for the kind of resource to be created separately, and if the permission is missing, the request succeeds and only the resource quietly fails to appear. In this environment the controller does not run, so no actual resources are created, and grading uses the results evaluated locally with the kyverno CLI and the structure of the policy YAML.

Steps

  1. In /root/policy/generate/add-networkpolicy.yaml, create a kind: ClusterPolicy. In spec.rules[0].generate, write apiVersion: networking.k8s.io/v1, kind: NetworkPolicy, and name: default-deny, and in generate.data.spec put podSelector: {} and policyTypes, making sure to include Ingress.
  2. Set the same rule's match.any[0].resources.kinds to Namespace, and write labhub.io/tenant: "true" in selector.matchLabels so that it reacts only to tenant namespaces.
  3. Set generate.synchronize to true. And in /root/policy/generate/out/sync-note.txt, write both the behavior when it is on (if someone edits it by hand, it reverts it) and the difference when it is off (it intervenes only once at creation).
  4. Create /root/policy/generate/clone-registry-secret.yaml. generate.kind is Secret, generate.clone.namespace is pol-lab, and generate.clone.name is registry-creds. You must not write generate.data, and the secret content must not go into the policy file either.
  5. Set generate.namespace of add-networkpolicy.yaml to "{{ request.object.metadata.name }}" and state generate.name explicitly. And in /root/policy/generate/out/vars-note.txt, sum up the variables usable in a policy, making sure to include request.object and a variable that holds requester information (request.operation or request.userInfo).
  6. In /root/policy/generate/background-rbac.yaml, create a kind: ClusterRole. Include networkpolicies in rules and put create and update in verbs (since synchronization is on, updating is needed too). Attach an aggregation label such as rbac.kyverno.io/aggregate-to-background-controller: "true" in metadata.labels.
  7. Run kyverno apply /root/policy/generate/add-networkpolicy.yaml --resource /opt/lab/fixtures/policy/resources/target-ns.yaml, and save only the generated NetworkPolicy manifest to /root/policy/generate/out/generated.txt. Do not include the execution summary line (pass: ..., error: 0 ...).
  8. In /root/policy/generate/tenant-onboarding.yaml, create a policy with 3 rules. Each rule generates a NetworkPolicy, a ResourceQuota, and a LimitRange respectively, the rule names must differ from one another, and all three rules must have generate.synchronize: true. And in /root/policy/generate/out/onboarding-report.json, put trigger_namespace as tenant-alpha and, in the generated array, at least 3 resources that will be generated, including the kind key (ResourceQuota must be included).

Notes

Write a rule that creates a default NetworkPolicy

In /root/policy/generate/add-networkpolicy.yaml, create a kind: ClusterPolicy. In spec.rules[0].generate, write apiVersion: networking.k8s.io/v1, kind: NetworkPolicy, and name: default-deny, and in generate.data.spec put podSelector: {} and policyTypes, making sure to include Ingress.

A generation rule first writes the apiVersion, kind, name, and namespace of the resource to create. There is a separate key for writing the content directly in the policy.

Narrow the trigger and target selector

Set the same rule's match.any[0].resources.kinds to Namespace, and write labhub.io/tenant: "true" in selector.matchLabels so that it reacts only to tenant namespaces.

In match, you write what creation it should react to. If you create in every namespace, system namespaces also become targets, so narrow it with a label.

Turn on synchronization and sum up what it means

Set generate.synchronize to true. And in /root/policy/generate/out/sync-note.txt, write both the behavior when it is on (if someone edits it by hand, it reverts it) and the difference when it is off (it intervenes only once at creation).

When it is on, it keeps watching even after creation. You pass only if you write up in a file how it differs from when it is off.

Write a clone rule that copies the source

Create /root/policy/generate/clone-registry-secret.yaml. generate.kind is Secret, generate.clone.namespace is pol-lab, and generate.clone.name is registry-creds. You must not write generate.data, and the secret content must not go into the policy file either.

If you write the secret content in the policy file, the policy itself becomes a leak path. There is a method that only tells where the source is, and it cannot be used together with the key that writes the content directly.

Decide the target with request context variables

Set generate.namespace of add-networkpolicy.yaml to "{{ request.object.metadata.name }}" and state generate.name explicitly. And in /root/policy/generate/out/vars-note.txt, sum up the variables usable in a policy, making sure to include request.object and a variable that holds requester information (request.operation or request.userInfo).

You take the name of the namespace that was created from the request object. You pass only if you sum up three kinds of variables usable in a policy.

Write the ClusterRole for generation permissions

In /root/policy/generate/background-rbac.yaml, create a kind: ClusterRole. Include networkpolicies in rules and put create and update in verbs (since synchronization is on, updating is needed too). Attach an aggregation label such as rbac.kyverno.io/aggregate-to-background-controller: "true" in metadata.labels.

The background controller has no permissions of its own. If you turned on synchronization, you need updating as well as creating. Do not forget the aggregation label that gathers permissions.

Run it against the trigger resource and save the generated result

Run kyverno apply /root/policy/generate/add-networkpolicy.yaml --resource /opt/lab/fixtures/policy/resources/target-ns.yaml, and save only the generated NetworkPolicy manifest to /root/policy/generate/out/generated.txt. Do not include the execution summary line (pass: ..., error: 0 ...).

What you save is the generated resource manifest. Do not include the execution summary line — grading looks for the error string.

Build an onboarding policy bundle and a result report

In /root/policy/generate/tenant-onboarding.yaml, create a policy with 3 rules. Each rule generates a NetworkPolicy, a ResourceQuota, and a LimitRange respectively, the rule names must differ from one another, and all three rules must have generate.synchronize: true. And in /root/policy/generate/out/onboarding-report.json, put trigger_namespace as tenant-alpha and, in the generated array, at least 3 resources that will be generated, including the kind key (ResourceQuota must be included).

You create the three things, network, quota, and default resources, as different rules inside one policy. The rule names must not overlap, and synchronization must be on for all of them.