Designing Conflict-Free Kyverno Policies: Patterns for Ownership, Scope, and Determinism

Published: (February 9, 2026 at 03:15 AM EST)
3 min read
Source: Dev.to

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:

  1. RBAC – Very few having access to create, update, and delete the policy.
  2. Policy ownership – Only one policy may own a given field.
  3. Intent isolation – Policies for different intents must never overlap.
  4. 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 / ClusterRoleBindings so 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.

0 views
Back to Blog

Related posts

Read more »