Skip to main content
CI/CD·6 min read

Kyverno with ArgoCD — Policy as Code Delivered via GitOps

Use Kyverno to enforce Kubernetes policies written in plain YAML and let ArgoCD deliver them through GitOps so governance is version-controlled, reviewable, and self-healing.

DT

DevOps Engineer & Technical Writer

The Problem

Cluster governance tends to live in people's heads: "don't use the latest tag," "always set resource limits," "no privileged pods." These rules get written in a wiki and ignored. Kyverno turns them into enforceable Kubernetes policies — written in YAML, no Rego to learn — that run at admission time. The missing piece is delivery: how do policies get onto every cluster and stay there? That is where ArgoCD comes in, syncing policies from Git and self-healing them if anyone tampers with them.

POLICY AS CODE — GIT → ARGOCD → KYVERNO → ADMISSION Git Repo ClusterPolicy YAML ArgoCD sync + self-heal Kyverno admission webhook ALLOW ✓ compliant pod scheduled BLOCK ✗ uses :latest tag request rejected

The result is governance that is reviewed in pull requests, versioned in Git, enforced at admission, and automatically restored if removed.

Why Kyverno

Kyverno is a Kubernetes-native policy engine. Its policies are plain Kubernetes resources, so the same kubectl, RBAC, and GitOps tooling you already use applies to them. There are three policy actions:

ActionWhat it does
<strong>validate</strong>Allow or deny a request based on rules (e.g. require resource limits)
<strong>mutate</strong>Modify a request before it is stored (e.g. inject default labels)
<strong>generate</strong>Create related resources automatically (e.g. a default NetworkPolicy per namespace)

Because there is no Rego, platform engineers who already read Kubernetes YAML can author policies on day one.

A Validation Policy — Disallow the <code class="inline-code">latest</code> Tag

The classic first policy: reject any container image using the mutable :latest tag.

apiVersion: kyverno.io/v1

kind: ClusterPolicy

metadata:

name: disallow-latest-tag

spec:

validationFailureAction: Enforce

background: true

rules:

- name: require-image-tag

match:

any:

- resources:

kinds: ["Pod"]

validate:

message: "Using the 'latest' tag or no tag is not allowed."

pattern:

spec:

containers:

- image: "!:latest & :*"

A Validation Policy — Require Resource Limits

A second common rule: every container must declare CPU and memory limits so one workload cannot starve a node.

apiVersion: kyverno.io/v1

kind: ClusterPolicy

metadata:

name: require-resource-limits

spec:

validationFailureAction: Enforce

rules:

- name: validate-limits

match:

any:

- resources:

kinds: ["Pod"]

validate:

message: "CPU and memory limits are required."

pattern:

spec:

containers:

- resources:

limits:

memory: "?*"

cpu: "?*"

Enforce vs Audit

The validationFailureAction field decides how strict a policy is:

  • Enforce — non-compliant requests are rejected at admission. Use this for hard rules in production.
  • Audit — non-compliant requests are allowed but a PolicyReport records the violation. Use this to roll out a new policy without breaking existing workloads.

A safe rollout is: ship a policy as Audit, watch the reports for a week, then flip it to Enforce in a reviewed pull request.

kubectl get clusterpolicy

kubectl get policyreport -A

kubectl describe clusterpolicy disallow-latest-tag

Delivering Policies with ArgoCD

Store all policies in a Git folder and let ArgoCD own it. Because policies are just manifests, the Application looks like any other:

apiVersion: argoproj.io/v1alpha1

kind: Application

metadata:

name: kyverno-policies

namespace: argocd

spec:

project: platform

source:

repoURL: https://github.com/devopskit/policies.git

targetRevision: main

path: kyverno

destination:

server: https://kubernetes.default.svc

namespace: kyverno

syncPolicy:

automated:

prune: true

selfHeal: true

syncOptions:

- ServerSideApply=true

selfHeal: true means if someone deletes a policy to sneak a non-compliant workload through, ArgoCD puts it right back. Governance becomes tamper-resistant.

Ordering with Sync Waves

Kyverno (the controller) must exist before its policies. Use ArgoCD sync waves so the engine installs first:

metadata:

annotations:

argocd.argoproj.io/sync-wave: "1" # policies apply after wave 0 (Kyverno)

Common Mistakes to Avoid

  • Shipping new policies straight as Enforce. You will block existing, non-compliant workloads and cause an outage. Start with Audit.
  • No sync-wave ordering. If ArgoCD applies policies before the Kyverno controller is ready, the webhook calls fail. Install the engine in an earlier wave.
  • Letting the webhook fail-closed without a plan. If Kyverno is down and the webhook fail policy is strict, admission for the whole cluster can stall. Scope policies and set failurePolicy deliberately.
  • Policies that match everything. Over-broad match blocks can hit kube-system and break the cluster. Scope by namespace or labels.
  • Editing policies with kubectl on the cluster. With selfHeal on, ArgoCD reverts you. Change policies in Git, through review.

FAQ

How is Kyverno different from OPA/Gatekeeper?

Kyverno policies are written in native Kubernetes YAML, whereas Gatekeeper uses the Rego language via OPA. For teams already fluent in Kubernetes manifests, Kyverno has a gentler learning curve, and it also supports mutate and generate actions, not just validation.

What is the difference between Enforce and Audit?

Enforce rejects non-compliant requests at admission, stopping the resource from being created. Audit allows the request but records the violation in a PolicyReport, which is ideal for testing a policy's impact before turning it on fully.

Why deliver Kyverno policies through ArgoCD instead of applying them directly?

GitOps delivery makes every policy change a reviewed, versioned pull request and gives you self-healing: if a policy is deleted or edited on the cluster, ArgoCD restores it from Git. That turns governance into something auditable and tamper-resistant rather than ad hoc.

Can Kyverno fix non-compliant resources automatically?

Yes, through mutate policies. Instead of rejecting a request, a mutate rule can inject sensible defaults — for example adding standard labels or a default security context — before the resource is stored, which reduces friction for application teams.