<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Core Solutions</title>
    <description>The latest articles on DEV Community by Core Solutions (coresolutions).</description>
    <link>https://dev.to/coresolutions</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14548%2Fb59cdcc2-4bee-4a01-b9a1-4fbf07d6253e.jpg</url>
      <title>DEV Community: Core Solutions</title>
      <link>https://dev.to/coresolutions</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coresolutions"/>
    <language>en</language>
    <item>
      <title>Helm charts are technical debt: blame the templating model</title>
      <dc:creator>Billy Walker</dc:creator>
      <pubDate>Mon, 31 Aug 2026 08:03:53 +0000</pubDate>
      <link>https://dev.to/coresolutions/helm-charts-are-technical-debt-blame-the-templating-model-19g9</link>
      <guid>https://dev.to/coresolutions/helm-charts-are-technical-debt-blame-the-templating-model-19g9</guid>
      <description>&lt;p&gt;The pull request is four lines of template, adding an optional &lt;code&gt;nodeSelector&lt;/code&gt; to an internal chart. CI has already done its job properly: &lt;code&gt;helm lint&lt;/code&gt; passed, the chart rendered against every environment's values file, &lt;code&gt;kubeconform&lt;/code&gt; validated each manifest that came out, and the snapshot diff of the rendered output is sitting in the check run waiting to be read.&lt;/p&gt;

&lt;p&gt;And you still cannot say, from the diff, what this change does in staging without going and reading three other templates first.&lt;/p&gt;

&lt;p&gt;Nobody files that as technical debt. It goes in the mental folder marked “charts are a bit fiddly”. But if your team maintains a dozen first-party charts, that tax gets levied on every review, and no amount of pipeline pays it off, because the pipeline was never the thing that was broken.&lt;/p&gt;

&lt;p&gt;This is not a “Helm was always bad” post. Helm solved a real problem, and most teams running Kubernetes are better off with it than without it. The rot is somewhere more specific: &lt;strong&gt;Helm renders Kubernetes manifests by running Go's &lt;code&gt;text/template&lt;/code&gt; over YAML as strings, and once you are authoring charts rather than just installing them, that model generates work forever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That model has survived every major version so far, Helm 4 included. If your chart estate already feels brittle, the next release will not make it less so, and I will come back to why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Helm won for good reasons
&lt;/h2&gt;

&lt;p&gt;Helm is a CNCF graduated project and the default packaging story in Kubernetes, and it deserves to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install, upgrade, rollback and release history in a single tool&lt;/li&gt;
&lt;li&gt;a standard way for vendors and open-source projects to ship applications&lt;/li&gt;
&lt;li&gt;OCI registry support that slots into existing GitOps workflows&lt;/li&gt;
&lt;li&gt;ecosystem gravity, which by now matters nearly as much as technical merit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody sensible is still having the “should we use Helm at all?” argument. If you are installing Grafana, or &lt;a href="https://dev.to/blog/secrets-management-scale-external-secrets-operator"&gt;External Secrets Operator&lt;/a&gt;, or almost anything with a healthy community behind it, the chart is the path of least resistance and you should take it.&lt;/p&gt;

&lt;p&gt;The trouble is that the same tool that consumes packaged software so well also makes it very easy to start writing your own. One chart for the internal API. One for the batch worker. Then a shared library chart, because three of them had grown the same &lt;code&gt;_helpers.tpl&lt;/code&gt;. At some point you stopped being a Helm user and became the maintainer of a templating system. That is a perfectly reasonable thing to be, as long as it was a decision rather than a drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The debt is in the rendering model
&lt;/h2&gt;

&lt;p&gt;Helm templates know nothing about Kubernetes objects. They emit text that happens to be YAML.&lt;/p&gt;

