How to Install Kyverno 1.19 and Secure Kubernetes Workloads with CEL

Updated on Sep 4, 2026
Mila H
14 MINS READ
Table of Contents
Kyverno 1.19 CEL Policies

Kyverno CEL policy controls help stop unsafe workloads before Kubernetes creates them. This guide installs Kyverno 1.19 with Helm, creates a clean policy workspace, tests policies in Audit mode, and then changes them to Deny mode after review. 

What You Will Build

The goal is to build a practical Kubernetes security baseline that:

  • Blocks privileged containers.
  • Requires containers to run as non-root.
  • Requires CPU and memory requests and limits.
  • Blocks host network, PID, and IPC namespace access.
  • Allows images only from approved registries.
  • Creates basic resources automatically in new production namespaces.
  • Lets you test every rule before it blocks real applications.

For full Kubernetes protection, you can use Kyverno together with runtime security like Falco. Kyverno checks manifests before workloads start, while Falco can detect suspicious behavior after a container is already running. 

For the runtime security setup, you can check the Falco Runtime Security Setup on Kubernetes.

Requirements for Kyverno Setup

Before installing Kyverno, make sure you have access to a Kubernetes cluster and that kubectl is configured correctly.

You need:

  • A Kubernetes cluster.

  • A Linux machine, VPS, or admin workstation with access to that cluster.

  • kubectl installed.

  • Helm 3 installed.

  • Cluster-admin permissions.

  • A test cluster or a non-critical namespace for the first policy tests.

Run these commands to check your local tools:

Bash
kubectl version --clienthelm version

Check which Kubernetes cluster your terminal is connected to:

Bash
kubectl config current-contextkubectl cluster-info

Then confirm that you can create Kyverno policy resources:

Bash
kubectl auth can-i create validatingpolicies.policies.kyverno.iokubectl auth can-i create generatingpolicies.policies.kyverno.io

Both commands should return Yes. Do not start by enforcing these rules on system namespaces such as kube-system, kyverno, or the namespace where Falco runs. Start with a controlled application namespace first.

Step 1: Create a Kyverno Project Folder

Before creating YAML files, make a clear working folder on your admin machine. This keeps policies, test workloads, and future CI files in one place.

Bash
mkdir -p ~/kyverno-1.19-production/policiesmkdir -p ~/kyverno-1.19-production/workloadsmkdir -p ~/kyverno-1.19-production/testscd ~/kyverno-1.19-production
  • policies/ stores Kyverno YAML policy files.

  • workloads/ stores good and bad example Pods for testing.

  • tests/ can store CI checks, test cases, or future Kyverno CLI test files.

This simple structure makes policies easier to manage in Git later.

Step 2: Add the Kyverno Helm Repository

Helm installs Kubernetes applications from charts. Kyverno publishes its official chart in the Kyverno Helm repository.

First, add the repository and download the newest chart information:

Bash
helm repo add kyverno https://kyverno.github.io/kyverno/helm repo update

Check the chart versions that Helm can see:

Bash
helm search repo kyverno/kyverno --versions | head -20

Look for a Kyverno 1.19 release before continuing. Helm is the recommended Kyverno installation option because it supports repeatable configuration and upgrades.

Step 3: Install Kyverno 1.19

Kyverno runs several controllers inside your Kubernetes cluster. The admission controller is the most important step because it receives workload requests and checks them with your policies.

Install Kyverno in its own namespace:

Bash
helm upgrade --install kyverno kyverno/kyverno \  --namespace kyverno \  --create-namespace \  --version v1.19.0 \  --set replicaCount=3

Wait for the admission controller to be ready and check all Kyverno Pods:

Bash
kubectl rollout status deployment/kyverno-admission-controller \  -n kyverno \  --timeout=5m kubectl get pods -n kyverno

The Pods should eventually show Running status. If a Pod is still starting, wait a little longer:

Bash
kubectl get pods -n kyverno --watch

Press Ctrl+C once all required Pods are running. Confirm the installed Helm release:

Bash
helm list -n kyverno

Kyverno uses Kubernetes admission webhooks to inspect resources during creation and update requests. Confirm that its webhooks exist:

Bash
kubectl get validatingwebhookconfigurations | grep kyvernokubectl get mutatingwebhookconfigurations | grep kyverno

If the commands return Kyverno-related entries, the admission layer is installed.

Step 4: Install the Kyverno CLI

The Kyverno CLI lets you check policies with YAML files before you deploy them to Kubernetes. This is useful for local testing, CI pipelines, and GitOps workflows.

