#17639·kyverno

[PSS] Align Kyverno Helm Chart CEL Policies with Kubernetes PSA

Author: Suhani95Created Sep 18, 2026Updated Sep 19, 2026

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, and runAsUser, plus seccompProfile_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 privileged
policy/check_privileged.go
disallow-privileged-containers
templates/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 hostNamespaces
policy/check_hostNamespaces.go
disallow-host-namespaces
templates/baseline/disallow-host-namespaces.cel.yaml
No Both require spec.hostNetwork, spec.hostPID, and spec.hostIPC to be unset or false.
Baseline hostPathVolumes
policy/check_hostPathVolumes.go
disallow-host-path
templates/baseline/disallow-host-path.cel.yaml
No Both reject any volume that carries a hostPath source and ignore every other volume type.
Baseline hostPorts
policy/check_hostPorts.go
disallow-host-ports
templates/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_baseline
policy/check_capabilities_baseline.go
disallow-capabilities
templates/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 appArmorProfile
policy/check_appArmorProfile.go
restrict-apparmor-profiles
templates/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 seLinuxOptions
policy/check_seLinuxOptions.go
disallow-selinux
templates/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-mount
templates/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_baseline
policy/check_seccompProfile_baseline.go
restrict-seccomp
templates/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 sysctls
policy/check_sysctls.go
restrict-sysctls
templates/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 windowsHostProcess
policy/check_windowsHostProcess.go
disallow-host-process
templates/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 restrictedVolumes
policy/check_restrictedVolumes.go
restrict-volume-types
templates/restricted/restrict-volume-types.cel.yaml
No Both allow the same nine inline volume sources, including image, and reject everything else.
Restricted allowPrivilegeEscalation
policy/check_allowPrivilegeEscalation.go
disallow-privilege-escalation
templates/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_restricted
policy/check_capabilities_restricted.go
disallow-capabilities-strict
templates/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-nonroot
templates/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-user
templates/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_restricted
policy/check_seccompProfile_restricted.go
restrict-seccomp-strict
templates/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-mount
templates/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 no securityContext, no capabilities, or no capabilities.drop passes, so drop: ["ALL"] is never actually enforced.
  • runAsNonRoot: a pod-level runAsNonRoot: false passes when every container sets true.
  • hostProbesAndHostLifecycle: any probe or lifecycle handler host value passes.

Drifts that deny Pods Kubernetes admits

Treat these as false positives:

  • appArmorProfile: valid localhost/<profile> annotation values are rejected.
  • procMount, runAsNonRoot, and runAsUser: 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, and seccompProfile_restricted: Windows Pods are rejected because the chart has no spec.os.name exemption.

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.