&lt;p&gt;Here is the shape of it, lifted from roughly every &lt;code&gt;deployment.yaml&lt;/code&gt; in every internal chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# templates/deployment.yaml&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- with .Values.nodeSelector&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;nodeSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- toYaml . | nindent 8&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Chart.Name&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.image.repository&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;:{{ .Values.image.tag }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;8&lt;/code&gt; is load-bearing. &lt;code&gt;nodeSelector:&lt;/code&gt; sits at column six, so its keys have to land at column eight, and &lt;a href="https://helm.sh/docs/chart_template_guide/function_list/" rel="noopener noreferrer"&gt;&lt;code&gt;nindent&lt;/code&gt;&lt;/a&gt; exists precisely because chart authors need to re-indent embedded blocks constantly. Write &lt;code&gt;nindent 6&lt;/code&gt; by mistake and you have not made a formatting error. You have set &lt;code&gt;nodeSelector&lt;/code&gt; to null and promoted its contents to siblings of &lt;code&gt;containers&lt;/code&gt;, and the render will print that quite happily, because it is still a well-formed YAML document. Nothing in the YAML layer objects. It takes schema validation downstream, &lt;code&gt;kubeconform -strict&lt;/code&gt; in the pipeline or a server-side dry run, before anything flags it. That is a lot of machinery to need in order to catch a two-space mistake.&lt;/p&gt;

&lt;p&gt;Two spaces. Helm's own &lt;a href="https://helm.sh/docs/chart_best_practices/templates/" rel="noopener noreferrer"&gt;template best practices&lt;/a&gt; page walks through indentation, whitespace chomping and generated-output formatting, and concedes the underlying problem in passing: “YAML is a whitespace-oriented language.” Full marks for honesty, but that is a peculiar property for an abstraction layer to have.&lt;/p&gt;

&lt;p&gt;All of which is the cheap version of the problem, and it is cheap precisely because a machine can express it. Schema validation catches a wrong shape, and it catches it in seconds. What no pipeline catches is a chart whose &lt;em&gt;right&lt;/em&gt; shapes have become impossible to hold in your head.&lt;/p&gt;

&lt;p&gt;Once your abstraction works on strings rather than typed objects, a few things follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;indentation becomes part of correctness, not just tidiness&lt;/li&gt;
&lt;li&gt;a branch that renders the wrong shape still looks plausible in review&lt;/li&gt;
&lt;li&gt;values become an API surface long before anyone has designed them like one&lt;/li&gt;
&lt;li&gt;refactoring is hard, because the behaviour lives across helpers, partials and value conventions rather than in a schema your tooling understands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disciplined teams are not exempt from this. They often accumulate &lt;em&gt;more&lt;/em&gt; chart machinery, because they build helpers and layering and conventions to contain the complexity. That containment is genuinely useful, and it is also the tell: you are building scaffolding around a model that produces complexity faster than you can absorb it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it shows up on day two
&lt;/h2&gt;

&lt;p&gt;You rarely spot chart debt by reading a chart. You spot it in the changes that need far more scrutiny than their diff size suggests they should.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;values.yaml&lt;/code&gt; turns into an untyped product surface
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;values.yaml&lt;/code&gt; is the front door to your chart, which is convenient right up until you realise you have shipped a long-lived API with no type system behind it.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;values.schema.json&lt;/code&gt; belongs in every first-party chart, and Helm will enforce it on &lt;code&gt;install&lt;/code&gt;, &lt;code&gt;upgrade&lt;/code&gt;, &lt;code&gt;lint&lt;/code&gt; and &lt;code&gt;template&lt;/code&gt;. But the &lt;a href="https://helm.sh/docs/topics/charts/#schema-files" rel="noopener noreferrer"&gt;schema file is optional&lt;/a&gt;, so it is opt-in per chart and easy to leave behind on the one chart nobody has revisited, and it describes structure rather than behaviour. A value can be structurally perfect and still produce nonsense once it has threaded its way through conditionals, helper templates, defaults and subchart overrides.&lt;/p&gt;

&lt;p&gt;Then another team starts depending on a value name. Renaming a field stops being a cleanup and becomes a migration, complete with a deprecation window and a note in the release notes that nobody reads.&lt;/p&gt;

&lt;h3&gt;
  
  
  The source is harder to review than the output
&lt;/h3&gt;

&lt;p&gt;A rendered manifest can be entirely ordinary while the logic that produced it is anything but.&lt;/p&gt;