Set the Kyverno version and download the Linux AMD64 CLI archive:

Bash
export KYVERNO_VERSION=v1.19.0 curl -L -o kyverno.tar.gz \  "https://github.com/kyverno/kyverno/releases/download/${KYVERNO_VERSION}/kyverno-cli_${KYVERNO_VERSION#v}_linux_x86_64.tar.gz"

Extract the archive and install the binary package:

Bash
tar -xzf kyverno.tar.gzsudo install -m 0755 kyverno /usr/local/bin/kyverno

Confirm the CLI works:

Bash
kyverno version

If you use ARM64, download the ARM64 build instead of the linux_x86_64 archive. Use the release files that match your server architecture.

Step 5: Create a Safe Test Namespace

Policies should not be enforced across the entire cluster immediately. A safer method is to target only namespaces with a specific label. Create a namespace for testing:

Bash
kubectl create namespace policy-lab

Add the label used by every workload policy:

Bash
kubectl label namespace policy-lab \  kyverno.perlod.com/security-tier=production

Check the label:

Bash
kubectl get namespace policy-lab --show-labels

All workload validation policies in this guide only apply to namespaces with this label:

Bash
kyverno.perlod.com/security-tier=production

This approach gives you control. You can test policies in policy-lab, then label a staging namespace, and only later label production application namespaces.

Step 6: Understand Audit Mode and Deny Mode

Before creating the first rule, understand the two policy actions:

  • Audit reports failures but lets the workload continue.

  • Deny rejects workloads that do not meet the rule.

Always start in Audit mode. This lets you find workloads that need changes without breaking deployments. The policy configuration looks like this in Audit mode:

YAML
validationActions:  Audit:    action: Audit

After testing and fixing workloads, change it to Deny mode:

YAML
validationActions:  Deny:    action: Deny

Kyverno can create PolicyReports that show which workloads passed or failed. ValidatingPolicy includes policy reporting support for these CEL-based validation checks.

Step 7: Block Privileged Containers

A privileged container has deep access to the host. It can bypass many normal container isolation controls and should not be used for common application workloads. Create the policy file:

Bash
nano ~/kyverno-1.19-production/policies/01-block-privileged.yaml
YAML
apiVersion: policies.kyverno.io/v1kind: ValidatingPolicymetadata:  name: block-privileged-containers  annotations:    policies.kyverno.io/title: Block privileged containers    policies.kyverno.io/category: Pod Security    policies.kyverno.io/severity: highspec:  validationActions:    Audit:      action: Audit  matchConstraints:    resourceRules:      - apiGroups:          - ""        apiVersions:          - v1        operations:          - CREATE          - UPDATE        resources:          - pods    namespaceSelector:      matchLabels:        kyverno.perlod.com/security-tier: production  validations:    - expression: >-        object.spec.containers.all(c,          !has(c.securityContext) ||          !has(c.securityContext.privileged) ||          c.securityContext.privileged == false        )      message: Privileged containers are not allowed.

Save and exit. Then, apply the policy and check that Kubernetes created it:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/01-block-privileged.yamlkubectl get validatingpolicy block-privileged-containers

The CEL expression checks every container in the Pod. It passes when privileged is missing or set to false. It fails only if a container sets:

YAML
securityContext:  privileged: true

Step 8: Test the Privileged Container Rule

You should test a policy with a failing workload before moving to the next policy. Create an unsafe test Pod:

Bash
nano ~/kyverno-1.19-production/workloads/privileged-pod.yaml
YAML
apiVersion: v1kind: Podmetadata:  name: privileged-test  namespace: policy-labspec:  containers:    - name: nginx      image: nginx:1.27      securityContext:        privileged: true

First, run a local policy test:

Bash
kyverno apply \  ~/kyverno-1.19-production/policies/01-block-privileged.yaml \  --resource ~/kyverno-1.19-production/workloads/privileged-pod.yaml

Because the policy is in Audit mode, Kubernetes will allow the Pod to be created, but Kyverno should report a violation:

Bash
kubectl apply -f ~/kyverno-1.19-production/workloads/privileged-pod.yaml

Check policy reports and delete the unsafe Pod after testing:

Bash
kubectl delete pod privileged-test -n policy-lab

Step 9: Require Non-Root Containers

Running containers as root gives an attacker more power if the application is compromised. A safer default is to run the application process with a non-root UID. Create the policy file:

