<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kyverno on Zwindler's Reflection</title><link>https://blog.zwindler.fr/en/tags/kyverno/</link><description>Recent content in Kyverno on Zwindler's Reflection</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Licensed under CC BY-SA 4.0</copyright><lastBuildDate>Tue, 19 May 2026 12:00:00 +0200</lastBuildDate><atom:link href="https://blog.zwindler.fr/en/tags/kyverno/index.xml" rel="self" type="application/rss+xml"/><item><title>nodes/proxy GET: One Kubernetes permission too many</title><link>https://blog.zwindler.fr/en/2026/05/19/nodes/proxy-get-one-kubernetes-permission-too-many/</link><pubDate>Tue, 19 May 2026 12:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/05/19/nodes/proxy-get-one-kubernetes-permission-too-many/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/05/node_proxy_RCE.webp" alt="Featured image of post nodes/proxy GET: One Kubernetes permission too many" /&gt;&lt;h2 id="tldr"&gt;TL;DR
&lt;/h2&gt;&lt;p&gt;This is a bit old now, but I still wanted to share a quick write-up on the topic.&lt;/p&gt;
&lt;p&gt;Back in January, a cybersecurity researcher reported a Kubernetes flaw that generated quite a buzz. It had been a while since we had a Kube vulnerability that got people talking this much, at least from Denis&amp;rsquo;s memory (yes, I talk about myself in the third person).&lt;/p&gt;
&lt;p&gt;Honestly, it&amp;rsquo;s pretty wild: the &lt;code&gt;nodes/proxy GET&lt;/code&gt; RBAC permission allows any &lt;strong&gt;ServiceAccount&lt;/strong&gt; to execute code inside &lt;strong&gt;any Pod in the cluster&lt;/strong&gt;, without leaving a single trace in the audit logs. That&amp;rsquo;s unfortunate, especially when you have ServiceAccounts named &lt;code&gt;rook-ceph-system&lt;/code&gt; that also happen to have read access to all Secrets in the cluster.&lt;/p&gt;
&lt;p&gt;This article details the issue, how to check if you are vulnerable, the fixes to apply, and the preventive measures you can put in place if you can&amp;rsquo;t patch right away.&lt;/p&gt;
&lt;h2 id="the-problem-websocket--kubelet--un-audited-exec"&gt;The problem: WebSocket + Kubelet = un-audited exec
&lt;/h2&gt;&lt;p&gt;The vulnerability was documented by Graham Helton in &lt;a class="link" href="https://grahamhelton.com/blog/nodes-proxy-rce" target="_blank" rel="noopener"
&gt;this article&lt;/a&gt;. Here is how it works.&lt;/p&gt;
&lt;p&gt;The Kubernetes API exposes a &lt;code&gt;nodes/proxy&lt;/code&gt; subresource that proxies HTTP requests to each node&amp;rsquo;s Kubelet. The Kubelet itself exposes an API on port 10250, specifically the &lt;code&gt;/exec&lt;/code&gt; endpoint which allows executing commands inside a container.&lt;/p&gt;
&lt;p&gt;The issue comes from how the Kubelet handles authorizations for WebSocket connections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;kubectl exec&lt;/code&gt; uses a WebSocket connection, whose handshake is an HTTP &lt;code&gt;GET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The Kubelet maps this initial &lt;code&gt;GET&lt;/code&gt; to the &lt;code&gt;get&lt;/code&gt; RBAC verb&lt;/li&gt;
&lt;li&gt;It checks &lt;code&gt;nodes/proxy GET&lt;/code&gt;, then authorizes the operation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No secondary check&lt;/strong&gt; is performed for the &lt;code&gt;CREATE&lt;/code&gt; verb normally required for &lt;code&gt;/exec&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Result: any ServiceAccount with &lt;code&gt;nodes/proxy GET&lt;/code&gt; can execute commands in any Pod in the cluster, &lt;strong&gt;including system Pods&lt;/strong&gt; (&lt;code&gt;etcd&lt;/code&gt;, &lt;code&gt;kube-apiserver&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Exploitation via websocat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;websocat --insecure &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --header &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --protocol &lt;span class="s2"&gt;&amp;#34;v4.channel.k8s.io&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;wss://&lt;/span&gt;&lt;span class="nv"&gt;$NODE_IP&lt;/span&gt;&lt;span class="s2"&gt;:10250/exec/default/nginx/nginx?output=1&amp;amp;error=1&amp;amp;command=id&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s not all. &lt;strong&gt;Commands executed via this method do not generate any Kubernetes audit logs&lt;/strong&gt; (well, assuming you even collect them 🙈). The access goes directly through the Kubelet, which does not report events back to the API server.&lt;/p&gt;
&lt;p&gt;The official Kubernetes status on this: &lt;strong&gt;Won&amp;rsquo;t Fix&lt;/strong&gt;. It is a &amp;ldquo;design behavior&amp;rdquo; (note the quotes), addressed via a feature gate (&lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2862-fine-grained-kubelet-authz/README.md" target="_blank" rel="noopener"
&gt;KEP-2862&lt;/a&gt;, see below).&lt;/p&gt;
&lt;p&gt;Ouch.&lt;/p&gt;
&lt;h2 id="the-audit-vulnerable-serviceaccounts-on-our-clusters"&gt;The Audit: Vulnerable ServiceAccounts on our clusters
&lt;/h2&gt;&lt;p&gt;In January 2026, following the publication of Graham Helton&amp;rsquo;s article, a lot of people had to urgently audit their clusters. You can either manually audit all your Roles / ClusterRoles, or use a &lt;a class="link" href="https://gist.github.com/grahamhelton/f5c8ce265161990b0847ac05a74e466a" target="_blank" rel="noopener"
&gt;detection script&lt;/a&gt; provided by the researcher.&lt;/p&gt;
&lt;p&gt;As an example, here are three relatively common components that make great candidates for a juicy privilege escalation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;ClusterRole&lt;/th&gt;
&lt;th&gt;ServiceAccounts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTelemetry Collector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;otel-otelcol-k8sobjects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opentelemetry-collector-daemonset-collector&lt;/code&gt;, &lt;code&gt;opentelemetry-collector-deployment-collector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTelemetry Operator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;otel-operator-resources&lt;/code&gt; / &lt;code&gt;opentelemetry-operator-manager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opentelemetry-operator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rook-Ceph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rook-ceph-global&lt;/code&gt;, &lt;code&gt;rook-ceph-mgr-cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rook-ceph-system&lt;/code&gt;, &lt;code&gt;rook-ceph-mgr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Note: there are many more. Graham Helton added an &amp;ldquo;Appendix: Affected Helm Charts&amp;rdquo; section at the end of his article, referencing AT LEAST 69 affected Helm charts according to him.&lt;/p&gt;
&lt;h3 id="the-critical-case-rook-ceph-system"&gt;The critical case: rook-ceph-system
&lt;/h3&gt;&lt;p&gt;In the official chart, the &lt;code&gt;rook-ceph-system&lt;/code&gt; ServiceAccount combined two particularly dangerous permissions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nodes/proxy GET&lt;/code&gt;&lt;/strong&gt; - the RCE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;secrets GET/LIST/WATCH&lt;/code&gt;&lt;/strong&gt; across the &lt;strong&gt;entire cluster&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Accessible secrets can include LUKS keys for volume encryption, Ceph admin keyrings, dashboard passwords&amp;hellip; this kind of access makes it a prime target for an attacker.&lt;/p&gt;
&lt;p&gt;The attack scenario: a compromise of the &lt;code&gt;rook-ceph-operator&lt;/code&gt; Pod (via CVE, supply chain, or a malicious image) would allow reading all Ceph secrets, and then executing code in any Pod (including &lt;code&gt;etcd&lt;/code&gt;), leading to a full compromise of the cluster and encrypted data.&lt;/p&gt;
&lt;p&gt;To manually check if a ServiceAccount is vulnerable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl auth can-i get nodes --subresource&lt;span class="o"&gt;=&lt;/span&gt;proxy &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --as&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:&amp;lt;namespace&amp;gt;:&amp;lt;serviceaccount&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="examples-of-fixes-to-apply"&gt;Examples of fixes to apply
&lt;/h2&gt;&lt;h3 id="rook-ceph-upstream-fix"&gt;Rook-Ceph: Upstream fix
&lt;/h3&gt;&lt;p&gt;For Rook-Ceph, the fix came from upstream: PR &lt;a class="link" href="https://github.com/rook/rook/pull/16979" target="_blank" rel="noopener"
&gt;rook/rook#16979&lt;/a&gt; removed &lt;code&gt;nodes/proxy&lt;/code&gt; from ClusterRoles. This fix is included in Rook v1.19.1, so updating the affected clusters was enough.&lt;/p&gt;
&lt;p&gt;After updating, checking across all clusters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl get clusterroles rook-ceph-global -o yaml &lt;span class="p"&gt;|&lt;/span&gt; grep -A3 nodes/proxy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -&amp;gt; nothing&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="otel--otel-operator"&gt;OTel / OTel operator
&lt;/h3&gt;&lt;p&gt;For OpenTelemetry, the situation is potentially more complex. If you are using &lt;code&gt;otel-operator&lt;/code&gt; and &lt;strong&gt;OtelCollector&lt;/strong&gt; Custom Resources, you likely have to manage your own RBAC manifests &lt;strong&gt;yourself&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Having had to do it myself, it&amp;rsquo;s quite painful. Depending on your collector type and the receivers you enabled, you need to cross-reference multiple documents on the official OTel and otel-operator websites.&lt;/p&gt;
&lt;p&gt;Upstream merged a conditional approach in &lt;a class="link" href="https://github.com/open-telemetry/opentelemetry-helm-charts/pull/2083" target="_blank" rel="noopener"
&gt;open-telemetry/opentelemetry-helm-charts#2083&lt;/a&gt; based on the Kubernetes version:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Upstream approach (opentelemetry-helm-charts#2083)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- &lt;span class="l"&gt;if semverCompare &amp;#34;&amp;gt;=1.33-0&amp;#34; .Capabilities.KubeVersion.Version }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/pods&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- &lt;span class="l"&gt;else }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- &lt;span class="l"&gt;end }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here again, if all your clusters are up to date, you can simply replace &lt;code&gt;nodes/proxy&lt;/code&gt; with &lt;code&gt;nodes/pods&lt;/code&gt; directly (without the condition).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;otel-collector-crb.yaml&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/proxy &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# &amp;lt;- RCE risk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/spec&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/stats&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# After&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# nodes/pods replaces nodes/proxy (RCE risk, see [https://grahamhelton.com/blog/nodes-proxy-rce](https://grahamhelton.com/blog/nodes-proxy-rce))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Requires K8s &amp;gt;= 1.33 (KEP-2862 fine-grained kubelet authz)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/pods&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/spec&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/stats&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;otel-operator-rbac.yaml&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/proxy &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# &amp;lt;- RCE risk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# After&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# nodes/pods replaces nodes/proxy (RCE risk, see [https://grahamhelton.com/blog/nodes-proxy-rce](https://grahamhelton.com/blog/nodes-proxy-rce))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Requires K8s &amp;gt;= 1.33 (KEP-2862 fine-grained kubelet authz)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/pods&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="preventive-measures"&gt;Preventive Measures
&lt;/h2&gt;&lt;h3 id="kep-2862-fine-grained-kubelet-api-authorization"&gt;KEP-2862: Fine-Grained Kubelet API Authorization
&lt;/h3&gt;&lt;p&gt;As teased earlier, the real long-term solution is &lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2862-fine-grained-kubelet-authz/README.md" target="_blank" rel="noopener"
&gt;KEP-2862&lt;/a&gt; (Fine-Grained Kubelet API Authorization). It introduces granular subresources (&lt;code&gt;nodes/pods&lt;/code&gt;, &lt;code&gt;nodes/metrics&lt;/code&gt;, &lt;code&gt;nodes/stats&lt;/code&gt;, &lt;code&gt;nodes/log&lt;/code&gt;, etc.) allowing precise access without using &lt;code&gt;nodes/proxy&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;K8s Version&lt;/th&gt;
&lt;th&gt;KEP-2862 Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;Alpha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Beta, enabled by default&lt;/strong&gt; - &lt;code&gt;nodes/proxy GET&lt;/code&gt; no longer grants access to &lt;code&gt;/exec&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.36&lt;/td&gt;
&lt;td&gt;GA (locked to enabled)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;But this requires going through EVERY chart in use and checking all deployed manifests now &lt;strong&gt;and in the future&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="ciliumnetworkpolicy-blocking-the-kubelet-port"&gt;CiliumNetworkPolicy: Blocking the Kubelet port
&lt;/h3&gt;&lt;p&gt;While waiting for the K8s upgrade, or as defense-in-depth, you can block access to port 10250 from the affected pods using NetworkPolicies (or CiliumNetworkPolicy if you use Cilium as your CNI plugin).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: This only applies to components that do &lt;em&gt;not&lt;/em&gt; need to access the Kubelet. The OTel collector potentially needs it to gather Kubelet metrics. In that case, you have no choice but to fix the RBAC.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cilium.io/v2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;CiliumNetworkPolicy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;deny-kubelet-api-access&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;namespace&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endpointSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;matchLabels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;app-label&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;egressDeny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;toEntities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;remote-node&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;toPorts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10250&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;TCP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="kyverno-blocking-the-creation-of-new-roles-with-nodesproxy"&gt;Kyverno: Blocking the creation of new Roles with nodes/proxy
&lt;/h3&gt;&lt;p&gt;To prevent any regression (remember, we need to protect ourselves &lt;strong&gt;in the future&lt;/strong&gt;), we can add a Kyverno &lt;code&gt;ClusterPolicy&lt;/code&gt; that rejects the creation or modification of a &lt;code&gt;ClusterRole&lt;/code&gt; or &lt;code&gt;Role&lt;/code&gt; containing &lt;code&gt;nodes/proxy&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Luckily, there are ready-to-use examples on the official Kyverno website:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://kyverno.io/policies/other-vpol/restrict-clusterrole-nodesproxy/restrict-clusterrole-nodesproxy/" target="_blank" rel="noopener"
&gt;validating variant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kyverno.io/policies/other-cel/restrict-clusterrole-nodesproxy/restrict-clusterrole-nodesproxy/" target="_blank" rel="noopener"
&gt;CEL variant&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note : &lt;a class="link" href="https://github.com/kyverno/policies/issues/1492" target="_blank" rel="noopener"
&gt;there probably is a hole in the official Kyverno policy, I opened an issue&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;kyverno.io/v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ClusterPolicy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;restrict-nodes-proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;validationFailureAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Audit &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# switch to Enforce after validation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;deny-nodes-proxy-in-clusterroles&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;kinds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;ClusterRole&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;Role&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;system:kubelet-api-admin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# built-in K8s, unmodifiable&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; nodes/proxy grants RCE capability via Kubelet WebSocket exec.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Use nodes/pods (requires K8s &amp;gt;= 1.33, KEP-2862) instead.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;nodes/proxy&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;AnyIn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ request.object.rules[].resources[] }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Deployment is done in two stages: first in &lt;code&gt;Audit&lt;/code&gt; mode to ensure there are no remaining vulnerable manifests (which you should fix &lt;strong&gt;before&lt;/strong&gt; blocking), then in &lt;code&gt;Enforce&lt;/code&gt; mode to actually block them.&lt;/p&gt;
&lt;h3 id="monitoring-the-audit-log"&gt;Monitoring the Audit Log
&lt;/h3&gt;&lt;p&gt;Even if commands executed &lt;em&gt;via&lt;/em&gt; the Kubelet leave no trace, we can monitor &lt;strong&gt;SubjectAccessReviews&lt;/strong&gt; to detect attempts at enumerating &lt;code&gt;nodes/proxy&lt;/code&gt; permissions.&lt;/p&gt;
&lt;p&gt;The configuration in the Kubernetes audit policy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# audit-policy.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Request&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;create&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;authorization.k8s.io&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;subjectaccessreviews&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then a Prometheus/Alertmanager alert on SARs related to &lt;code&gt;nodes/proxy&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-promql" data-lang="promql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Detect SARs targeting nodes/proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;increase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;apiserver_audit_event_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;verb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;&lt;/span&gt;&lt;span class="s"&gt;subjectaccessreviews&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="s"&gt;5m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="references"&gt;References
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://grahamhelton.com/blog/nodes-proxy-rce" target="_blank" rel="noopener"
&gt;Kubernetes RCE via nodes/proxy GET&lt;/a&gt; - Graham Helton&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://gist.github.com/grahamhelton/f5c8ce265161990b0847ac05a74e466a" target="_blank" rel="noopener"
&gt;Detection Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2862-fine-grained-kubelet-authz/README.md" target="_blank" rel="noopener"
&gt;KEP-2862 Fine-Grained Kubelet API Authorization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/concepts/security/rbac-good-practices/#access-to-proxy-subresource-of-nodes" target="_blank" rel="noopener"
&gt;Kubernetes RBAC Good Practices - nodes/proxy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/rook/rook/pull/16979" target="_blank" rel="noopener"
&gt;rook/rook#16979&lt;/a&gt; - Fix upstream Rook-Ceph&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/open-telemetry/opentelemetry-helm-charts/pull/2083" target="_blank" rel="noopener"
&gt;open-telemetry/opentelemetry-helm-charts#2083&lt;/a&gt; - Fix upstream OTel&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Kyverno killed my API Server. Again.</title><link>https://blog.zwindler.fr/en/2026/02/26/kyverno-killed-my-api-server.-again./</link><pubDate>Thu, 26 Feb 2026 08:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/02/26/kyverno-killed-my-api-server.-again./</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/0_days_without_kyverno.webp" alt="Featured image of post Kyverno killed my API Server. Again." /&gt;&lt;h2 id="the-revenge-strikes-back"&gt;The revenge strikes back
&lt;/h2&gt;&lt;p&gt;You may have read &lt;a class="link" href="https://blog.zwindler.fr/en/2023/11/30/kubernetes-error-etcdserver-mvcc-database-space-exceeded/" &gt;my previous article about etcd 3 years ago&lt;/a&gt;. If you remember correctly, the crash was etcd, but the real culprit was Kyverno. I love Kyverno. It&amp;rsquo;s really a piece of software I&amp;rsquo;m very fond of. Mainly because it&amp;rsquo;s incredibly powerful. I even wrote &lt;a class="link" href="https://blog.zwindler.fr/2022/08/01/vos-politiques-de-conformite-sur-kubernetes-avec-kyverno/" &gt;an introductory article&lt;/a&gt; and &lt;a class="link" href="https://blog.zwindler.fr/2022/09/05/vos-politiques-de-conformite-sur-kubernetes-avec-kyverno-part2/" &gt;a second one that goes deeper&lt;/a&gt; on the topic (both in french, though).&lt;/p&gt;
&lt;p&gt;But the sheer number of incidents and weird side effects it causes. Mamamia&amp;hellip; This isn&amp;rsquo;t the first incident of the year I&amp;rsquo;ve had with Kyverno (yes, we&amp;rsquo;re in February) but since this one is entertaining, I&amp;rsquo;m sharing it with you.&lt;/p&gt;
&lt;p&gt;During a routine maintenance operation to upgrade a Kubernetes cluster to version &lt;strong&gt;1.34&lt;/strong&gt; (from 1.32), we ended up facing the dreaded scenario for any kube admin: a completely unreachable API Server after restarting the Control Plane nodes.&lt;/p&gt;
&lt;p&gt;What initially looked like a typical network error turned out to be a subtle &lt;strong&gt;deadlock&lt;/strong&gt; between new native Kubernetes networking features and our dear Kyverno 😘.&lt;/p&gt;
&lt;p&gt;Spoiler: it wasn&amp;rsquo;t a network issue. It&amp;rsquo;s never a network issue. Well, sometimes it is. But not this time.&lt;/p&gt;
&lt;h2 id="the-upgrade-that-starts-well"&gt;The upgrade that starts well
&lt;/h2&gt;&lt;p&gt;Alright, a Kubernetes upgrade has become pretty routine at this point. We do it regularly, we have our procedures, we&amp;rsquo;re pros (I swear). We jump from 1.32 to 1.34 in a single commit, skipping the hop through 1.33.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;YOLO.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the technical context I&amp;rsquo;m talking about, everything is managed as code. From machine provisioning all the way to Talos deployment, including MachineConfigs (the CustomResources to modify&amp;hellip; well, the machine).&lt;/p&gt;
&lt;p&gt;For more details, see &lt;a class="link" href="https://docs.siderolabs.com/talos/v1.12/reference/configuration/v1alpha1/config#machineconfig" target="_blank" rel="noopener"
&gt;the Talos documentation on Machine Configs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The first cluster we test has only one control plane (don&amp;rsquo;t ask me why, it probably wouldn&amp;rsquo;t have changed anything). Talos restarts the API Server with the new version and then&amp;hellip; nothing.&lt;/p&gt;
&lt;p&gt;The &amp;ldquo;weird&amp;rdquo; API Server logs (technical term) speak for themselves:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;I0224 15:32:50.979280 1 default_servicecidr_controller.go:166] Creating default ServiceCIDR with CIDRs: [10.1.0.0/20]
W0224 15:32:50.984784 1 dispatcher.go:225] rejected by webhook &amp;#34;validate.kyverno.svc-fail&amp;#34;:
admission webhook &amp;#34;validate.kyverno.svc-fail&amp;#34; denied the request:
Get &amp;#34;https://10.1.0.1:443/api&amp;#34;: dial tcp 10.1.0.1:443: connect: operation not permitted
I0224 15:32:50.985342 1 event.go:389] &amp;#34;Event occurred&amp;#34; kind=&amp;#34;ServiceCIDR&amp;#34;
apiVersion=&amp;#34;networking.k8s.io/v1&amp;#34; type=&amp;#34;Warning&amp;#34;
reason=&amp;#34;KubernetesDefaultServiceCIDRError&amp;#34;
message=&amp;#34;The default ServiceCIDR can not be created&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;😬😬😬&lt;/p&gt;
&lt;h2 id="the-root-cause-a-magnificent-vicious-circle"&gt;The root cause: a magnificent vicious circle
&lt;/h2&gt;&lt;p&gt;After investigation, we discovered that the incident was the result of a collision between a Kubernetes core evolution and our Kyverno configuration. A textbook deadlock case.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s break down the mechanism:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The new &lt;code&gt;ServiceCIDR&lt;/code&gt; Kind&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In recent versions (v1.33+), Kubernetes migrates service IP range management to dedicated objects named &lt;code&gt;ServiceCIDR&lt;/code&gt;. On the first boot after the upgrade, the API Server automatically tries to create the default object (e.g., &lt;code&gt;10.1.0.0/20&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;For the curious, &lt;a class="link" href="https://github.com/kubernetes/enhancements/issues/1880" target="_blank" rel="noopener"
&gt;KEP-1880&lt;/a&gt; and the &lt;a class="link" href="https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/service-cidr-v1/" target="_blank" rel="noopener"
&gt;official ServiceCIDR documentation&lt;/a&gt; detail this evolution.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s new, it&amp;rsquo;s clean, it&amp;rsquo;s well designed. Except that&amp;hellip;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Interception by the Kyverno Webhook&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kyverno, configured with &lt;code&gt;failurePolicy: Fail&lt;/code&gt; (because we&amp;rsquo;re serious people who don&amp;rsquo;t let just anything through in prod), is set up to intercept resource creations to validate them, and &lt;strong&gt;fail the call if Kyverno doesn&amp;rsquo;t respond&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Including the &lt;code&gt;ServiceCIDR&lt;/code&gt; freshly created by the API Server itself.&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Deadlock&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And this is where it gets beautiful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The API Server pauses the &lt;code&gt;ServiceCIDR&lt;/code&gt; creation waiting for Kyverno&amp;rsquo;s &amp;ldquo;OK&amp;rdquo;&lt;/li&gt;
&lt;li&gt;To contact the Kyverno service, the API Server needs to route the request through the Kubernetes service IP (typically &lt;code&gt;10.1.0.1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;But&lt;/strong&gt; the network layer (service routing) can&amp;rsquo;t initialize until the &lt;code&gt;ServiceCIDR&lt;/code&gt; object is validated and created&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s the chicken and the egg, &amp;ldquo;I locked my keys inside the car&amp;rdquo; edition.&lt;/p&gt;
&lt;p&gt;PTSD. Yes, that actually happened to me. In the desert. With no cell service.&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Profit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The API Server times out or returns a &lt;code&gt;connect: operation not permitted&lt;/code&gt; error when trying to reach the webhook, blocking its own initialization. CrashLoopBackOff on the API Server. :D&lt;/p&gt;
&lt;h2 id="breaking-out-of-the-deadlock"&gt;Breaking out of the deadlock
&lt;/h2&gt;&lt;p&gt;To escape this deadlock, you need to temporarily bypass the admission layer. Easy, right?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &amp;ldquo;usual&amp;rdquo; workaround: useless&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Deadlocks with Kyverno, we&amp;rsquo;re used to them at this point. Normally, since &lt;code&gt;kube-system&lt;/code&gt; is ignored, you can simply connect with a break-glass kubeconfig (we normally use OIDC) that has the cluster-admin cluster role and delete the Kyverno validating webhooks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl delete validatingwebhookconfiguration kyverno-resource-validating-webhook-cfg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Except here&lt;/strong&gt;, the API Server won&amp;rsquo;t even start. My &lt;code&gt;kubectl&lt;/code&gt; isn&amp;rsquo;t going to work, obviously!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The real workaround: disable webhooks at boot&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The solution we chose was to modify the API Server configuration to temporarily disable validation webhooks at startup. My esteemed colleague Maxime hot-edited the machine config (using break-glass &lt;code&gt;talosctl&lt;/code&gt; access) &lt;a class="link" href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#how-do-i-turn-off-an-admission-controller" target="_blank" rel="noopener"
&gt;to add the following flag&lt;/a&gt; directly in the API server&amp;rsquo;s &lt;code&gt;extraArgs&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;--disable-admission-plugins=ValidatingAdmissionWebhook
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For those unfamiliar with admission control in Kubernetes, just know that there&amp;rsquo;s a list of &amp;ldquo;default&amp;rdquo; plugins but everything can be toggled off. I might do a deep dive on Kubernetes admission control someday, it&amp;rsquo;s fascinating ;).&lt;/p&gt;
&lt;p&gt;With this flag, the API Server can finally create its &lt;code&gt;ServiceCIDR&lt;/code&gt; objects without asking anyone for permission (completely bypassing all validation mechanisms that Kyverno or similar tools &lt;em&gt;enforce&lt;/em&gt;), the network initializes, Kyverno starts, and then you can remove the flag and restart cleanly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &amp;ldquo;funny&amp;rdquo; option we didn&amp;rsquo;t try&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Personally, I thought it would be hilarious to go directly into the etcd database and delete the webhook key causing the issue (also through &lt;code&gt;talosctl&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Example via etcdctl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;etcdctl del /registry/admissionregistration.k8s.io/validatingwebhookconfigurations/kyverno-resource-validating-webhook-cfg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My colleagues were less enthusiastic: &amp;ldquo;Yeah but you know, if we break etcd it&amp;rsquo;s gonna be painful&amp;rdquo;. We played it safe with the flag. I&amp;rsquo;m deeply disappointed we didn&amp;rsquo;t try 😂.&lt;/p&gt;
&lt;h2 id="the-permanent-fix-matchconditions"&gt;The permanent fix: MatchConditions
&lt;/h2&gt;&lt;p&gt;OK, now that the cluster is back up, how do we make sure this doesn&amp;rsquo;t happen again on the next upgrade?&lt;/p&gt;
&lt;p&gt;The clean solution is to use &lt;code&gt;matchConditions&lt;/code&gt; (introduced in Kubernetes 1.27) on the &lt;code&gt;ValidatingWebhookConfiguration&lt;/code&gt;. This allows you to exclude critical network bootstrap resources &lt;strong&gt;before&lt;/strong&gt; the request even attempts to leave the API Server toward the Kyverno pod.&lt;/p&gt;
&lt;p&gt;See &lt;a class="link" href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchconditions" target="_blank" rel="noopener"
&gt;the official documentation on matchConditions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We were already using this option to throttle the sometimes excessive Kyverno traffic (if you manage Kyverno, you know what I&amp;rsquo;m talking about) on a number of events (we&amp;rsquo;d overwhelm the API server or Kyverno, in CPU or RAM, depending on the case). We just had to add exclusions for the new types:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Exclude network bootstrap resources to prevent the deadlock&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;matchConditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;exclude-ServiceCIDR&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!(request.kind.kind == &amp;#34;ServiceCIDR&amp;#34;)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;exclude-IPAddress&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!(request.kind.kind == &amp;#34;IPAddress&amp;#34;)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this, when the API Server creates a &lt;code&gt;ServiceCIDR&lt;/code&gt; at boot, the request no longer goes through the Kyverno webhook. No circular dependency, no deadlock, everyone&amp;rsquo;s happy.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;As the current French president would say about something that was painfully predictable:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;who could have predicted this?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK fine, all we had to do was read the Kubernetes 1.33 release notes. That said, we have a staging cluster, that&amp;rsquo;s what it&amp;rsquo;s for. We broke staging, no big deal.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/bigdeal.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Context: this is the &amp;ldquo;Big deal&amp;rdquo; TV Game Mascot&lt;/p&gt;
&lt;p&gt;Maybe we&amp;rsquo;ll actually read them next time?&lt;/p&gt;</description></item><item><title>Kubernetes Compliance Policies with Kyverno - part 2</title><link>https://blog.zwindler.fr/en/2022/09/05/kubernetes-compliance-policies-with-kyverno-part-2/</link><pubDate>Mon, 05 Sep 2022 06:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2022/09/05/kubernetes-compliance-policies-with-kyverno-part-2/</guid><description>&lt;img src="https://blog.zwindler.fr/2022/07/Kyverno_Horizontal.webp" alt="Featured image of post Kubernetes Compliance Policies with Kyverno - part 2" /&gt;&lt;h2 id="part-1-recap"&gt;Part 1 recap
&lt;/h2&gt;&lt;p&gt;In the &lt;a class="link" href="https://blog.zwindler.fr/2022/08/01/vos-politiques-de-conformite-sur-kubernetes-avec-kyverno" target="_blank" rel="noopener"
&gt;previous article&lt;/a&gt;, I introduced &lt;a class="link" href="https://kyverno.io/" target="_blank" rel="noopener"
&gt;Kyverno&lt;/a&gt;, a &amp;ldquo;cloud native&amp;rdquo; tool for managing compliance policies on Kubernetes.&lt;/p&gt;
&lt;p&gt;To illustrate the tool&amp;rsquo;s capabilities, I used an example: preventing two Ingresses from using the same entry URL. I had done the same exercise with OPA in an earlier article (&lt;a class="link" href="https://blog.zwindler.fr/2020/07/20/vos-politiques-de-conformite-sur-kubernetes-avec-opa-et-gatekeeper/" &gt;Open Policy Agent (OPA) and its Kubernetes counterpart, Gatekeeper&lt;/a&gt;) and compared the two syntaxes (Kyverno vs rego) at the end.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/07/rego_vs_kyverno.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;I have a clear preference for Kyverno&amp;rsquo;s syntax, even though OPA has been around longer and isn&amp;rsquo;t limited to Kubernetes (Kyverno is Kube-only).&lt;/p&gt;
&lt;h2 id="going-a-bit-further"&gt;Going a bit further
&lt;/h2&gt;&lt;p&gt;Beyond my example, I mentioned that Kyverno provides a very large number of pre-written policies (191 at the time of writing).&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://kyverno.io/policies/" target="_blank" rel="noopener"
&gt;kyverno.io/policies&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By default, most available policies are set to &amp;ldquo;audit&amp;rdquo; mode initially. Don&amp;rsquo;t forget to switch them to &amp;ldquo;enforce&amp;rdquo; mode once your cluster is mature enough.&lt;/p&gt;
&lt;p&gt;Obviously, when you&amp;rsquo;re just getting started, you don&amp;rsquo;t really know where to begin. The simplest approach is to deploy the &lt;code&gt;kyverno/kyverno-policies&lt;/code&gt; Helm chart, which contains the Kyverno equivalent of the &amp;ldquo;Pod Security Standards&amp;rdquo; (PSPs being officially deprecated since 1.21 I think, and no longer usable in 1.25).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm install kyverno-policies kyverno/kyverno-policies -n kyverno
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Thank you &lt;span class="k"&gt;for&lt;/span&gt; installing kyverno-policies v2.5.2 😀
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;We have installed the &lt;span class="s2"&gt;&amp;#34;baseline&amp;#34;&lt;/span&gt; profile of Pod Security Standards and &lt;span class="nb"&gt;set&lt;/span&gt; them in audit mode.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Visit https://kyverno.io/policies/ to find more sample policies.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s what was deployed (the last one was already there from the previous article):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl get clusterpolicy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NAME BACKGROUND ACTION READY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-capabilities &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-host-namespaces &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-host-path &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-host-ports &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-host-process &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-privileged-containers &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-proc-mount &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;disallow-selinux &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;restrict-apparmor-profiles &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;restrict-seccomp &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;restrict-sysctls &lt;span class="nb"&gt;true&lt;/span&gt; audit &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;unique-ingress-host &lt;span class="nb"&gt;false&lt;/span&gt; enforce &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that, we already have plenty to work with to educate our users (developers) on producing secure Kubernetes deployments.&lt;/p&gt;
&lt;h2 id="user-experience"&gt;User experience
&lt;/h2&gt;&lt;p&gt;One of the annoying things about both tools is that out of the box, they&amp;rsquo;re not very user friendly. Personally, running a &lt;code&gt;kubectl get events&lt;/code&gt; command (or &lt;code&gt;kubectl get events --field-selector=reason=PolicyViolation&lt;/code&gt; in Kyverno&amp;rsquo;s case) to figure out why a deployment failed doesn&amp;rsquo;t bother me much (I almost enjoy it).&lt;/p&gt;
&lt;p&gt;But for some people, it&amp;rsquo;s a bit off-putting, and I can understand that.&lt;/p&gt;
&lt;p&gt;My goal is to make infrastructure simpler for people whose job it isn&amp;rsquo;t, and imposing my way of working isn&amp;rsquo;t necessarily the best approach.&lt;/p&gt;
&lt;p&gt;Often, to help adopt a new tool or practice, the simplest thing is to provide a graphical interface.&lt;/p&gt;
&lt;p&gt;And it turns out Kyverno has a tool for that.&lt;/p&gt;
&lt;h2 id="kyverno-policy-reporter"&gt;Kyverno policy reporter
&lt;/h2&gt;&lt;p&gt;In reality, the tool wasn&amp;rsquo;t originally designed for that exact purpose. But you&amp;rsquo;ll see it&amp;rsquo;s actually even better.&lt;/p&gt;
&lt;p&gt;Historically, it&amp;rsquo;s called policy reporter because the developers needed a tool to easily and quickly export the &lt;em&gt;PolicyReports&lt;/em&gt; I mentioned in the previous article:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;kubectl describe policyreport polr-ns-default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Name: polr-ns-default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Namespace: default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Labels: managed-by=kyverno
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Annotations: &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;API Version: wgpolicyk8s.io/v1alpha2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Kind: PolicyReport
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Results:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Category: Sample
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Message: The Ingress host name must be unique.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Policy: unique-ingress-host
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Resources:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; API Version: networking.k8s.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Kind: Ingress
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Name: ingress2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Summary:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Error: 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Fail: 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Pass: 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Skip: 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Warn: 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The policy reporter&amp;rsquo;s role is to export PolicyReports to several external sources for easier processing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Grafana Loki&lt;/li&gt;
&lt;li&gt;Elasticsearch&lt;/li&gt;
&lt;li&gt;Slack&lt;/li&gt;
&lt;li&gt;Discord&lt;/li&gt;
&lt;li&gt;MS Teams&lt;/li&gt;
&lt;li&gt;Policy Reporter UI&lt;/li&gt;
&lt;li&gt;S3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ll cover 2 of them: &lt;strong&gt;Slack&lt;/strong&gt; and &lt;strong&gt;Policy Reporter UI&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="policy-reporter-ui"&gt;Policy Reporter UI
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ll start with the UI since it&amp;rsquo;s the simplest to set up. You just need to deploy the Policy Reporter Helm chart, making sure to specify that you want the UI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm repo add policy-reporter https://kyverno.github.io/policy-reporter
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm install -n kyverno policy-reporter policy-reporter/policy-reporter --set kyvernoPlugin.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; --set ui.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; --set ui.plugins.kyverno&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl -n kyverno get pods
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NAME READY STATUS RESTARTS AGE
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kyverno-5f8bfd6fc5-xp6bm 1/1 Running &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;59m ago&lt;span class="o"&gt;)&lt;/span&gt; 33d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;policy-reporter-74cc9bf4b9-9ggwh 1/1 Running &lt;span class="m"&gt;0&lt;/span&gt; 18s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;policy-reporter-kyverno-plugin-6b48c4bc5f-5rwfb 1/1 Running &lt;span class="m"&gt;0&lt;/span&gt; 18s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;policy-reporter-ui-5c9449d58-f9fwf 1/1 Running &lt;span class="m"&gt;0&lt;/span&gt; 18s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We could obviously add an Ingress, but if you&amp;rsquo;re lazy like me during this demo, a simple port-forward will do.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl port-forward service/policy-reporter-ui 8082:8080 -n kyverno
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The UI itself is fairly intuitive. It includes notably:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a Dashboard with a big global indicator showing how many policy violations you have&lt;/li&gt;
&lt;li&gt;a page with your &amp;ldquo;policy reports&amp;rdquo;, filterable by namespace or policy&lt;/li&gt;
&lt;li&gt;the list of your policies&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/09/kyverno-policy-reporter-ui.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/09/kyverno-policy-reporter-ui-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="slack"&gt;Slack
&lt;/h2&gt;&lt;p&gt;You can immediately see the value of alerting administrators in Slack (or Teams&amp;hellip;) when a policy violation occurs.&lt;/p&gt;
&lt;p&gt;The configuration is fairly trivial — you just need to add a webhook in Slack and a small piece of configuration in Kyverno:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kyvernoPlugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;kyverno&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://hooks.slack.com/services/T0xxxxxxxx&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;minimumPriority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;medium&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;skipExistingOnStartup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;kyverno&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And we re-apply all this to our Helm release:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm upgrade -n kyverno policy-reporter policy-reporter/policy-reporter -f kyverno-values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note: If you don&amp;rsquo;t know how to do this, you need to be admin of the Slack workspace, create an application at &lt;a class="link" href="https://api.slack.com/apps" target="_blank" rel="noopener"
&gt;https://api.slack.com/apps&lt;/a&gt;, enable &lt;code&gt;Incoming Webhooks&lt;/code&gt;, and create one with permissions on a channel.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/09/slack-create-app.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/09/slack-create-app-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/09/slack-app-webhooks.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/09/slack-app-webhooks-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="how-about-we-trigger-a-policy-violation-just-to-see"&gt;How about we trigger a policy violation, just to see?
&lt;/h2&gt;&lt;p&gt;To keep it simple, I went back to the example from the previous article. I switch my &lt;strong&gt;unique-ingress-host&lt;/strong&gt; policy back to &lt;strong&gt;audit&lt;/strong&gt; mode and recreate the problematic ingress:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl edit clusterpolicy unique-ingress-host
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f ingress2.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instantly, 2 failures show up in the Kyverno UI, along with messages in Slack:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/09/kyverno-fail.avif"
loading="lazy"
&gt;
&lt;img src="https://blog.zwindler.fr/2022/09/kyverno-slack.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Victory :)&lt;/p&gt;
&lt;h2 id="additional-resources"&gt;Additional Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://kyverno.io/policies/" target="_blank" rel="noopener"
&gt;&amp;ldquo;Catalog&amp;rdquo; of official Kyverno Policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://dev.to/aurelievache/understanding-kubernetes-part-44-tools-kyverno-52mc" target="_blank" rel="noopener"
&gt;Aurélie Vache - Understanding Kubernetes: part 44 – Tools - Kyverno&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Kubernetes Compliance Policies with Kyverno</title><link>https://blog.zwindler.fr/en/2022/08/01/kubernetes-compliance-policies-with-kyverno/</link><pubDate>Mon, 01 Aug 2022 06:00:00 +0000</pubDate><guid>https://blog.zwindler.fr/en/2022/08/01/kubernetes-compliance-policies-with-kyverno/</guid><description>&lt;img src="https://blog.zwindler.fr/2022/07/Kyverno_Horizontal.webp" alt="Featured image of post Kubernetes Compliance Policies with Kyverno" /&gt;&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;If the title of this article sounds familiar, it&amp;rsquo;s probably because I already used it 2 years ago when talking about another compliance policy management tool: &lt;a class="link" href="https://blog.zwindler.fr/2020/07/20/vos-politiques-de-conformite-sur-kubernetes-avec-opa-et-gatekeeper/" &gt;Open Policy Agent (OPA) and its Kubernetes counterpart, Gatekeeper&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And I won&amp;rsquo;t lie, I haven&amp;rsquo;t done much OPA since that 2020 article&amp;hellip;&lt;/p&gt;
&lt;p&gt;The first reason is that I changed companies right at that time and had other priorities at my new employer.&lt;/p&gt;
&lt;p&gt;The second reason, probably more accurate but perhaps less flattering, is that OPA is not exactly user friendly. Especially because of &lt;strong&gt;rego&lt;/strong&gt;, the compliance policy language.&lt;/p&gt;
&lt;p&gt;Since then, I discovered &lt;a class="link" href="https://kyverno.io/" target="_blank" rel="noopener"
&gt;Kyverno&lt;/a&gt;, a newcomer in the &amp;ldquo;cloud native&amp;rdquo; game (recently promoted to &amp;ldquo;CNCF sandbox&amp;rdquo; level) that offers compliance policies for Kubernetes only (unlike OPA + rego which are general-purpose), written in YAML.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With Kyverno, policies are managed as Kubernetes resources and no new language is required to write policies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Take that, &lt;strong&gt;rego&lt;/strong&gt; ;-)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This article will remain relatively simple, it&amp;rsquo;s an introduction. We&amp;rsquo;ll go further in subsequent articles (writing your own policy, the UI, etc).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not going to innovate much compared to the OPA article and will start from the same use case: preventing two developers from using the same URL for their web app deployed on Kubernetes (because we&amp;rsquo;ve seen it&amp;rsquo;s pretty bad when that happens).&lt;/p&gt;
&lt;p&gt;And you&amp;rsquo;ll see that the initial learning curve is MUUUCH more accessible with Kyverno than with OPA (even though it might annoy half of my Twitter followers that I&amp;rsquo;m once again talking about YAML).&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites
&lt;/h2&gt;&lt;p&gt;Same setup as for OPA / Gatekeeper, you need Kube 1.14 to use Kyverno. Version 1.14 wasn&amp;rsquo;t the latest back in 2020, it&amp;rsquo;s practically prehistoric by now ;-).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Your Kubernetes cluster version must be above v1.14 which adds webhook timeouts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Obviously, you also need admin access to the cluster, because we&amp;rsquo;ll have to create &lt;em&gt;Validating Admission Webhooks&lt;/em&gt; and &lt;em&gt;CRDs&lt;/em&gt;. Again, I&amp;rsquo;ll refer you to the &lt;a class="link" href="https://blog.zwindler.fr/2020/07/20/vos-politiques-de-conformite-sur-kubernetes-avec-opa-et-gatekeeper/" &gt;previous article&lt;/a&gt; and the official documentation if this doesn&amp;rsquo;t ring a bell.&lt;/p&gt;
&lt;p&gt;In short, we&amp;rsquo;re going to insert ourselves into Kubernetes&amp;rsquo; deployment process to prevent people from doing things that don&amp;rsquo;t follow your best practices (with webhooks) and we&amp;rsquo;ll add extra APIs to extend Kubernetes (the CRDs), so we can describe what&amp;rsquo;s not allowed.&lt;/p&gt;
&lt;h2 id="deployment"&gt;Deployment
&lt;/h2&gt;&lt;p&gt;Installation-wise, we&amp;rsquo;re keeping it simple and efficient.&lt;/p&gt;
&lt;p&gt;Kyverno offers several Helm Charts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one for &lt;strong&gt;Kyverno&lt;/strong&gt; itself&lt;/li&gt;
&lt;li&gt;one for &lt;strong&gt;Policy Reporter&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;one containing a &amp;ldquo;pack&amp;rdquo; of default rules, configured as &amp;ldquo;non-blocking&amp;rdquo; (basically the equivalent of what you could do with Pod Security Policies)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For this article, I&amp;rsquo;m only interested in Kyverno itself. But as I mentioned above, we&amp;rsquo;ll discuss the other two in a future post.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s start by adding Kyverno itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; helm repo add kyverno https://kyverno.github.io/kyverno/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; helm install kyverno kyverno/kyverno --namespace kyverno --create-namespace
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; NAME: kyverno
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; LAST DEPLOYED: Mon Jul 25 21:55:11 2022
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; NAMESPACE: kyverno
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; STATUS: deployed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; REVISION: 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; NOTES:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Chart version: v2.5.2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Kyverno version: v1.7.2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Thank you for installing kyverno! Your release is named kyverno.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ⚠️ WARNING: Setting replicas count below 3 means Kyverno is not running in high availability mode.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; 💡 Note: There is a trade-off when deciding which approach to take regarding Namespace exclusions. Please see the documentation at https://kyverno.io/docs/installation/#security-vs-operability to understand the risks.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; you&amp;rsquo;ll notice the &lt;strong&gt;WARNING&lt;/strong&gt; during install. By default, Kyverno is deployed with a single replica. If it crashes, you can no longer deploy or modify anything on your cluster (it happened to me). The solution to recover is to delete Kyverno&amp;rsquo;s &lt;strong&gt;webhooks&lt;/strong&gt; to unblock the situation. And of course, give it more room (higher limits, especially for RAM) and definitely 3 replicas (or more)&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note 2:&lt;/strong&gt; It&amp;rsquo;s probably best to exclude certain namespaces (kube-system, kyverno, &amp;hellip;) from Kyverno&amp;rsquo;s scope to avoid nasty surprises. &lt;em&gt;Thibault Lengagne&lt;/em&gt;&amp;rsquo;s blogpost at Padok also discusses this (link at the bottom of the article).&lt;/p&gt;
&lt;h2 id="what-did-we-deploy"&gt;What did we deploy?
&lt;/h2&gt;&lt;p&gt;At first glance, not much. If you run &lt;code&gt;kubectl -n kyverno get all&lt;/code&gt;, you&amp;rsquo;ll find just a &lt;em&gt;Pod&lt;/em&gt; (+&lt;em&gt;ReplicaSet&lt;/em&gt; +&lt;em&gt;Deployment&lt;/em&gt;) and two &lt;em&gt;Services&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;But behind the scenes, we&amp;rsquo;ve deployed much more than that ;)&lt;/p&gt;
&lt;p&gt;Looking for CRDs, you&amp;rsquo;ll see we have quite a few new objects at our disposal.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl api-resources --api-group&lt;span class="o"&gt;=&lt;/span&gt;kyverno.io
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; NAME SHORTNAMES APIVERSION NAMESPACED KIND
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; clusterpolicies cpol kyverno.io/v1 false ClusterPolicy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; clusterreportchangerequests crcr kyverno.io/v1alpha2 false ClusterReportChangeRequest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; generaterequests gr kyverno.io/v1 true GenerateRequest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; policies pol kyverno.io/v1 true Policy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; reportchangerequests rcr kyverno.io/v1alpha2 true ReportChangeRequest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; updaterequests ur kyverno.io/v1beta1 true UpdateRequest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Similarly, we also have new webhooks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl get validatingwebhookconfigurations.admissionregistration.k8s.io
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; NAME WEBHOOKS AGE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; kyverno-policy-validating-webhook-cfg 1 8m48s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; kyverno-resource-validating-webhook-cfg 2 8m48s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; NAME WEBHOOKS AGE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; kyverno-policy-mutating-webhook-cfg 1 11m
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; kyverno-resource-mutating-webhook-cfg 2 11m
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; kyverno-verify-mutating-webhook-cfg 1 11m
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;What&amp;rsquo;s it for? How does it work?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To simplify, we&amp;rsquo;ll describe what we don&amp;rsquo;t want to allow on our cluster using the &lt;em&gt;ClusterPolicy&lt;/em&gt; CRD.&lt;/p&gt;
&lt;p&gt;Based on the rules we write, the &lt;strong&gt;webhooks&lt;/strong&gt; will insert themselves into Kubernetes&amp;rsquo; normal resource deployment process to either allow or reject them.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/07/admission-controller-phases.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a class="link" href="https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/" target="_blank" rel="noopener"
&gt;Kubernetes Blog - A Guide to Kubernetes Admission Controllers&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="ingress-with-the-same-url-example"&gt;Ingress with the same URL example
&lt;/h2&gt;&lt;p&gt;Enough theory, the easiest way is to show you. As I mentioned at the beginning of the article, to compare the two products, I&amp;rsquo;m going to start from the same use case.&lt;/p&gt;
&lt;p&gt;In my Kubernetes clusters, I want to prevent two applications from creating an &lt;em&gt;Ingress&lt;/em&gt; with the same URL, because in my case, that&amp;rsquo;s not allowed. It&amp;rsquo;s a sign of a copy/paste error from one Helm Chart to another (or any other human error).&lt;/p&gt;
&lt;p&gt;The consequence of this error is a conflict in routing incoming HTTP requests, and you end up with &lt;em&gt;some kind of janky round robin&lt;/em&gt; where half the requests hit the wrong backend. In production, that&amp;rsquo;s a bad look, and you risk ending up with customers complaining on Twitter that the application is down ;-).&lt;/p&gt;
&lt;p&gt;Just as OPA provides a community policy to address this problem, &lt;a class="link" href="https://kyverno.io/policies/other/unique-ingress-paths/unique-ingress-paths/" target="_blank" rel="noopener"
&gt;Kyverno has the same&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll look in more detail in the next article at how these rules are built. For now, we&amp;rsquo;ll just deploy this rule in &amp;ldquo;audit&amp;rdquo; mode and see if it works.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;spec:
validationFailureAction: audit&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl apply -f https://raw.githubusercontent.com/kyverno/policies/main/other/unique-ingress-paths/unique-ingress-paths.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; clusterpolicy.kyverno.io/unique-ingress-host created
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you have a Kubernetes cluster at hand, I suggest you apply these two &lt;em&gt;Ingresses&lt;/em&gt;, configured to receive traffic from the same URL (toto.example.org):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; counter in &lt;span class="m"&gt;1&lt;/span&gt; 2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;cat &amp;gt; ingress${counter}.yaml &amp;lt;&amp;lt;EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;apiVersion: networking.k8s.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;kind: Ingress
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;metadata:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; name: ingress${counter}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;spec:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; rules:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; - host: toto.example.org
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; http:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; paths:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; - path: /
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; pathType: Prefix
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; backend:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; service:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; name: service${counter}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; port:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; number: 4200
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;done
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl apply -f ingress1.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ingress.networking.k8s.io/ingress1 created
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl apply -f ingress2.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ingress.networking.k8s.io/ingress2 created
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Normally, nothing happens. Kyverno doesn&amp;rsquo;t prevent you from creating these two &lt;em&gt;Ingresses&lt;/em&gt; since we&amp;rsquo;re in &amp;ldquo;validationFailureAction: audit&amp;rdquo; mode (even if the backends don&amp;rsquo;t actually exist behind them, that doesn&amp;rsquo;t matter).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl get ingress
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; NAME CLASS HOSTS ADDRESS PORTS AGE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ingress1 &amp;lt;none&amp;gt; toto.example.org 80 90s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ingress2 &amp;lt;none&amp;gt; toto.example.org 80 87s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, if you check the &lt;strong&gt;Events&lt;/strong&gt;, you&amp;rsquo;ll find &lt;strong&gt;PolicyViolation&lt;/strong&gt; entries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl get events --field-selector&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PolicyViolation
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;LAST SEEN TYPE REASON OBJECT MESSAGE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;118s Warning PolicyViolation ingress/ingress1 policy unique-ingress-host/check-single-host fail: The Ingress host name must be unique.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;118s Warning PolicyViolation ingress/ingress1 policy unique-ingress-host/check-single-host fail: The Ingress host name must be unique.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;118s Warning PolicyViolation clusterpolicy/unique-ingress-host Ingress default/ingress1: [check-single-host] fail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;115s Warning PolicyViolation clusterpolicy/unique-ingress-host Ingress default/ingress2: [check-single-host] fail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The fact that we have 2 &lt;em&gt;Ingresses&lt;/em&gt; pointing to the same URL is properly detected and shows up in Kubernetes &lt;em&gt;Events&lt;/em&gt;. Nice :)&lt;/p&gt;
&lt;p&gt;In a much more verbose format, you can also retrieve &lt;em&gt;PolicyReports&lt;/em&gt;, which say the same thing in an even less readable way.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;kubectl describe policyreport polr-ns-default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Name: polr-ns-default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Namespace: default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Labels: managed-by=kyverno
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Annotations: &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;API Version: wgpolicyk8s.io/v1alpha2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Kind: PolicyReport
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Results:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Category: Sample
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Message: The Ingress host name must be unique.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Policy: unique-ingress-host
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Resources:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; API Version: networking.k8s.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Kind: Ingress
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Name: ingress2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Summary:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Error: 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Fail: 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Pass: 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Skip: 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; Warn: 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s clean up for what&amp;rsquo;s next:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl delete ingress ingress1 ingress2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-law-is-me"&gt;&amp;ldquo;The law is me&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Imagine you&amp;rsquo;ve now thoroughly tested your policies, your developers no longer generate manifests that don&amp;rsquo;t follow your best practices (e.g., no root containers, mandatory labels present, correct limits/requests, etc). Your cluster is clean!&lt;/p&gt;
&lt;p&gt;You can switch your compliance policies to &amp;ldquo;enforcing&amp;rdquo; mode. Unlike &amp;ldquo;audit&amp;rdquo; mode, this mode is blocking, and manifests that don&amp;rsquo;t comply with the policies will be rejected by Kubernetes.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl patch clusterpolicies.kyverno.io unique-ingress-host --patch &lt;span class="s1"&gt;&amp;#39;{&amp;#34;spec&amp;#34;:{&amp;#34;validationFailureAction&amp;#34;:&amp;#34;enforce&amp;#34;}}&amp;#39;&lt;/span&gt; --type merge
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;Yes, I&amp;rsquo;m using &amp;ldquo;patch&amp;rdquo; instead of &amp;ldquo;edit&amp;rdquo; to show off ;-)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And let&amp;rsquo;s test again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl apply -f ingress1.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ingress.networking.k8s.io/ingress1 created
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So far, so good&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; kubectl apply -f ingress2.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Error from server: error when creating &amp;#34;ingress2.yaml&amp;#34;: admission webhook &amp;#34;validate.kyverno.svc-fail&amp;#34; denied the request:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;resource Ingress/default/ingress2.yaml was blocked due to the following policies
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;unique-ingress-host:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; check-single-host: The Ingress host name must be unique.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;AND BAM!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2020/05/chocapic.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;In this basic (but genuinely useful) example, we can just as easily prevent users from creating &lt;em&gt;Ingresses&lt;/em&gt; with the same URL as we could with OPA.&lt;/p&gt;
&lt;p&gt;So one might ask &amp;ldquo;why prefer Kyverno?&amp;rdquo;. Beyond some other nice features we&amp;rsquo;ll explore in future posts, my answer fits in a single image.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/07/rego_vs_kyverno.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;My (very personal) take is that &lt;strong&gt;rego&lt;/strong&gt; is unreadable and verbose, and therefore unmaintainable. Again, this is personal, but I&amp;rsquo;ve never managed to get into it.&lt;/p&gt;
&lt;p&gt;Without going as far as idealizing Kyverno&amp;rsquo;s language*, I find it is clearly &lt;strong&gt;much simpler&lt;/strong&gt; to write, but more importantly, to read. If you ever need to modify/write/maintain your own policies, this will be very handy.&lt;/p&gt;
&lt;p&gt;(*Yes, there is a language, especially for pattern matching&amp;hellip; and as an annoying point, it&amp;rsquo;s &lt;strong&gt;not&lt;/strong&gt; jsonpath like &lt;code&gt;kubectl&lt;/code&gt;, but &lt;a class="link" href="https://kyverno.io/docs/writing-policies/variables/" target="_blank" rel="noopener"
&gt;JMESPath&lt;/a&gt;, similar but not identical&amp;hellip;)&lt;/p&gt;
&lt;h2 id="additional-resources"&gt;Additional Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://kyverno.io/policies/" target="_blank" rel="noopener"
&gt;&amp;ldquo;Catalog&amp;rdquo; of official Kyverno Policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.padok.fr/blog/securite-kubernetes-kyverno" target="_blank" rel="noopener"
&gt;Blog de Padok - Kyverno: Securing your Kubernetes environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://dev.to/aurelievache/understanding-kubernetes-part-44-tools-kyverno-52mc" target="_blank" rel="noopener"
&gt;Aurélie Vache - Understanding Kubernetes: part 44 – Tools - Kyverno&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/" target="_blank" rel="noopener"
&gt;Kubernetes Blog - A Guide to Kubernetes Admission Controllers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>