&lt;p&gt;Humans review the chart source. A pull request that flips a condition or threads one more flag through three templates is not asking “is this &lt;code&gt;Deployment&lt;/code&gt; valid?”. It is asking “which combinations of values, defaults and includes now produce a different document?”, and that question has no diff you can look at.&lt;/p&gt;

&lt;p&gt;Which is why the gates all end up pointed at the rendered output rather than the source. Those gates are worth building, and I will come back to ours. They are also a fair signal about the model: you test the output because nobody can reliably reason about the input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subcharts and value plumbing sprawl quietly
&lt;/h3&gt;

&lt;p&gt;Small Helm setups look tidy. Then the platform grows, one chart wraps five subcharts, globals appear, child values get overridden from the parent, and a flag in one place changes behaviour three templates away.&lt;/p&gt;

&lt;p&gt;Some of those values exist because users asked for them. Some exist because a subchart expects them. Some exist because somebody needed a workaround two years ago and nobody has ever felt brave enough to delete it. That is the point where a chart stops being a packaging unit and starts being institutional memory encoded as YAML conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Helm 4 improved Helm without changing this
&lt;/h2&gt;

&lt;p&gt;This is where lazy anti-Helm takes get sloppy, so let me be precise about it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/helm/helm/releases/tag/v4.0.0" rel="noopener noreferrer"&gt;Helm 4.0.0&lt;/a&gt; shipped in November 2025 and it is a genuine improvement: server-side apply, a redesigned plugin system with WebAssembly plugins, resource watching built on &lt;code&gt;kstatus&lt;/code&gt;, post-renderers as plugins, reproducible chart archives. If you run Helm heavily, upgrade.&lt;/p&gt;

&lt;p&gt;All of that is release machinery, though. Rendering still goes through Go's text templating, &lt;code&gt;values.yaml&lt;/code&gt; is still the primary interface, and the combinatorial growth of flags and helpers in your own charts remains entirely your problem. Helm 4 made Helm better at the job Helm is good at. Chart authoring was never on the list.&lt;/p&gt;

&lt;p&gt;A version bump on its own will not retire this argument, so here is what would: a release that swaps text templating for a typed object model, or one that makes schema validation mandatory instead of opt-in. If you are reading this a few releases later, that is the thing to go and check. Short of it, the version numbers in this post move and the point does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the alternatives actually change
&lt;/h2&gt;

&lt;p&gt;Different tools replace different &lt;em&gt;parts&lt;/em&gt; of Helm, which is where most “Helm alternatives” posts go wrong. There is no swap that hands you better authoring, better packaging, better release management and better ecosystem compatibility all at once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it replaces&lt;/th&gt;
&lt;th&gt;What you still need&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kustomize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Templating, for the overlay case&lt;/td&gt;
&lt;td&gt;Packaging, versioning, release history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timoni&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The authoring model, with CUE typing&lt;/td&gt;
&lt;td&gt;Ecosystem gravity, and patience while it is pre-1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cdk8s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Templating, with a real programming language&lt;/td&gt;
&lt;td&gt;Packaging, plus a language runtime you now own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jsonnet / Tanka&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Templating, with a compact composition syntax&lt;/td&gt;
&lt;td&gt;Types, and a way to stop dynamic behaviour relocating complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KCL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Templating, with a typed config language&lt;/td&gt;
&lt;td&gt;Ecosystem gravity, and a team willing to learn a new language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The developer-facing values surface&lt;/td&gt;
&lt;td&gt;A platform implementation underneath that satisfies the spec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://kubectl.docs.kubernetes.io/references/kustomize/" rel="noopener noreferrer"&gt;Kustomize&lt;/a&gt; is the one I reach for most often outside Helm, largely because it already ships inside &lt;code&gt;kubectl&lt;/code&gt; and patches structured YAML instead of templating arbitrary strings. If your real problem is “I have plain manifests and need per-environment overlays”, it removes a surprising amount of cleverness for almost no adoption cost. Expecting it to do packaging too is how people end up disappointed by it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://timoni.sh/" rel="noopener noreferrer"&gt;Timoni&lt;/a&gt; is the most interesting answer to “what if we fixed the authoring model?”. CUE, real typing, modules distributed as OCI artifacts: conceptually most of what people are asking for. The caveat is maturity, and it is still on its 0.x line (&lt;code&gt;v0.29.0&lt;/code&gt; as I write this), so go and check where it has got to before committing. A 1.0 would change the calculation considerably more than another point release will.&lt;/p&gt;