Bash
nano ~/kyverno-1.19-production/policies/02-require-non-root.yaml
YAML
apiVersion: policies.kyverno.io/v1kind: ValidatingPolicymetadata:  name: require-non-root  annotations:    policies.kyverno.io/title: Require non-root containers    policies.kyverno.io/category: Pod Security    policies.kyverno.io/severity: highspec:  validationActions:    Audit:      action: Audit  matchConstraints:    resourceRules:      - apiGroups:          - ""        apiVersions:          - v1        operations:          - CREATE          - UPDATE        resources:          - pods    namespaceSelector:      matchLabels:        kyverno.perlod.com/security-tier: production  validations:    - expression: >-        object.spec.containers.all(c,          has(c.securityContext) &&          has(c.securityContext.runAsNonRoot) &&          c.securityContext.runAsNonRoot == true        )      message: Every container must set securityContext.runAsNonRoot to true.

Apply it with:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/02-require-non-root.yaml

This rule checks every regular container. It requires this field:

YAML
securityContext:  runAsNonRoot: true

A stronger workload configuration also sets a fixed non-root UID:

YAML
securityContext:  runAsNonRoot: true  runAsUser: 10001

The container image must support this user ID. If an application writes to protected paths or needs root-only ports, fix the image or its file permissions before enforcing this policy.

Step 10: Require CPU and Memory Requests and Limits

Resource settings protect the Kubernetes node and other workloads. Requests tell the scheduler what a Pod needs. Limits stop one container from using unlimited CPU or memory. Create the policy file:

Bash
nano ~/kyverno-1.19-production/policies/03-require-resources.yaml
YAML
apiVersion: policies.kyverno.io/v1kind: ValidatingPolicymetadata:  name: require-resource-requests-and-limits  annotations:    policies.kyverno.io/title: Require CPU and memory resources    policies.kyverno.io/category: Resource Management    policies.kyverno.io/severity: mediumspec:  validationActions:    Audit:      action: Audit  matchConstraints:    resourceRules:      - apiGroups:          - ""        apiVersions:          - v1        operations:          - CREATE          - UPDATE        resources:          - pods    namespaceSelector:      matchLabels:        kyverno.perlod.com/security-tier: production  validations:    - expression: >-        object.spec.containers.all(c,          has(c.resources) &&          has(c.resources.requests) &&          has(c.resources.requests.cpu) &&          has(c.resources.requests.memory) &&          has(c.resources.limits) &&          has(c.resources.limits.cpu) &&          has(c.resources.limits.memory)        )      message: Every container must set CPU and memory requests and limits.

Apply the policy:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/03-require-resources.yaml

A compliant container needs settings like these:

YAML
resources:  requests:    cpu: 100m    memory: 128Mi  limits:    cpu: 500m    memory: 256Mi

The policy checks whether values exist. It does not decide whether 100m or 128Mi is the correct size for your application. Set values based on real application usage, then monitor and adjust them.

Step 11: Restrict Host Network, PID, and IPC

Kubernetes Pods are normally isolated from the host. Settings such as hostNetwork, hostPID, and hostIPC weaken that separation. Create the policy file:

Bash
nano ~/kyverno-1.19-production/policies/04-restrict-host-namespaces.yaml
YAML
apiVersion: policies.kyverno.io/v1kind: ValidatingPolicymetadata:  name: restrict-host-namespaces  annotations:    policies.kyverno.io/title: Restrict host namespaces    policies.kyverno.io/category: Pod Security    policies.kyverno.io/severity: highspec:  validationActions:    Audit:      action: Audit  matchConstraints:    resourceRules:      - apiGroups:          - ""        apiVersions:          - v1        operations:          - CREATE          - UPDATE        resources:          - pods    namespaceSelector:      matchLabels:        kyverno.perlod.com/security-tier: production  validations:    - expression: >-        (!has(object.spec.hostNetwork) || object.spec.hostNetwork == false) &&        (!has(object.spec.hostPID) || object.spec.hostPID == false) &&        (!has(object.spec.hostIPC) || object.spec.hostIPC == false)      message: hostNetwork, hostPID, and hostIPC must be false or unset.

Then, apply it:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/04-restrict-host-namespaces.yaml

This policy allows the normal Kubernetes default, where these fields are unset or false. It rejects a Pod if it requests access such as:

YAML
hostNetwork: true

Host access may be needed by certain infrastructure DaemonSets, node monitoring agents, or network components. This is why these policies target only labeled application namespaces and do not apply to system namespaces.

Step 12: Allow Only Approved Image Registries

Image registry rules help reduce the risk of deploying unreviewed images from unknown sources. They also help ensure that applications use your company registry, approved GitHub Container Registry organization, or another trusted registry. Create the policy file:

