{"id":186,"date":"2026-02-18T06:52:19","date_gmt":"2026-02-18T06:52:19","guid":{"rendered":"https:\/\/yassinemoumen.com\/?p=186"},"modified":"2026-02-18T06:52:20","modified_gmt":"2026-02-18T06:52:20","slug":"stop-yelling-at-developers-let-kyverno-enforce-your-kubernetes-policies","status":"publish","type":"post","link":"https:\/\/yassinemoumen.com\/?p=186","title":{"rendered":"Stop Yelling at Developers, Let Kyverno Enforce Your Kubernetes Policies"},"content":{"rendered":"\n<p>Picture this: it&#8217;s 2 AM, your phone is buzzing, and your on-call engineer is staring at a Kubernetes cluster where someone just deployed a container running as root , with no resource limits and pulling from latest. The app is hammering the node, other workloads are starving, and the post-mortem is going to be a fun read.<br>You could yell at the developer. You could write a passive-aggressive Confluence page titled &#8220;Kubernetes Best Practices (Please Read This Time)&#8221;. Or, and hear me out, you could just make it impossible to deploy bad configurations in the first place.<br>That&#8217;s exactly what Kyverno does. And it does it without making you learn a new programming language.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Wild West of Kubernetes<\/h2>\n\n\n\n<p>Kubernetes is incredibly powerful. It&#8217;s also incredibly permissive by default.<br>Out of the box, nothing stops a developer from:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Running containers as root<\/li>\n\n\n\n<li>Skipping resource requests and limits<\/li>\n\n\n\n<li>Using latest as an image tag<\/li>\n\n\n\n<li>Exposing a service without the right labels<\/li>\n<\/ul>\n\n\n\n<p><br>Pulling images from an untrusted registry<br>Kubernetes trusts you. Maybe too much.<br>The classic answer to this problem has been OPA (Open Policy Agent) with Gatekeeper. And while OPA is powerful, it comes with a steep price: you have to write policies in Rego, a purpose-built query language that has humbled many senior engineers. It&#8217;s expressive, yes. Approachable? Not particularly.<br>Here&#8217;s a taste of Rego:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>package kubernetes.admission\n\ndeny&#91;msg] {\n  input.request.kind.kind == \"Pod\"\n  input.request.object.spec.containers&#91;_].securityContext.runAsRoot == true\n  msg := \"Containers must not run as root\"\n}<\/code><\/pre>\n\n\n\n<p>It works. But if your team isn&#8217;t already fluent in Rego, you&#8217;re adding a new language to your stack just to enforce some guardrails. That&#8217;s a hard sell.<br>Enter Kyverno.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>What is Kyverno?<\/strong><\/h2>\n\n\n\n<p>Kyverno (from the Greek \u03ba\u03c5\u03b2\u03b5\u03c1\u03bd\u03ce, &#8220;to govern&#8221;) is a policy engine built specifically for Kubernetes. The key difference from OPA\/Gatekeeper: policies are Kubernetes resources. No new language. No Rego. Just YAML , which your team is already writing.<br>Kyverno was donated to the CNCF in 2020 and is now a CNCF incubating project, meaning it&#8217;s production-grade and here to stay.<br>It works as an admission controller, a webhook that intercepts requests to the Kubernetes API before they&#8217;re persisted. Every time someone runs kubectl apply, Kyverno gets a look at the request and decides what to do with it.<br>Kyverno can do three things:<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-regular\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Mode<\/th><th class=\"has-text-align-left\" data-align=\"left\">What it does<\/th><\/tr><\/thead><tbody><tr><td class=\"has-text-align-left\" data-align=\"left\">Validate<\/td><td class=\"has-text-align-left\" data-align=\"left\">Block resources that don&#8217;t meet your rules<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">Mutate<\/td><td class=\"has-text-align-left\" data-align=\"left\">Automatically modify resources to make them compliant<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">Generate<\/td><td class=\"has-text-align-left\" data-align=\"left\">Create additional resources automatically when a new resource appears<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Let&#8217;s look at each one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Installing Kyverno<\/strong><\/h2>\n\n\n\n<p>First things first. You can install Kyverno with Helm:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>helm repo add kyverno https:\/\/kyverno.github.io\/kyverno\/\nhelm repo update\nhelm install kyverno kyverno\/kyverno -n kyverno --create-namespace<\/code><\/pre>\n\n\n\n<p>That&#8217;s it. Kyverno is now watching your cluster.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Validate: Stop the Bad Stuff<\/strong><\/h2>\n\n\n\n<p>Validation policies block requests that don&#8217;t meet your rules. If a resource violates a policy, the API server rejects it and returns an error to the userk, instantly, before anything gets deployed.<br><br><strong>Example 1: No Latest Tags<\/strong><br>Using latest as an image tag is an anti-pattern. It makes deployments non-deterministic, you never know exactly what&#8217;s running. Let&#8217;s enforce that:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apiVersion: kyverno.io\/v1\nkind: ClusterPolicy\nmetadata:\n  name: disallow-latest-tag\nspec:\n  validationFailureAction: Enforce\n  rules:\n    - name: require-image-tag\n      match:\n        any:\n          - resources:\n              kinds:\n                - Pod\n      validate:\n        message: \"Using 'latest' as an image tag is not allowed. Pin your image to a specific version.\"\n        pattern:\n          spec:\n            containers:\n              - image: \"!*:latest\"<\/code><\/pre>\n\n\n\n<p>Now if a developer tries to deploy with <code>image: nginx:latest<\/code>, they get a clear error message back. No more mystery failures from a silently updated image.<br><br><strong>Example 2: Require Resource Limits<\/strong><br>Resource limits prevent a single pod from consuming an entire node. Without them, one misbehaving app becomes everyone&#8217;s problem:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apiVersion: kyverno.io\/v1\nkind: ClusterPolicy\nmetadata:\n  name: require-resource-limits\nspec:\n  validationFailureAction: Enforce\n  rules:\n    - name: check-container-resources\n      match:\n        any:\n          - resources:\n              kinds:\n                - Pod\n      validate:\n        message: \"CPU and memory limits are required for all containers.\"\n        pattern:\n          spec:\n            containers:\n              - resources:\n                  limits:\n                    memory: \"?*\"\n                    cpu: \"?*\"<\/code><\/pre>\n\n\n\n<p>Note <code>validationFailureAction: Enforce<\/code>, this is what makes the policy actually block. If you set it to Audit instead, violations are logged but not blocked. Audit mode is great for rolling out a new policy without immediately breaking things.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Mutate: Fix It Automatically<\/strong><\/h2>\n\n\n\n<p>Sometimes you don&#8217;t want to block, you want to silently correct. Mutation policies automatically patch resources before they&#8217;re stored, so developers get compliant deployments without lifting a finger.<br><br><strong>Example 3: Add a Default Label<\/strong><br>Your team forgot to add the team label again. Instead of blocking the deploy and sending them back to fix it, just add it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apiVersion: kyverno.io\/v1\nkind: ClusterPolicy\nmetadata:\n  name: add-default-labels\nspec:\n  rules:\n    - name: add-team-label\n      match:\n        any:\n          - resources:\n              kinds:\n                - Deployment\n      mutate:\n        patchStrategicMerge:\n          metadata:\n            labels:\n              +(team): \"platform\"\n<\/code><\/pre>\n\n\n\n<p>The <code>+(team)<\/code> syntax means &#8220;add this label only if it doesn&#8217;t already exist&#8221;. If the developer set it themselves, Kyverno leaves it alone.<br><br><strong>Example 4: Set a Default Security Context<\/strong><br>Want all containers to run as non-root unless explicitly overridden? Mutate it in:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apiVersion: kyverno.io\/v1\nkind: ClusterPolicy\nmetadata:\n  name: add-default-securitycontext\nspec:\n  rules:\n    - name: set-run-as-non-root\n      match:\n        any:\n          - resources:\n              kinds:\n                - Pod\n      mutate:\n        patchStrategicMerge:\n          spec:\n            containers:\n              - (name): \"*\"\n                securityContext:\n                  +(runAsNonRoot): true\n<\/code><\/pre>\n\n\n\n<p>Security posture improved, zero developer friction.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Generate: Create Resources on Demand<\/strong><\/h2>\n\n\n\n<p>Generate policies create new resources automatically when a triggering resource appears. This is perfect for enforcing conventions at namespace creation time.<br><br><strong>Example 5: Auto-create a NetworkPolicy for Every New Namespace<\/strong><br>Every new namespace should have a default NetworkPolicy that denies all ingress traffic unless explicitly allowed. Doing this manually is error-prone. Let Kyverno handle it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apiVersion: kyverno.io\/v1\nkind: ClusterPolicy\nmetadata:\n  name: default-network-policy\nspec:\n  rules:\n    - name: generate-network-policy\n      match:\n        any:\n          - resources:\n              kinds:\n                - Namespace\n      generate:\n        apiVersion: networking.k8s.io\/v1\n        kind: NetworkPolicy\n        name: default-deny-ingress\n        namespace: \"{{request.object.metadata.name}}\"\n        data:\n          spec:\n            podSelector: {}\n            policyTypes:\n              - Ingress\n<\/code><\/pre>\n\n\n\n<p>Every time someone creates a namespace, they automatically get a deny-all NetworkPolicy. Security defaults baked in from day one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Wrapping Up<\/strong><\/h2>\n\n\n\n<p>Kyverno doesn&#8217;t replace good engineering judgment, but it does make good judgment the default. Instead of writing runbooks that nobody reads and sending passive-aggressive Slack messages about image tags, you bake your standards directly into the cluster.<br>The developer experience is better too. Clear, immediate error messages at deploy time are far more useful than a 2 AM incident where someone has to reverse-engineer what went wrong.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Validate<\/strong>\u00a0to block bad configurations<\/li>\n\n\n\n<li><strong>Mutate<\/strong>\u00a0to silently enforce defaults<\/li>\n\n\n\n<li><strong>Generate<\/strong>\u00a0to provision resources automatically<\/li>\n\n\n\n<li><strong>Audit mode<\/strong>\u00a0to roll out safely<\/li>\n<\/ul>\n\n\n\n<p>And since policies are just Kubernetes resources, your GitOps pipeline manages them like everything else. One less thing to context-switch on.<br>Stop yelling at developers. Kyverno will do it for you, politely, consistently, and at 1 AM instead of 2 AM.<\/p>\n\n\n\n<p><em>Want to go deeper? The <a href=\"https:\/\/kyverno.io\/docs\/\" target=\"_blank\" rel=\"noreferrer noopener\">Kyverno documentation<\/a> is genuinely good, and the <a href=\"https:\/\/kyverno.io\/policies\/\" target=\"_blank\" rel=\"noreferrer noopener\">Kyverno policies library<\/a> has ready-to-use policies for common scenarios.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Picture this: it&#8217;s 2 AM, your phone is buzzing, and your on-call engineer is staring at a Kubernetes cluster where someone just deployed a container running as root , with no resource limits and pulling from latest. The app is hammering the node, other workloads are starving, and the post-mortem is going to be a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[7],"tags":[9],"class_list":["post-186","post","type-post","status-publish","format-standard","hentry","category-programming","tag-devops","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=\/wp\/v2\/posts\/186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=186"}],"version-history":[{"count":6,"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=\/wp\/v2\/posts\/186\/revisions"}],"predecessor-version":[{"id":192,"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=\/wp\/v2\/posts\/186\/revisions\/192"}],"wp:attachment":[{"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/yassinemoumen.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}