&lt;p&gt;The code-generation family trades typing for operational surface. &lt;a href="https://www.cncf.io/projects/cdk-for-kubernetes-cdk8s/" rel="noopener noreferrer"&gt;cdk8s&lt;/a&gt; (joined the CNCF Sandbox in 2020) gives you real programming languages, Jsonnet with Tanka gives you a compact composition language, and &lt;a href="https://www.cncf.io/projects/kcl/" rel="noopener noreferrer"&gt;KCL&lt;/a&gt; (joined in September 2023) gives you an explicitly typed configuration language. In exchange you own a language runtime, a build step, and an onboarding cost for everyone who ever touches a manifest. Good trade for a platform team with a large estate, poor trade for a team with six charts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cncf.io/projects/score/" rel="noopener noreferrer"&gt;Score&lt;/a&gt; (joined in July 2024) sits one layer up. It is a workload specification, asking “what does this application need from the platform?” rather than “how do I render manifests for every deployment case?”. If your real complaint is that developers keep being handed an ever-growing values surface to fill in, that is the layer to attack. It still assumes a platform underneath that can satisfy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we actually run this
&lt;/h2&gt;

&lt;p&gt;We stayed on Helm, so it is only fair to say what containing the cost looks like day to day.&lt;/p&gt;

&lt;p&gt;Most of our platform components are wrapper charts, and that pattern carries a lot of the weight. &lt;code&gt;Chart.yaml&lt;/code&gt; declares a pinned dependency on the upstream chart, &lt;code&gt;Chart.lock&lt;/code&gt; freezes what actually resolved, and our own &lt;code&gt;templates/&lt;/code&gt; directory layers on what upstream does not ship: the NetworkPolicies, the PrometheusRules, the RBAC that upstream leaves to you. Renovate raises the upstream bumps, and they do not land until CI is green.&lt;/p&gt;

&lt;p&gt;Values sit in one place per chart. &lt;code&gt;values.yaml&lt;/code&gt; carries the defaults every environment shares, and a &lt;code&gt;values/&lt;/code&gt; directory holds the environment-specific overrides beside it, one file each. CI then lints the chart once against every one of those files rather than once overall, so a change that only breaks one environment cannot ride in on another's values.&lt;/p&gt;

&lt;p&gt;That catches everything Helm can be made to fail on. The quieter failure needs its own gate, because of a footgun worth internalising: Helm deep-merges maps, but it replaces lists outright. An environment file that overrides a list has to restate the entire list, so adding a component in one environment and forgetting another raises no error anywhere. Both files are valid. You just ship a shorter list to the environment nobody updated, and find out when something is missing. Nothing you can put in &lt;code&gt;values.yaml&lt;/code&gt; expresses “these lists must stay in step”, so a parity check in CI does it instead, and a red PR replaces copy-paste discipline. That is the scaffolding tax from earlier made concrete: a shell script exists because the type system does not.&lt;/p&gt;

&lt;p&gt;None of this makes the templating model typed. It makes the cost visible and bounded, which is the achievable goal rather than the ideal one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I would draw the line
&lt;/h2&gt;

&lt;p&gt;Helm is not technical debt because it exists in your stack, and authoring your own charts is not a mistake. We do both, and given the same constraints we would do both again. A third-party chart you install, version and occasionally override is packaging doing exactly the job it is good at, and a first-party chart is a reasonable way to ship your own software into a cluster where everything else already arrives as one.&lt;/p&gt;