Bash
nano ~/kyverno-1.19-production/policies/05-approved-registries.yaml
YAML
apiVersion: policies.kyverno.io/v1kind: ValidatingPolicymetadata:  name: require-approved-registries  annotations:    policies.kyverno.io/title: Require approved registries    policies.kyverno.io/category: Supply Chain Security    policies.kyverno.io/severity: highspec:  validationActions:    Audit:      action: Audit  matchConstraints:    resourceRules:      - apiGroups:          - ""        apiVersions:          - v1        operations:          - CREATE          - UPDATE        resources:          - pods    namespaceSelector:      matchLabels:        kyverno.perlod.com/security-tier: production  validations:    - expression: >-        object.spec.containers.all(c,          c.image.startsWith("registry.example.com/") ||          c.image.startsWith("ghcr.io/your-github-organization/")        )      message: Container images must come from an approved registry.

Apply the policy:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/05-approved-registries.yaml

This policy validates the image path, not the image signature. For stronger supply-chain control, use Kyverno ImageValidatingPolicy for signature and verification when your organization is ready.

Step 13: Generate Namespace Baseline Resources

New namespaces often need the same base controls. Creating them manually is slow and easy to forget. A Kyverno GeneratingPolicy can create Kubernetes resources automatically after a matching resource is created. Kyverno supports CEL-based generation for this purpose.

This example automatically creates:

  • A default-deny ingress NetworkPolicy.

  • A ResourceQuota to limit total resource use.

Create the file:

Bash
nano ~/kyverno-1.19-production/policies/06-generate-namespace-baseline.yaml
YAML
apiVersion: policies.kyverno.io/v1kind: GeneratingPolicymetadata:  name: generate-namespace-baseline  annotations:    policies.kyverno.io/title: Generate namespace baseline controls    policies.kyverno.io/category: Multi-Tenancy    policies.kyverno.io/severity: mediumspec:  evaluation:    synchronize:      enabled: true    generateExisting:      enabled: false    orphanDownstreamOnPolicyDelete:      enabled: true  matchConstraints:    resourceRules:      - apiGroups:          - ""        apiVersions:          - v1        operations:          - CREATE        resources:          - namespaces  matchConditions:    - name: production-namespaces-only      expression: >-        has(object.metadata.labels) &&        object.metadata.labels["kyverno.perlod.com/security-tier"] == "production"  generate:    - template:        value: |          apiVersion: networking.k8s.io/v1          kind: NetworkPolicy          metadata:            name: default-deny-ingress            namespace: (( object.metadata.name ))          spec:            podSelector: {}            policyTypes:              - Ingress          ---          apiVersion: v1          kind: ResourceQuota          metadata:            name: default-resource-quota            namespace: (( object.metadata.name ))          spec:            hard:              requests.cpu: "4"              requests.memory: 8Gi              limits.cpu: "8"              limits.memory: 16Gi              pods: "20"        interpolate: cel

Apply with:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/06-generate-namespace-baseline.yaml

The generated default-deny ingress policy blocks incoming traffic to all Pods in the namespace until you create separate allow rules. This is a safe default, but it can stop application traffic if you do not add rules for ingress controllers, monitoring, DNS-related needs, or internal services.

The quota values are example values. Adjust CPU, memory, and Pod limits based on the namespace purpose and your cluster capacity.

Step 14: Test Namespace Resource Generation

Create a fresh namespace with the required label:

Bash
kubectl create namespace app-production

Add the label:

Bash
kubectl label namespace app-production \  kyverno.perlod.com/security-tier=production

Check for the generated resources:

Bash
kubectl get networkpolicy -n app-productionkubectl get resourcequota -n app-production

Check the NetworkPolicy:

Bash
kubectl describe networkpolicy default-deny-ingress \  -n app-production

Check the ResourceQuota:

Bash
kubectl describe resourcequota default-resource-quota \  -n app-production

If the resources do not appear, check the generating policy and Kyverno logs:

Bash
kubectl describe generatingpolicy generate-namespace-baseline kubectl logs -n kyverno \  deployment/kyverno-background-controller \  --tail=200

Step 15: Create a Compliant Test Pod

A good test needs both failing and passing examples. This Pod includes the settings required by the policies in this guide. Create the file:

Bash
nano ~/kyverno-1.19-production/workloads/compliant-pod.yaml

Paste the YAML below. Replace the image registry name with one allowed by your registry policy:

