On September 7 this blog covered Kyverno and how to enforce that only signed images enter your cluster (“Nur signierte Images im Cluster: Provenance mit Kyverno erzwingen”, in German). Kyverno is the right toolbox for that: freely definable admission policies that you write yourself, for rules Kubernetes does not ship with.
For a large part of pod hardening, though, you don’t need to open that toolbox at all. Since version 1.25, Kubernetes ships with its own admission controller that knows three ready-made security levels and is activated by a single namespace label: Pod Security Admission (PSA). No Helm chart, no CRD, no update cycle for a third component. Just a label that the kube-apiserver evaluates itself.
This article covers what PSA can actually do, where the built-in boundary lies, and at what point you inevitably end up with Kyverno (or a comparable policy engine).
What Pod Security Admission is
PSA is an admission controller built into kube-apiserver. The official documentation describes it as “built-in Pod Security admission controller to enforce the Pod Security Standards” (kubernetes.io, Pod Security Admission). Its feature status has been “Stable” since Kubernetes 1.25, so PSA is no longer a beta feature but GA (General Availability).
The Pod Security Standards that PSA enforces are not something we came up with. They are a Kubernetes specification in their own right, with three cumulative levels (kubernetes.io, Pod Security Standards):
- Privileged: “purposely-open, and entirely unrestricted”. No restrictions, intended for system and infrastructure workloads under trusted control.
- Baseline: “aimed at ease of adoption for common containerized workloads while preventing known privilege escalations”. Among other things, it forbids privileged containers, sharing host namespaces (
hostNetwork,hostPID,hostIPC), host path volumes, and Linux capabilities beyond a fixed allowlist. - Restricted: “Heavily restricted policy, following current Pod hardening best practices”. On top of Baseline, it requires among other things that containers do not run as root, that privilege escalation (
allowPrivilegeEscalation) is disabled, that all capabilities are dropped (onlyNET_BIND_SERVICEmay optionally be added back), and that a seccomp profile is set explicitly.
These three levels are predefined. You can pick one, but you can’t rewrite it. That is exactly the difference from Kyverno, more on that below.
The three modes: enforce, audit, warn
PSA has three independently configurable modes per namespace (kubernetes.io, Pod Security Admission):
| Mode | Behavior |
|---|---|
enforce | A violation causes the pod to be rejected. |
audit | A violation is recorded as an audit annotation in the audit log, and the pod is still created. |
warn | A violation triggers a warning for the user (e.g. on kubectl apply), and the pod is still created. |
Important in practice: according to the documentation, enforce only applies to the final pod objects, not to the templates of Deployments, StatefulSets, or Jobs themselves. audit and warn, on the other hand, are additionally applied to the workload resources, so violations become visible at kubectl apply on a Deployment, before the first pod even exists.
The namespace label in practice
Each level and each mode is set through a label pair pod-security.kubernetes.io/<MODE>: <LEVEL>. Optionally, you can pin the Kubernetes minor version to check against (kubernetes.io, Pod Security Admission):
apiVersion: v1kind: Namespacemetadata: name: team-checkout labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted
With these labels, the namespace team-checkout is immediately set to the most restrictive level, with no controller deployment, no custom policy language, and no additional maintenance. If you want to observe before you enforce hard, set enforce to baseline for now and audit/warn to restricted already. That way you see in the audit log and in the kubectl output which workloads would break if you tightened the level, without blocking anything in production yet.
Where the built-in boundary ends
PSA checks only fields in a pod’s securityContext and some related spec fields, as defined by the three standard levels. There is no mechanism to add a custom rule, such as “only images from a specific registry” or “only images with a valid signature”. That was exactly the subject of the Kyverno article from September 7: provenance and signature verification is not part of the three predefined Pod Security Standards, so PSA cannot express it in principle, regardless of which level you choose.
Kyverno, in turn, is built according to its own documentation to manage policies “as declarative Kubernetes resources” and to use “YAML and CEL … with no new language to learn” for freely defined rules (kyverno.io, Introduction). It can run as its own admission controller in the cluster and checks not only pods but any resource type against conditions you write yourself.
How this differs from the Kyverno article from September 7 (in German): That article was about freely definable admission policies, such as enforcing image signatures, which are written as custom rules and run in the cluster. This one is about the built-in, predefined Pod Security Standards, which live directly in the namespace label without a custom policy language and without an additional installation.
PSA vs. Kyverno compared
| Pod Security Admission | Kyverno | |
|---|---|---|
| Installation | Built into kube-apiserver, no additional component (kubernetes.io) | Its own admission controller, must be operated in the cluster (kyverno.io) |
| Rule scope | Three predefined levels: privileged, baseline, restricted (kubernetes.io) | Freely definable rules as custom YAML resources (kyverno.io) |
| Configuration | One namespace label per mode | Policy resources (e.g. ClusterPolicy) with custom logic |
| What it checks | Pod securityContext and related spec fields | Any resource types and fields |
| Example use case | Forbid root containers, privileged containers, host namespaces | Enforce image signatures, registry allowlisting, mandatory labels |
| Operational effort | None beyond maintaining the label | Its own lifecycle: updates, CRDs, monitoring of the policy engine |
When PSA is enough, and when it isn’t
If your requirement can be phrased in one sentence with “root”, “privileged”, “host namespace”, or “capabilities”, one of the three Pod Security Standards probably already covers it. Then the namespace label is enough, and you save yourself the operation of an additional policy engine. As soon as your requirement is a rule that doesn’t exist in the three standards (image origin, registry membership, custom label or annotation requirements, relationships between multiple resources), PSA has reached the end of its range, and you need an engine like Kyverno that understands custom rules.
The economically sensible approach is therefore not “Kyverno for everything” but to separate the levels: first set the built-in, free PSA label to restricted wherever it is enough, and use Kyverno specifically for the rules that PSA structurally cannot express.
If you want help deciding between Pod Security Admission and Kyverno for your cluster: write to me at mm@mhm-dl.de.
Hinterlasse einen Kommentar