&lt;p&gt;So I reach for Helm when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I am consuming third-party software that already ships a chart and I have no wish to become its packager&lt;/li&gt;
&lt;li&gt;I want release history, rollback and the install workflow Helm already gives me&lt;/li&gt;
&lt;li&gt;the surrounding ecosystem expects Helm artifacts, &lt;a href="https://dev.to/blog/gitops-for-kubernetes-with-argo-cd"&gt;GitOps tooling&lt;/a&gt; included&lt;/li&gt;
&lt;li&gt;I am packaging our own components for a platform where every other component already arrives as a chart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What accrues interest is the estate you build afterwards, and how much of it you let grow without a gate on it. Our largest first-party chart carries a &lt;code&gt;values.yaml&lt;/code&gt; north of 800 lines. That is not automatically a failure, but it is a number I would want any team to be able to quote about themselves, because the moment nobody can tell you what a chart's values surface costs, you have stopped managing it. Same instinct as &lt;a href="https://dev.to/blog/stop-copying-big-tech-platform-architecture"&gt;right-sizing the rest of your platform&lt;/a&gt;: the tool is fine, and the real question is whether the estate around it still fits the team.&lt;/p&gt;

&lt;p&gt;The alternatives are worth knowing even when you stay. We have stayed, because packaging gravity and release history are worth more to us today than typed authoring would be. If that trade flips, if the estate outgrows its gates or Timoni reaches 1.0 with real ecosystem behind it, the honest move is to notice rather than to defend the original choice.&lt;/p&gt;

&lt;p&gt;You do not have to become anti-Helm to admit that the charts you maintain can rot. In most Kubernetes estates that is hardly a hot take. It is just what year two looks like.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>helm</category>
      <category>kustomize</category>
    </item>
    <item>
      <title>PodSecurityPolicy replacement: Pod Security Admission</title>
      <dc:creator>Billy Walker</dc:creator>
      <pubDate>Mon, 31 Aug 2026 07:57:40 +0000</pubDate>
      <link>https://dev.to/coresolutions/podsecuritypolicy-replacement-pod-security-admission-45i3</link>
      <guid>https://dev.to/coresolutions/podsecuritypolicy-replacement-pod-security-admission-45i3</guid>
      <description>&lt;p&gt;If you still think of Pod Security Admission as the cut-down replacement for PodSecurityPolicy, that is exactly the mindset that makes rollouts harder than they need to be. It &lt;em&gt;is&lt;/em&gt; less powerful. That is also why it is practical. You get three fixed profiles, three modes, a handful of labels, and an admission controller that is already in the API server. No custom policy language, no mutation, no controller to install, and far fewer ways to surprise yourself mid-change.&lt;/p&gt;

&lt;p&gt;The trick is to use that simplicity properly. A safe rollout is not “turn on &lt;code&gt;restricted&lt;/code&gt; everywhere and hope for the best”. It is version-pinned labels, audit and warn first, then namespace-by-namespace enforcement once you have seen what will break. Done that way, you can move from the old PSP world to a built-in control that teams will actually leave enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaced PodSecurityPolicy
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PodSecurityPolicy&lt;/code&gt; was deprecated in Kubernetes &lt;code&gt;v1.21&lt;/code&gt; and removed in &lt;code&gt;v1.25&lt;/code&gt;. &lt;a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/" rel="noopener noreferrer"&gt;Pod Security Admission&lt;/a&gt; became generally available in the same release, which matters because there is nothing extra to deploy: if your cluster is current enough, the mechanism is already there.&lt;/p&gt;

&lt;p&gt;The model is deliberately small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profiles&lt;/strong&gt;: &lt;code&gt;privileged&lt;/code&gt;, &lt;code&gt;baseline&lt;/code&gt;, &lt;code&gt;restricted&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modes&lt;/strong&gt;: &lt;code&gt;enforce&lt;/code&gt;, &lt;code&gt;audit&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version pinning&lt;/strong&gt;: &lt;code&gt;pod-security.kubernetes.io/&amp;lt;mode&amp;gt;-version&lt;/code&gt;, pinned to a Kubernetes minor version or &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those labels live on namespaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# pin policy to the minor version your cluster is running&lt;/span&gt;
&lt;span class="nv"&gt;pss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl version &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.serverVersion.gitVersion'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;,2&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

kubectl label ns payments &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;baseline &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/enforce-version&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pss&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That simplicity is the point. PSP mixed policy with RBAC, allowed mutation, and had enough moving parts that many teams either avoided it or never quite trusted what a change would do. PSA is narrower. It sets a floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: three profiles, three modes, one version pin
&lt;/h2&gt;

