How to balance developer velocity with security using Policy-as-Code and Common Expression Language.

Platform Engineering has never been a buzzword: it provides the backbone of scalable, secure, and developer-friendly [Kubernetes] operations. But as platforms grow, so does the chaos. At the heart of effective platform engineering lies a single challenge: How do you enforce consistent policies across clusters, workloads, and teams without becoming a bottleneck?

This is where Kyverno shines. By making use of the recent addition of CEL (Common Expression Language) support, it has evolved from a policy engine into a comprehensive governance platform.

What is Kyverno?

Kyverno is a policy engine explicitly designed for Kubernetes. Unlike general-purpose policy tools that require learning new languages (like OPA and Rego), Kyverno speaks Kubernetes natively. It uses standard Kubernetes resources to define and enforce policies, meaning no new complex syntax—just the YAML manifests that Kubernetes administrators already know and LOVEEEE...

Kyverno enables you to:

  1. Validate resources against policies before they're admitted to the cluster.
  2. Mutate resources automatically to inject defaults, labels, or sidecars.
  3. Generate resources dynamically based on triggers (like creating NetworkPolicies for new namespaces).
  4. Verify image signatures and attestations for supply chain security.

Why should platform engineering teams use Kyverno?

Platform Engineering is about enabling developer velocity while maintaining security and compliance. Kyverno acts as the guardrails that make this possible.

Self-service with guardrails

You can create an environment where developers can self-serve without requiring manual reviews for every deployment. Kyverno automates the “boring” stuff.

For example, you can automatically add resource limits to containers that don't specify them, inject sidecar containers for logging or monitoring, apply consistent labels and annotations across all workloads, or enforce naming conventions for resources.

Security by Default

Security shouldn't be an afterthought. Using Kyverno, platform teams can enforce security policies cluster-wide by blocking privileged containers, requiring container images from approved registries, enforcing pod security standards, verifying image signatures with Sigstore/Cosign integration, preventing hostPath volume mounts, and many other best practices.

Audit-ready compliance

Meeting standards like SOC 2, PCI-DSS, or HIPAA requires proof. Kyverno provides Policy Reports that show compliance status across clusters, and an Audit Mode to test policies safely before enabling enforcement.

Pro Tip: Always start new policies in Audit mode. This generates reports on what would have been blocked, allowing you to fix existing violations without disrupting production traffic.

The game-changer: CEL support

Common Expression Language (CEL) is a non-Turing-complete expression language that's fast becoming the standard for policy evaluation in the Kubernetes ecosystem. It is fast, portable, extensible, and safe.

Kubernetes supports CEL as Generally Available (GA) with CRD validation rules since v1.25 and on ValidatingAdmissionPolicy since v1.30. Kyverno extends this support in its latest releases.

While the ClusterPolicy (and namespaced Policy) supports CEL expressions, new policy types have been created for CEL. Figure 1 shows the additions.

Figure 1: Kyverno Policy types on versions 1.15 and 1.16
Why does CEL matter for platform engineers?
  1. Performance: CEL expressions are compiled and executed significantly faster than traditional webhook validations, reducing latency on resource creation/modification.
  2. Native Integration: Since Kubernetes 1.26+, CEL has been built into the ValidatingAdmissionPolicy (started as “alpha”). Kyverno’s support means you can use one language across your entire stack.
  3. Expressiveness: CEL handles complex logic with elegant, concise expressions.

Some Examples

  1. The following example creates a ClusterPolicy using CEL that requires each pod to have a label that contains only lowercase letters, numbers, and hyphens (underscores, uppercase letters, and other characters are not allowed).
apiVersion: kyverno.io/v1                                    # policy-require-labels.yaml
kind: ClusterPolicy
metadata:
  name: require-labels-cel
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-team-label
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      cel:
        expressions:
        - expression: "has(object.metadata.labels.team)"
          message: "All pods must have a 'team' label"
        - expression: "object.metadata.labels.team.matches('^[-a-z0-9]+$')"
          message: "Team label must contain only lowercase letters, numbers, and hyphens"

The above example uses the old ClusterPolicy type with the CEL extension, but we also have other policy types for the same purpose. The following examples will use those.

You can apply it to your cluster and see the result:

$> kubectl apply -f policy-require-labels.yaml 
clusterpolicy.kyverno.io/require-labels-cel created

$> kubectl run test-pod --image=busybox                                                    
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request: 

resource Pod/default/test-pod was blocked due to the following policies 

require-labels-cel:
  check-team-label: All pods must have a 'team' label

$> kubectl run test-pod --image=busybox -l team=my_team
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request: 