YAML
apiVersion: v1kind: Podmetadata:  name: compliant-nginx  namespace: policy-labspec:  securityContext:    runAsNonRoot: true    runAsUser: 10001    runAsGroup: 10001    fsGroup: 10001  containers:    - name: nginx      image: registry.example.com/nginx:1.27      ports:        - containerPort: 8080      securityContext:        allowPrivilegeEscalation: false        privileged: false        runAsNonRoot: true        runAsUser: 10001        capabilities:          drop:            - ALL      resources:        requests:          cpu: 100m          memory: 128Mi        limits:          cpu: 500m          memory: 256Mi

Test it with the Kyverno CLI:

Bash
kyverno apply \  ~/kyverno-1.19-production/policies/ \  --resource ~/kyverno-1.19-production/workloads/compliant-pod.yaml

Then apply it to Kubernetes and check the Pod:

Bash
kubectl apply -f ~/kyverno-1.19-production/workloads/compliant-pod.yamlkubectl get pod compliant-nginx -n policy-lab

If the image does not exist in your registry, the Pod may fail to pull the image. That does not mean the policy failed. Check policy behavior separately from image availability:

Bash
kubectl describe pod compliant-nginx -n policy-lab

Remove the test Pod when finished:

Bash
kubectl delete pod compliant-nginx -n policy-lab

Step 16: Review Policy Reports

Audit mode is useful only if you review the results. Kyverno reports show whether a workload passed or failed each policy. List policy reports across all namespaces:

Bash
kubectl get policyreport -A

View reports from the test namespace:

Bash
kubectl get policyreport -n policy-lab -o yaml

Search for failures:

Bash
kubectl get policyreport -A -o yaml | grep -i -E "fail|error|violation"

If you need more detail, check the admission controller logs:

Bash
kubectl logs -n kyverno \  deployment/kyverno-admission-controller \  --tail=200

Do not change a policy to Deny mode until the Audit reports show that your expected workloads are ready.

Step 17: Change Policies from Audit to Deny

After the test results look good, enforce one policy at a time. Start with the privileged container rule because it is clear and usually has fewer exceptions. Open the policy file:

Bash
nano ~/kyverno-1.19-production/policies/01-block-privileged.yaml

Find this block and replace it with Deny:

YAML
validationActions:  Deny:    action: Deny

Save the file and apply it:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/01-block-privileged.yaml

Now try applying the unsafe privileged Pod again:

Bash
kubectl apply -f ~/kyverno-1.19-production/workloads/privileged-pod.yaml

Kubernetes should reject the request because the Pod sets privileged: true. After that works, enforce the remaining policies:

Before enforcing each file, change its validationActions block from Audit to Deny.

Bash
nano ~/kyverno-1.19-production/policies/02-require-non-root.yamlkubectl apply -f ~/kyverno-1.19-production/policies/02-require-non-root.yaml nano ~/kyverno-1.19-production/policies/03-require-resources.yamlkubectl apply -f ~/kyverno-1.19-production/policies/03-require-resources.yaml nano ~/kyverno-1.19-production/policies/04-restrict-host-namespaces.yamlkubectl apply -f ~/kyverno-1.19-production/policies/04-restrict-host-namespaces.yaml nano ~/kyverno-1.19-production/policies/05-approved-registries.yamlkubectl apply -f ~/kyverno-1.19-production/policies/05-approved-registries.yaml

Step 18: Roll Back Safely

If a policy blocks a workload unexpectedly, switch it back to Audit mode instead of deleting it immediately. Open the policy file:

Bash
nano ~/kyverno-1.19-production/policies/01-block-privileged.yaml

Change the action back to:

YAML
validationActions:  Audit:    action: Audit

Apply the file again:

Bash
kubectl apply -f ~/kyverno-1.19-production/policies/01-block-privileged.yaml

To delete one policy, you can use:

Bash
kubectl delete -f ~/kyverno-1.19-production/policies/01-block-privileged.yaml

To delete all policies:

Bash
kubectl delete -f ~/kyverno-1.19-production/policies/

To clean up the test namespaces, you can run:

Bash
kubectl delete namespace policy-labkubectl delete namespace app-production

Conclusion

Kyverno 1.19 gives Kubernetes teams a simple way to enforce workload security before containers start. Its newer CEL-based policy types let you write clear rules for privileged access, non-root users, resource limits, host namespace controls, trusted image sources, and generated namespace defaults.

Start every CEL policy in Audit mode, review results, fix workloads, then switch to Deny mode for one policy. This avoids surprise outages while building a stronger production security baseline.

For clusters that need stable performance and full infrastructure control, deploy Kubernetes on PerLod dedicated servers.

For current CEL syntax and policy fields, see the official Kyverno ValidatingPolicy documentation.