[PSS] Align Kyverno Helm Chart CEL Policies with Kubernetes PSA
The kyverno-policies Helm chart ships CEL-based ValidatingPolicy templates that implement the Kubernetes Pod Security Standards. Several of these policies no longer match what Kubernetes Pod Security Admission (PSA) does today. Some policies allow Pods that Kubernetes denies, and others deny Pods that Kubernetes allows.
This issue tracks the work to bring the chart's CEL policies back in line with PSA, including the user namespace relaxations added in Kubernetes v1.35 (issue #15979)
Sources
| Source | Path | State |
|---|---|---|
| Kubernetes PSA | kubernetes\pod-security-admission |
master at 49bb59e |
| Kyverno Helm chart | kyverno\kyverno\charts\kyverno-policies |
main at 21168fdfe |
The Kyverno checkout is 11 commits behind upstream/main, but none of those commits change charts/kyverno-policies/templates/, so the comparison reflects current upstream chart behavior.
Drift Summary
Of the 19 Kubernetes PSA checks:
- 10 match.
- 8 exist in the chart but drift behaviorally:
appArmorProfile,procMount,sysctls,allowPrivilegeEscalation,capabilities_restricted,runAsNonRoot, andrunAsUser, plusseccompProfile_restricted. - 1 has no chart template and is marked Missing:
hostProbesAndHostLifecycle.
Scope
This comparison covers only the 18 .cel.yaml templates under charts/kyverno-policies/templates/, which render policies.kyverno.io/v1beta1 ValidatingPolicy resources. Deprecated legacy implementations are out of scope: the .yaml ClusterPolicy templates that use validate.pattern, validate.deny, or validate.podSecurity.
Kubernetes PSA registers 19 checks: 12 Baseline and 7 Restricted. Each check evaluates at the highest revision whose MinimumVersion the policy version allows, so this comparison uses the latest revision of each check. The Restricted profile also runs every Baseline check that no Restricted check overrides.
In the chart, podSecurityStandard: restricted enables the Baseline templates as well as the Restricted templates, as kyverno-policies.podSecurityBaseline in templates/_helpers.tpl shows. Rows that depend on this behavior say so.
Summary table
| Profile | K8s | Kyverno Helm CEL | Drift (Yes/No) | Note |
|---|---|---|---|---|
| Baseline | privilegedpolicy/check_privileged.go |
disallow-privileged-containerstemplates/baseline/disallow-privileged-containers.cel.yaml |
No | Both reject securityContext.privileged=true in containers, init containers, and ephemeral containers, and both allow the field to be unset. |
| Baseline | hostNamespacespolicy/check_hostNamespaces.go |
disallow-host-namespacestemplates/baseline/disallow-host-namespaces.cel.yaml |
No | Both require spec.hostNetwork, spec.hostPID, and spec.hostIPC to be unset or false. |
| Baseline | hostPathVolumespolicy/check_hostPathVolumes.go |
disallow-host-pathtemplates/baseline/disallow-host-path.cel.yaml |
No | Both reject any volume that carries a hostPath source and ignore every other volume type. |
| Baseline | hostPortspolicy/check_hostPorts.go |
disallow-host-portstemplates/baseline/disallow-host-ports.cel.yaml |
No | Both require every container port hostPort to be unset or 0 across all three container scopes. |
| Baseline | capabilities_baselinepolicy/check_capabilities_baseline.go |
disallow-capabilitiestemplates/baseline/disallow-capabilities.cel.yaml |
No | The chart repeats the same 13-entry allow-list as capabilities_allowed_1_0, with no extra escape value. |
| Baseline | appArmorProfilepolicy/check_appArmorProfile.go |
restrict-apparmor-profilestemplates/baseline/restrict-apparmor-profiles.cel.yaml |
Yes | allowedAnnotationValue accepts any annotation starting with localhost/, but the CEL compares against the literal string localhost/*, so the chart rejects a real profile name such as localhost/my-profile. |
| Baseline | seLinuxOptionspolicy/check_seLinuxOptions.go |
disallow-selinuxtemplates/baseline/disallow-selinux.cel.yaml |
No | The chart allows the same five types as selinuxAllowedTypes1_31, including container_engine_t, and blocks any user or role value at pod and container level. |
| Baseline | procMount (v1.35 user namespaces)policy/check_procMount_baseline.go |
disallow-proc-mounttemplates/baseline/disallow-proc-mount.cel.yaml |
Yes | procMount1_35baseline allows any procMount value when spec.hostUsers is false; the chart blocks anything other than Default unconditionally and never reads hostUsers. |
| Baseline | seccompProfile_baselinepolicy/check_seccompProfile_baseline.go |
restrict-seccomptemplates/baseline/restrict-seccomp.cel.yaml |
No | At the current revision PSA runs seccompProfileBaseline_1_19, which allows RuntimeDefault, Localhost, or unset at pod and container level. The chart applies the same rule. |
| Baseline | sysctlspolicy/check_sysctls.go |
restrict-sysctlstemplates/baseline/restrict-sysctls.cel.yaml |
Yes | sysctlsAllowedV1Dot37 holds 14 safe sysctls; the chart allows only the 10 from the v1.29 set, so it rejects net.ipv4.tcp_rmem, net.ipv4.tcp_wmem, net.ipv4.tcp_slow_start_after_idle, and net.ipv4.tcp_notsent_lowat. |
| Baseline | windowsHostProcesspolicy/check_windowsHostProcess.go |
disallow-host-processtemplates/baseline/disallow-host-process.cel.yaml |
No | Both reject windowsOptions.hostProcess=true at pod level and in all three container scopes. |
| Baseline | hostProbesAndHostLifecycle (v1.34)policy/check_hostProbesAndhostLifecycle.go |
Missing | Yes | No chart template reads probe or lifecycle handler fields, so a non-empty httpGet.host or tcpSocket.host passes the whole CEL Baseline set. |
| Restricted | restrictedVolumespolicy/check_restrictedVolumes.go |
restrict-volume-typestemplates/restricted/restrict-volume-types.cel.yaml |
No | Both allow the same nine inline volume sources, including image, and reject everything else. |
| Restricted | allowPrivilegeEscalationpolicy/check_allowPrivilegeEscalation.go |
disallow-privilege-escalationtemplates/restricted/disallow-privilege-escalation.cel.yaml |
Yes | allowPrivilegeEscalation_1_25 admits a Pod whose spec.os.name is windows without the field set; the chart has no spec.os guard and requires an explicit false, which a Windows Pod cannot set. |
| Restricted | capabilities_restrictedpolicy/check_capabilities_restricted.go |
disallow-capabilities-stricttemplates/restricted/disallow-capabilities-strict.cel.yaml |
Yes | capabilitiesRestricted_1_22 denies a container that omits securityContext, capabilities, or capabilities.drop, but each of those short-circuits the chart expression to true, so a container that never drops ALL is admitted. |
| Restricted | runAsNonRoot (v1.35 user namespaces)policy/check_runAsNonRoot.go |
require-run-as-nonroottemplates/restricted/require-run-as-nonroot.cel.yaml |
Yes | The chart admits an explicit pod-level runAsNonRoot: false whenever every container sets true, which runAsNonRoot1_0 denies, and it lacks the hostUsers: false relaxation that runAsNonRoot1_35 adds. |
| Restricted | runAsUser (v1.35 user namespaces)policy/check_runAsUser.go |
require-run-as-non-root-usertemplates/restricted/require-run-as-non-root-user.cel.yaml |
Yes | Both reject runAsUser: 0 at pod and container level, but runAsUser1_35 allows it when spec.hostUsers is false and the chart still denies it. |
| Restricted | seccompProfile_restrictedpolicy/check_seccompProfile_restricted.go |
restrict-seccomp-stricttemplates/restricted/restrict-seccomp-strict.cel.yaml |
Yes | The required-profile logic matches, including inheritance from a valid pod-level profile, but seccompProfileRestricted_1_25 admits a Windows Pod with no profile set and the chart denies it. |
| Restricted | procMount_restricted (v1.35)policy/check_procMount_restricted.go |
disallow-proc-mounttemplates/baseline/disallow-proc-mount.cel.yaml |
No | PSA overrides the relaxed Baseline check so Unmasked stays blocked under Restricted. podSecurityStandard: restricted also enables the Baseline chart template, which blocks unconditionally, so the net Restricted behavior matches. |
Drifts that admit Pods Kubernetes denies
Treat these as security gaps:
capabilities_restricted: a container with nosecurityContext, nocapabilities, or nocapabilities.droppasses, sodrop: ["ALL"]is never actually enforced.runAsNonRoot: a pod-levelrunAsNonRoot: falsepasses when every container setstrue.hostProbesAndHostLifecycle: any probe or lifecycle handlerhostvalue passes.
Drifts that deny Pods Kubernetes admits
Treat these as false positives:
appArmorProfile: validlocalhost/<profile>annotation values are rejected.procMount,runAsNonRoot, andrunAsUser: user-namespace Pods lose the v1.35 relaxations.sysctls: four sysctls that PSA added in v1.32 and v1.37 are rejected.allowPrivilegeEscalation,capabilities_restricted, andseccompProfile_restricted: Windows Pods are rejected because the chart has nospec.os.nameexemption.
Kubernetes v1.35 user namespace behavior
relaxPolicyForUserNamespacePod in policy/helpers.go returns true only when spec.hostUsers is present and set to false. An absent hostUsers does not relax anything, and hostUsers: true does not either.
Three checks call it:
| Check | Source file | Effect when hostUsers: false |
|---|---|---|
procMount (Baseline) |
policy/check_procMount_baseline.go |
Any procMount value is allowed, including Unmasked. |
runAsNonRoot (Restricted) |
policy/check_runAsNonRoot.go |
Any runAsNonRoot value is allowed, including false. |
runAsUser (Restricted) |
policy/check_runAsUser.go |
Any runAsUser value is allowed, including 0. |
The Restricted profile does not inherit the procMount relaxation. CheckProcMountRestricted in policy/check_procMount_restricted.go registers at v1.35 with OverrideCheckIDs set to procMount and runs procMount_1_0, so Unmasked stays blocked under Restricted even inside a user namespace.
No template under charts/kyverno-policies/templates/ references hostUsers. As a result, the chart is stricter than Kubernetes for user-namespace Pods on all three checks.
Extension policies
This chart template has no Kubernetes PSS counterpart and does not count as a PSS match:
| Kyverno Helm CEL | Path | Reason |
|---|---|---|
require-non-root-groups |
templates/other/require-non-root-groups.cel.yaml |
Requires runAsGroup != 0 at pod and container level. PSA registers no runAsGroup check at any level, so this is an addition rather than a PSS control. |
Source: kyverno/kyverno