Designing Conflict-Free Kyverno Policies: Patterns for Ownership, Scope, and Determinism
Source: Dev.to
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 article is not 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 create, 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.
Two complementary ways to address this:
- Restrict access via RBAC – Use
RoleBindings/ClusterRoleBindingsso that only approved users or groups are allowed to create, update, or delete Kyverno policies. - Enforce ownership with a Kyverno validating 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.
Example Owner Policy
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. The following guardrail policy denies creation of conflicting 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
Policies of the same kind but with different intentions must be clearly separated.
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
Conclusion
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.