&lt;p&gt;You can think of PSA as a matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enforce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejects a non-compliant pod creation request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allows it, but records an audit annotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allows it, but shows a warning to the caller&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the profiles are ordered from least to most strict:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;What it is for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;privileged&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;System namespaces and workloads that genuinely need host access or privileged behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;baseline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A sensible minimum floor for ordinary application namespaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;restricted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Current pod hardening best practice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most application namespaces, the safe starting pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label &lt;span class="nt"&gt;--overwrite&lt;/span&gt; ns payments &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;baseline &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/enforce-version&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pss&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/warn&lt;span class="o"&gt;=&lt;/span&gt;restricted &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/warn-version&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pss&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/audit&lt;span class="o"&gt;=&lt;/span&gt;restricted &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/audit-version&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pss&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a firm minimum bar in &lt;code&gt;enforce&lt;/code&gt;, while &lt;code&gt;warn&lt;/code&gt; and &lt;code&gt;audit&lt;/code&gt; show you what stands between the namespace and &lt;code&gt;restricted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The version pin is not optional bureaucracy. If you leave a namespace on &lt;code&gt;latest&lt;/code&gt;, any cluster upgrade can quietly tighten policy under your workloads. Pinning to the minor version you are actually running means policy changes happen when &lt;em&gt;you&lt;/em&gt; decide to move the label.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;restricted&lt;/code&gt; actually checks
&lt;/h2&gt;

&lt;p&gt;The easiest way to lose credibility on this topic is to hand-wave the &lt;code&gt;restricted&lt;/code&gt; profile and get one detail wrong. Several of the checklists floating around in other posts are simply false — especially around &lt;code&gt;runAsUser&lt;/code&gt; and seccomp.&lt;/p&gt;

&lt;p&gt;Here is the practical checklist, straight from the &lt;a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/" rel="noopener noreferrer"&gt;Pod Security Standards&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;What &lt;code&gt;restricted&lt;/code&gt; expects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Volume types&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;configMap&lt;/code&gt;, &lt;code&gt;csi&lt;/code&gt;, &lt;code&gt;downwardAPI&lt;/code&gt;, &lt;code&gt;emptyDir&lt;/code&gt;, &lt;code&gt;ephemeral&lt;/code&gt;, &lt;code&gt;persistentVolumeClaim&lt;/code&gt;, &lt;code&gt;projected&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege escalation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;allowPrivilegeEscalation: false&lt;/code&gt; on every container, including init and ephemeral containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-root&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;runAsNonRoot: true&lt;/code&gt; at pod level or per container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UID&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;runAsUser&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; need to be set, but if it is, it cannot be &lt;code&gt;0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capabilities&lt;/td&gt;
&lt;td&gt;Drop &lt;code&gt;ALL&lt;/code&gt;; the only allowed add is &lt;code&gt;NET_BIND_SERVICE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seccomp&lt;/td&gt;
&lt;td&gt;Must be explicitly &lt;code&gt;RuntimeDefault&lt;/code&gt; or &lt;code&gt;Localhost&lt;/code&gt;; leaving it unset is a violation under &lt;code&gt;restricted&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those rows are where the bad checklists go wrong. &lt;code&gt;runAsUser&lt;/code&gt; is not required — the policy forbids UID &lt;code&gt;0&lt;/code&gt;, it does not make you hard-code some other UID just to satisfy the admission check. And seccomp cuts the other way: under &lt;code&gt;baseline&lt;/code&gt; an unset profile is fine, but under &lt;code&gt;restricted&lt;/code&gt; it is a violation in its own right, which is one of the reasons plain demo manifests still bounce when teams first try this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rollout that does not break production
&lt;/h2&gt;

&lt;p&gt;The safest PSA rollout starts with a dry run, not an enforcement change — the &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/migrate-from-psp/" rel="noopener noreferrer"&gt;official migration guide&lt;/a&gt; is built around the same idea.&lt;/p&gt;

&lt;p&gt;First, test label application server-side across all namespaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;server &lt;span class="nt"&gt;--overwrite&lt;/span&gt; ns &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not change anything. It asks the API server what violations it would report if the label were applied.&lt;/p&gt;

