Skip to content
Back to blog Kyverno vs OPA: Policy Engines Compared

Kyverno vs OPA: Policy Engines Compared

SecurityK8s

Kyverno vs OPA: Policy Engines Compared

Both Kyverno and OPA Gatekeeper enforce policies in Kubernetes. OPA uses Rego, a purpose-built language. Kyverno uses YAML. This guide compares them with real examples so you can choose.

TL;DR

  • Kyverno: YAML-based, easier to learn, Kubernetes-native
  • OPA/Gatekeeper: Rego-based, more powerful, general-purpose
  • Kyverno for simpler policies and faster adoption
  • OPA for complex logic and non-K8s use cases
  • Both production-ready, both well-maintained

Quick Comparison

FEATURE                 KYVERNO             OPA/GATEKEEPER
=======                 =======             ==============
Policy Language         YAML                Rego
Learning Curve          Low                 Medium-High
Validation              ✅                  ✅
Mutation                ✅                  ✅
Generation              ✅                  ❌
Image Verification      ✅                  ❌ (external)
CLI Testing             ✅ (kyverno test)   ✅ (gator, conftest)
Non-K8s Use             ❌                  ✅
Performance             Good                Good
Community               Growing             Established

Same Policy, Different Languages

Block Privileged Containers

Kyverno:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged
spec:
  validationFailureAction: Enforce
  rules:
    - name: deny-privileged
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Privileged containers are not allowed"
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: "!true"
            initContainers:
              - securityContext:
                  privileged: "!true"

OPA/Gatekeeper:

# ConstraintTemplate
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8spsprivileged
spec:
  crd:
    spec:
      names:
        kind: K8sPSPPrivileged
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8spsprivileged

        violation[{"msg": msg}] {
          c := input_containers[_]
          c.securityContext.privileged == true
          msg := sprintf("Privileged container not allowed: %v", [c.name])
        }

        input_containers[c] {
          c := input.review.object.spec.containers[_]
        }

        input_containers[c] {
          c := input.review.object.spec.initContainers[_]
        }

---
# Constraint
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivileged
metadata:
  name: deny-privileged
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]

Verdict: Kyverno is more concise for this use case.

Required Labels

Kyverno:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-team-label
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
      validate:
        message: "Label 'team' is required"
        pattern:
          metadata:
            labels:
              team: "?*"

OPA/Gatekeeper:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg}] {
          provided := {l | input.review.object.metadata.labels[l]}
          required := {l | l := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("Missing labels: %v", [missing])
        }

---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment", "StatefulSet"]
  parameters:
    labels:
      - team

Verdict: Kyverno wins on simplicity, OPA wins on reusability.

Kyverno Unique Features

Generate Resources

Automatically create resources when others are created:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-network-policy
spec:
  rules:
    - name: generate-default-deny
      match:
        any:
          - resources:
              kinds:
                - Namespace
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny
        namespace: "{{request.object.metadata.name}}"
        data:
          spec:
            podSelector: {}
            policyTypes:
              - Ingress
              - Egress

Image Signature Verification

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/company/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----

Mutate with Context

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-image-pull-secret
spec:
  rules:
    - name: add-pull-secret
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          spec:
            imagePullSecrets:
              - name: ghcr-pull-secret

OPA Unique Features

Complex Logic with Rego

package kubernetes.admission

# Deny if total CPU requests exceed namespace quota
deny[msg] {
  input.request.kind.kind == "Pod"
  namespace := input.request.namespace
  
  # Get existing pods in namespace
  existing_pods := data.kubernetes.pods[namespace]
  
  # Calculate current CPU usage
  current_cpu := sum([cpu |
    pod := existing_pods[_]
    container := pod.spec.containers[_]
    cpu := parse_cpu(container.resources.requests.cpu)
  ])
  
  # Calculate requested CPU
  requested_cpu := sum([cpu |
    container := input.request.object.spec.containers[_]
    cpu := parse_cpu(container.resources.requests.cpu)
  ])
  
  # Get quota
  quota := data.quotas[namespace].cpu
  
  # Check if exceeds
  current_cpu + requested_cpu > quota
  
  msg := sprintf("CPU request exceeds namespace quota: %v + %v > %v",
    [current_cpu, requested_cpu, quota])
}

Cross-Resource Validation

package kubernetes.admission

# Deny if service selector doesn't match any deployment
deny[msg] {
  input.request.kind.kind == "Service"
  service := input.request.object
  selector := service.spec.selector
  
  # Check if any deployment matches
  not deployment_exists(input.request.namespace, selector)
  
  msg := sprintf("Service %v selector doesn't match any deployment", 
    [service.metadata.name])
}

deployment_exists(namespace, selector) {
  deployment := data.kubernetes.deployments[namespace][_]
  matches_selector(deployment.spec.template.metadata.labels, selector)
}

matches_selector(labels, selector) {
  all_match := [match |
    selector[key] = value
    match := labels[key] == value
  ]
  not false in all_match
}

Performance Comparison

Testing with 1000 pods:

SCENARIO                    KYVERNO     GATEKEEPER
========                    =======     ==========
Simple validation           ~2ms        ~3ms
Complex validation          ~5ms        ~4ms
Mutation                    ~3ms        ~4ms
Memory (idle)               ~200MB      ~300MB
Memory (1000 policies)      ~500MB      ~600MB

Both are production-ready. Performance is similar.

Migration: OPA to Kyverno

Common patterns:

# OPA: deny if no limits
# rego: not container.resources.limits.memory

# Kyverno equivalent:
validate:
  pattern:
    spec:
      containers:
        - resources:
            limits:
              memory: "?*"
# OPA: allow only specific registries
# rego: startswith(image, "gcr.io/company/")

# Kyverno equivalent:
validate:
  pattern:
    spec:
      containers:
        - image: "gcr.io/company/*"

When to Use Which

Choose Kyverno when:

  • Team prefers YAML over learning Rego
  • You need resource generation
  • You need image signature verification
  • Simpler policies are sufficient
  • Faster time-to-value is important

Choose OPA/Gatekeeper when:

  • You need complex policy logic
  • You have existing Rego expertise
  • You need policies outside K8s (Terraform, etc.)
  • Cross-resource validation is required
  • You need the OPA ecosystem (Conftest, etc.)

Install Both? (Hybrid Approach)

You can run both:

# Kyverno for mutations and generation
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-defaults
spec:
  rules:
    - name: add-labels
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              managed-by: platform

# Gatekeeper for complex validation
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sComplexValidation
metadata:
  name: cross-resource-check

References

======================================== Kyverno vs OPA Gatekeeper

Choose your weapon. Enforce your policies.

Found this helpful?

Comments