▶️Introduction
In Kyverno Policy Conflicts, we saw a pattern of structural failures:
- Multiple policies mutating the same fields
- Validation rules assuming a state that mutations later invalidate
- Policy intent fragmented across independent rules
- Even when policies are “correct,” nothing prevents someone from introducing a conflicting policy later
This is not a blog about fixing YAML syntax or tweaking individual rules.
It’s about designing policies so contradictions cannot exist in the first place.
🗺️The Strategy (High Level)
We prevent future contradictions using four layers, all enforceable by Kyverno:
- RBAC – Very few having access to creates, update,and delete the policy.
- Policy ownership – only one policy may own a given field.
- Intent isolation – policies for different intents must never overlap.
- Guardrail policies – deny creation of conflicting policies.
👥RBAC
RBAC can provide a basic guard against policy tampering or the introduction of contradictory Kyverno policies by unknown or unauthorized actors. However, RBAC alone is not sufficient—it only controls who can act, not what they are allowed to change or why.
There are two complementary ways to address this:
-
Restrict access via RBAC
Define strictRoleBindings/ClusterRoleBindingsso that only approved users or groups are allowed to create, update, or delete Kyverno policies. -
Enforce ownership with a Kyverno validating policy
Add a validating policy that denies any modification to Kyverno policies unless the request originates from approved service accounts—typically the ones responsible for policy reconciliation, such as Argo CD or CI/CD systems.
Example policy:
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: only-argocd-can-manage-kyverno-policies
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: ["policies.kyverno.io"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE", "DELETE"]
resources:
- mutatingpolicies
- validatingpolicies
- generatingpolicies
- deletingpolicies
- imagevalidatingpolicies
- policyexceptions
variables:
- name: sa
expression: parseServiceAccount(request.userInfo.username)
validations:
- message: "Only Argo CD is allowed to create/update/delete Kyverno policies."
expression: >
variables.sa.Namespace == "argocd" &&
variables.sa.Name == "argocd-application-controller"
🛡️Ownership + Guardrails
The core idea is simple: every mutable JSON path must have exactly one owning policy, and the cluster must enforce it.
Ownership means nothing if it isn’t enforced.Guardrails mean nothing without clear ownership.These two have to exist together.
Examples:
This policy owns the field entirely, encodes a complete and coherent intent, does not rely on execution order.
apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
name: canonical-security-context
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
expression: >
Object{
spec: Object.spec{
securityContext: Object.spec.securityContext{
runAsNonRoot: true,
runAsUser: 1000
}
}
}
Once a single policy is defined as the owner of securityContext, no other policy should be allowed to change it.
From that point on, the cluster must reject any new policy that tries to modify securityContext fields.
The YAML below enforces this by denying the creation of such policies.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: guardrail-security-context-ownership
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: ["policies.kyverno.io"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources:
- mutatingpolicies
validations:
- message: >
spec.securityContext is owned by the canonical-security-context policy.
Do not mutate it elsewhere.
expression: >
!object.spec.mutations.exists(m,
m.applyConfiguration.expression.contains("securityContext")
)
🏷️Intent Isolation: Prevent Accidental Interaction
The rule is simple policies of the same kind but with different intentions must be clearly separated.
This separation must be enforced through conditions in the policy spec. One of the most common and reliable ways to do this is by using labels to scope policy execution.
Intent via labels:
metadata:
labels:
policy.intent: security
Policies match only that intent:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
selector:
matchLabels:
policy.intent: security
📌Conculsion
Preventing policy conflicts isn’t about writing more rules it’s about setting clear boundaries.
When ownership is explicit, intent is isolated, and guardrails are enforced, Kyverno policies stop interacting in surprising ways. Exceptions stay intentional, and future mistakes are blocked early.