&lt;p&gt;Then stage &lt;code&gt;audit&lt;/code&gt; and &lt;code&gt;warn&lt;/code&gt; cluster-wide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label &lt;span class="nt"&gt;--overwrite&lt;/span&gt; ns &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/audit&lt;span class="o"&gt;=&lt;/span&gt;baseline &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/warn&lt;span class="o"&gt;=&lt;/span&gt;baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if your end state is more ambitious, go straight to the pattern that tends to work well in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enforce=baseline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;warn=restricted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;audit=restricted&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you signal without immediately creating an outage because some forgotten sidecar, root-running image, or hostPath mount was hiding in a namespace nobody had looked at for months.&lt;/p&gt;

&lt;p&gt;You can also find namespaces that have no PSA labels yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get ns &lt;span class="nt"&gt;--selector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'!pod-security.kubernetes.io/enforce'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the warnings are clean for a namespace, pin the version and turn on &lt;code&gt;enforce&lt;/code&gt; there. Namespace by namespace is slower than one giant switch, but slower is exactly what you want when admission control is involved.&lt;/p&gt;

&lt;p&gt;While the rollout is in flight, the API server's own metrics — &lt;code&gt;pod_security_evaluations_total&lt;/code&gt;, &lt;code&gt;pod_security_errors_total&lt;/code&gt; and &lt;code&gt;pod_security_exemptions_total&lt;/code&gt; — are worth scraping. They tell you whether evaluation is happening where you think it is, and whether exemptions are starting to sprawl.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real rejection looks like
&lt;/h2&gt;

&lt;p&gt;The good news about PSA failures is that the error message is the fix list.&lt;/p&gt;

&lt;p&gt;Apply a plain pod into a namespace with &lt;code&gt;enforce=restricted&lt;/code&gt; and you get something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error from server (Forbidden): pods "nginx" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message is unusually helpful by Kubernetes standards. Every violation names the exact field to set and the value it wants, so you can work through it like a checklist.&lt;/p&gt;

&lt;p&gt;A minimal compliant pod looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;seccompProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RuntimeDefault&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginxinc/nginx-unprivileged:stable&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ALL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two quiet fixes are hiding in that example: the container-level security context, and the image itself. A lot of “hello world” manifests still assume a root-running image on port &lt;code&gt;80&lt;/code&gt;, which is a fine way to discover that &lt;code&gt;restricted&lt;/code&gt; is not interested in your tutorial shortcuts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap most teams hit once: Deployments apply, pods do not
&lt;/h2&gt;

&lt;p&gt;This is the operational detail that makes or breaks a rollout plan.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;warn&lt;/code&gt; and &lt;code&gt;audit&lt;/code&gt; evaluate workload resources such as &lt;code&gt;Deployment&lt;/code&gt; pod templates. &lt;code&gt;enforce&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt;. &lt;code&gt;enforce&lt;/code&gt; applies only to the resulting pod objects.&lt;/p&gt;

&lt;p&gt;That means this can happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You apply a &lt;code&gt;Deployment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Kubernetes accepts the &lt;code&gt;Deployment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ReplicaSet&lt;/code&gt; tries to create pods&lt;/li&gt;
&lt;li&gt;The pods are rejected by PSA&lt;/li&gt;
&lt;li&gt;You now have a deployment that looks “applied” but never becomes healthy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When that happens, the signal is in the &lt;code&gt;ReplicaSet&lt;/code&gt; and the events, not in the initial &lt;code&gt;kubectl apply&lt;/code&gt; output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get rs &lt;span class="nt"&gt;-n&lt;/span&gt; payments
kubectl get events &lt;span class="nt"&gt;-n&lt;/span&gt; payments &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.metadata.creationTimestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you remember only one rollout lesson from this post, make it this one. Admission on pod creation and admission on workload templates are not symmetrical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The namespaces that should not be &lt;code&gt;restricted&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kube-system&lt;/code&gt; is the obvious example. CNIs, CSIs, node agents, and static pods routinely need things that &lt;code&gt;baseline&lt;/code&gt; or &lt;code&gt;restricted&lt;/code&gt; will reject: &lt;code&gt;hostPath&lt;/code&gt;, &lt;code&gt;hostNetwork&lt;/code&gt;, privileged containers, or other host-level access.&lt;/p&gt;

