Policy as code: RBAC, OPA Gatekeeper, Kyverno
What happens to a request before it reaches etcd, what RBAC can't enforce, and one policy written three ways: ValidatingAdmissionPolicy, Kyverno and OPA Gatekeeper.
Key points:
- You need both RBAC and admission policy - RBAC can’t look inside the object.
- Default to ValidatingAdmissionPolicy (GA in Kubernetes 1.30) for simple checks; nothing to install.
- Use Kyverno for mutation, generation, image verification or reports; write new policies as
ValidatingPolicy, since Kyverno 1.19 deprecatesClusterPolicy. - Use Gatekeeper when you already use OPA and want one Rego language across Kubernetes, Terraform and CI.
- Roll out every policy in audit mode first, and match controllers, not only Pods.
The path of a request
Every kubectl apply, controller update and ReplicaSet-created pod goes through the same API server steps:
flowchart LR
REQ[Request] --> AUTHN["Authentication<br/>who are you?"]
AUTHN --> AUTHZ["Authorization (RBAC)<br/>may you do this verb<br/>on this resource?"]
AUTHZ --> MUT["Mutating admission<br/>add defaults, sidecars, labels"]
MUT --> SCHEMA[Schema validation]
SCHEMA --> VAL["Validating admission<br/>is this object allowed?"]
VAL --> ETCD[(etcd)]
RBAC vs admission policy
Use both. RBAC alone means anyone allowed to create a pod can create a privileged one.
- RBAC: “may this user create Deployments in this namespace?” Verbs, resources, namespaces - it can’t look inside the object.
- Admission policy: “is this Deployment acceptable?” It reads the object: memory limits set, image from our registry, running as root?
Which engine
| ValidatingAdmissionPolicy | Kyverno | OPA Gatekeeper | |
|---|---|---|---|
| Install | Built in (GA in Kubernetes 1.30) | Extra controller | Extra controller |
| Language | CEL expressions | CEL (ValidatingPolicy); YAML patterns in the older ClusterPolicy | Rego |
| Validate | Yes | Yes | Yes |
| Mutate | Separate MutatingAdmissionPolicy, newer | Yes | Yes |
| Generate resources | No | Yes (e.g. a default NetworkPolicy per namespace) | No |
| Verify image signatures | No | Yes | Through external data |
| Reports on existing resources | No | Yes | Yes (audit) |
| Use outside Kubernetes | No | Mostly no | Yes - same Rego in CI with OPA or conftest |
- Default: ValidatingAdmissionPolicy for simple checks - nothing to install or keep running.
- Use Kyverno when: you also want mutation, generation, image verification or reports on what’s already running.
- Use Gatekeeper when: you already use OPA and want one policy language for Kubernetes, Terraform and CI.
One policy, three ways: require memory limits
ValidatingAdmissionPolicy (CEL)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-memory-limits
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments", "statefulsets", "daemonsets"]
validations:
- expression: >-
object.spec.template.spec.containers.all(c,
has(c.resources) && has(c.resources.limits) &&
'memory' in c.resources.limits)
message: "Every container must set a memory limit."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-memory-limits
spec:
policyName: require-memory-limits
validationActions: ["Deny"] # start with ["Warn", "Audit"]
matchResources:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system"]
Kyverno
Use ValidatingPolicy: the built-in CEL plus Kyverno’s extras. Kyverno auto-generates checks for Deployments, StatefulSets, Jobs and other pod controllers, so a Pod policy rejects a bad Deployment at kubectl apply, not later.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-memory-limits
spec:
validationActions: [Deny] # start with [Audit]
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: [kube-system, kyverno]
validations:
- expression: >-
object.spec.containers.all(c,
has(c.resources) && has(c.resources.limits) &&
'memory' in c.resources.limits)
message: "Every container must set a memory limit."
The older ClusterPolicy with YAML patterns is still everywhere and still works, but Kyverno 1.19 marks it deprecated:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-memory-limits
spec:
background: true
rules:
- name: memory-limit
match:
any:
- resources:
kinds: ["Pod"]
exclude:
any:
- resources:
namespaces: ["kube-system", "kyverno"]
validate:
failureAction: Enforce # start with Audit
message: "Every container must set a memory limit."
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
"?*" means “any non-empty value”.
OPA Gatekeeper (Rego)
A ConstraintTemplate defines the logic once; a Constraint applies it with parameters and scope. The Rego uses the classic (v0) syntax that Gatekeeper’s rego field expects.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredmemorylimits
spec:
crd:
spec:
names:
kind: K8sRequiredMemoryLimits
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredmemorylimits
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("container %v must set a memory limit", [container.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredMemoryLimits
metadata:
name: require-memory-limits
spec:
enforcementAction: deny # start with dryrun or warn
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system"]
Gotchas
- Pod-only policies fail late. With a Gatekeeper or VAP policy matching only Pods, the Deployment applies and the ReplicaSet silently fails to create pods (only an event shows it). Match the controllers too, or use Kyverno, which does it for you.
- A failing webhook can stop the cluster. With
failurePolicy: Failand the engine down, nothing matching can be created - including its own replacement pods. Run them highly available, and excludekube-systemand the engine’s own namespace. - Enforcing without auditing first. Direct enforcement rejects workloads you didn’t know were in violation. Use warn or audit mode, read what would be rejected, fix it, then enforce.
- Existing resources aren’t checked at admission. New policies only see new and updated objects. Use background scans or audit reports for what’s already running.
- Mutation order is not guaranteed. Multiple mutating webhooks run in no guaranteed order. Don’t write two mutations that depend on each other.
- Tags are not identities.
image: app:1.4can point to a different image tomorrow. Require digests, or verify signatures (KyvernoverifyImageswith cosign). - Untested policy breaks things. Policy is code and fails like code. Keep it in Git, deploy it through GitOps, and test it in CI with the Kyverno CLI,
gatorfor Gatekeeper, or a dry-run cluster.