resource Pod/default/test-pod was blocked due to the following policies 

require-labels-cel:
  check-team-label: Team label must contain only lowercase letters, numbers, and hyphens
  1. Dynamic resource requests for memory based on the team size:
## v1beta1 will work with Kyverno 1.16
## for v1.15, use v1alpha1
apiVersion: policies.kyverno.io/v1beta1
kind: ValidatingPolicy
metadata:
  name: memory-requests-per-team-size
spec:
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: [v1]
      operations:  [CREATE, UPDATE]
      resources:   [pods]
  validations:
    - message: "Memory request exceeds team quota"
      expression: |
        object.metadata.labels.team == 'small-team' ?
          int(object.spec.containers[0].resources.requests.memory.replace('Mi', '')) <= 512 :
          int(object.spec.containers[0].resources.requests.memory.replace('Mi', '')) <= 2048

This policy will check the “team” label for the incoming pod requests. If the value is “small-team”, the memory request must be less than or equal to 512Mi; for other values, it must be less than or equal to 2048Mi.

Pro Tip: This policy assumes that the request value will be made with Mi and only checks the first container. For other possible values, the expression must be extended.

  1. Create a default deny NetworkPolicy for newly created namespaces. The policy below assigns the requested namespace name to the nsName variable and uses it in the expression:
## v1beta1 will work with Kyverno v1.16
## for v1.15, use v1alpha1
apiVersion: policies.kyverno.io/v1beta1
kind: GeneratingPolicy
metadata:
  name: generate-network-policy
spec:
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE"]
      resources:   ["namespaces"]
  variables:
    - name: nsName
      expression: "object.metadata.name"
  generate:
    - expression: >
        generator.Apply(variables.nsName, [
          {
            "kind": dyn("NetworkPolicy"),
            "apiVersion": dyn("networking.k8s.io/v1"),
            "metadata": dyn({
              "name": "default-deny",
            }),
            "spec": dyn({
              "podSelector": dyn({}),
              "policyTypes": dyn(["Ingress", "Egress"])
            })
          }]
        )
  1. Parametrized Policies: Kyverno now supports ClusterPolicy with parameter resources. This allows you to write a policy once (as a template) and configure it differently for every namespace or environment.

Why is this huge for GitOps?

  1. Reusability: The policy logic stays static; only the config changes.
  2. Team Autonomy: Platform teams define the rule (e.g., "Replica Limit"), but individual teams can be assigned different limits via a simple ConfigMap, Secret, or CRD.
## Define the maxReplicas per environment in a ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: replica-limit
  namespace: default
data:
  maxReplicas: "3"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: check-deployment-replicas
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: deployment-replicas
      match:
        any:
        - resources:
            kinds:
              - Deployment
              - Deployment/scale
            operations:
              - CREATE
              - UPDATE
      validate:
        cel:
          paramKind: 
            apiVersion: v1
            kind: ConfigMap
          paramRef:
            name: "replica-limit"
            namespace: default
            parameterNotFoundAction: "Deny"
          expressions:
            - expression: "object.spec.replicas <= int(params.data.maxReplicas)"
              messageExpression: "'Deployment spec.replicas must be less than ' + string(params.data.maxReplicas)"

See it in action:

$> kubectl apply -f policy-replica-limit.yaml 
configmap/replica-limit created
clusterpolicy.kyverno.io/check-deployment-replicas created

$> kubectl create deploy test-deploy --image=nginx:mainline-alpine-slim --replicas=5
error: failed to create deployment: admission webhook "validate.kyverno.svc-fail" denied the request: 

resource Deployment/default/test-deploy was blocked due to the following policies 

check-deployment-replicas:
  deployment-replicas: Deployment spec.replicas must be less than 3

$> kubectl create deploy test-deploy --image=nginx:mainline-alpine-slim --replicas=2
deployment.apps/test-deploy created

$> kubectl scale deploy test-deploy --replicas=5                                    
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request: 

resource Scale/default/test-deploy was blocked due to the following policies 

check-deployment-replicas:
  deployment-replicas: Deployment spec.replicas must be less than 3

Final thoughts

Kyverno has become an essential tool for platform engineering teams building self-service Kubernetes platforms. Its Kubernetes-native approach, combined with CEL's raw performance and expressiveness, makes it the ideal choice for balancing velocity with governance.

Whether you are just starting your platform engineering journey or looking to level up your existing stack, Kyverno deserves a place in your toolkit.

Ready to get started? Check out the Kyverno documentation and join the growing community of platform engineers using policy-as-code to build better Kubernetes platforms. You can find more example policies here and try them out on the playground.