&lt;p&gt;Be explicit about that. Label those namespaces &lt;code&gt;privileged&lt;/code&gt; so the exception is documented in the cluster state rather than living as folklore.&lt;/p&gt;

&lt;p&gt;Istio adds another common surprise — relevant if you run &lt;a href="https://dev.to/blog/zero-trust-networking-kubernetes-istio-service-mesh"&gt;a mesh for zero-trust networking&lt;/a&gt;. With sidecar injection but without &lt;a href="https://istio.io/latest/docs/setup/additional-setup/cni/" rel="noopener noreferrer"&gt;Istio CNI&lt;/a&gt;, the injected &lt;code&gt;istio-init&lt;/code&gt; container needs &lt;code&gt;NET_ADMIN&lt;/code&gt; and &lt;code&gt;NET_RAW&lt;/code&gt; to set up traffic redirection. That is enough to fail &lt;strong&gt;baseline&lt;/strong&gt;, not just &lt;code&gt;restricted&lt;/code&gt;. The usual fix is to enable the CNI node agent and keep &lt;code&gt;istio-system&lt;/code&gt; itself &lt;code&gt;privileged&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why version pinning matters more than it looks
&lt;/h2&gt;

&lt;p&gt;A pinned PSA version is an operational control, not a documentation detail.&lt;/p&gt;

&lt;p&gt;Kubernetes &lt;code&gt;v1.34&lt;/code&gt; added a baseline control that blocks the &lt;code&gt;host&lt;/code&gt; field in &lt;code&gt;httpGet&lt;/code&gt; and &lt;code&gt;tcpSocket&lt;/code&gt; probes and lifecycle hooks. The reason is sensible: that field can be abused as an SSRF path through the kubelet. The operational consequence is that a cluster upgrade can start rejecting manifests that were fine the day before — &lt;em&gt;if&lt;/em&gt; your labels track &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With version pinning, you choose when that tighter rule lands. With &lt;code&gt;$pss&lt;/code&gt; still set to the minor version you were running before the upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label &lt;span class="nt"&gt;--overwrite&lt;/span&gt; ns payments &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/enforce-version&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pss&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/warn-version&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pss&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/audit-version&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pss&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes upgrades much less dramatic. Upgrade the cluster first. Move the policy version when you are ready to deal with the findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RBAC hole nobody should ignore
&lt;/h2&gt;

&lt;p&gt;PSA policy lives on namespace labels. That means anyone who can update namespace labels can also weaken or remove enforcement.&lt;/p&gt;

&lt;p&gt;Review that RBAC carefully. It is easy to focus on pod-creation rights and forget that &lt;code&gt;update&lt;/code&gt; on namespaces is effectively policy-admin access.&lt;/p&gt;

&lt;p&gt;There is a more central exemption mechanism through the API server &lt;code&gt;AdmissionConfiguration&lt;/code&gt;, where you can exempt namespaces, usernames, or runtime classes. That is useful on self-managed control planes. On managed services such as EKS, GKE, and AKS, you usually do not get to edit that file at all, which means namespace labels are the real policy surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Pod Security Admission stops
&lt;/h2&gt;

&lt;p&gt;PSA is the built-in floor. That is its job, and it is a good one.&lt;/p&gt;

&lt;p&gt;There is no mutation, so no PSP-style defaulting is waiting to save a sloppy manifest. There is no custom rule language, and no way to express fine-grained per-workload exceptions inside one namespace. Once you want those things, you are into policy engines such as Kyverno or OPA Gatekeeper.&lt;/p&gt;

&lt;p&gt;That is not a weakness so much as a division of labour. Use PSA to guarantee the cluster-wide minimum. Use a policy engine above it when you need mutation, richer exceptions, or application-specific constraints.&lt;/p&gt;

&lt;p&gt;PodSecurityPolicy is gone. The sensible replacement is not to rebuild PSP in another tool and pretend nothing changed. It is to accept the smaller built-in model for what it is: predictable, cheap to operate, and strong enough to become the default floor across a cluster. That is a better trade than a perfect policy system nobody trusts enough to enable.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
