Four of Our Six Policies Shouldn't Be Policies
A Kyverno ClusterPolicy with four rules — non-root, no privileged, no privilege escalation, drop ALL capabilities — is four rules Pod Security Admission already enforces for free. The policy file says so in its own header.
The most useful comment in the POC's policy set is the one that argues against the file it sits on top of. 03-require-nonroot-drop-caps.yaml is a Kyverno ClusterPolicy with four rules, and its header block ends like this:
NOTE: this duplicates a large part of the Pod Security Admission "restricted" profile, which is built into Kubernetes and costs nothing. Prefer PSA labels on the namespace for these four, and keep Kyverno for the supply-chain rules it alone can express (registry allow-list, digest, signature, CVE threshold).
That is a deliverable telling its reader not to deploy most of it.
What the four rules are
require-run-as-non-root—runAsNonRoot=trueat pod or container leveldisallow-privileged—securityContext.privilegedmust not be truedisallow-privilege-escalation—allowPrivilegeEscalationmust be falserequire-drop-all-capabilities—capabilities.dropmust containALL
Every one of those is a clause of the PSS restricted profile. Kubernetes will enforce all four by putting a label on a namespace. No webhook, no controller, no CRDs, no availability risk when the admission pod is unscheduled.
Two paths to the same four controls: a built-in namespace label with no moving parts, or a ClusterPolicy on the admission path.
The cost that does not appear in the YAML
Reading the manifest makes the asymmetry concrete. The Kyverno version of these four rules needs anyPattern blocks with optional-key markers for ephemeralContainers and initContainers, because the rule has to account for pod-level and container-level settings and for every container list a Pod can have. require-drop-all-capabilities needs two foreach blocks with a JMESPath fallback — request.object.spec.[initContainers, ephemeralContainers][] || \[]\`` — so that the absence of a list is not treated as a violation.
That expression is correct. It is also a thing someone now maintains, tests, and re-validates on every Kyverno upgrade, to reproduce behaviour that ships in the API server. The policy is declared background: true, and every rule carries failureAction: Audit with allowExistingViolations: true — the right way to phase a webhook in, and a whole phase-in plan PSA's warn and audit label modes give you as labels.
What Kyverno is for
The header names the four things it keeps for the policy engine: registry allow-list, digest, signature, CVE threshold. Look at what those have in common. None of them are statements about a pod's securityContext. They are statements about an image's provenance and its scan state — facts that live outside the admission request and have to be fetched, verified, or cross- referenced against another object in the cluster. PSA cannot express any of them, and no amount of labelling will change that.
That is the line worth holding: a custom policy engine earns its operational cost on the rules that require external knowledge. On the rules that are pure field inspection of the incoming object, the platform already won.
The honesty in the header
One more line in that file is worth reading for its own sake: POC DELIVERABLE — NOT APPLIED TO THE CLUSTER, validated offline with kyverno apply ... --resource. The policy was written, tested against fixtures, and handed over with an argument for why four-sixths of it should be replaced by a namespace label before anyone applies it.
A deliverable that tells you which parts of itself are redundant is worth more than one that does not.
Next: keyless signing has two hard blockers here — the supply-chain rule Kyverno alone can express, and why it still could not be turned on.