Kyverno: a PodSecurityPolicy replacement, or something more?
PodSecurityPolicy was removed in Kubernetes 1.25. Here is Kyverno as a replacement — validate, mutate and generate policies, on the actual manifests we run in production.
Originally published at
Hello!
Have you heard of Kyverno, and what it is for? In this article I will explain what it does and show, with examples, how we use it.
My name is Makariy Balashov, I am an SRE at Ak Bars Digital. Our team builds and maintains the infrastructure the development teams run on.
So what is this Kyverno of yours?
Kyverno is a policy engine built specifically for Kubernetes.
Kyverno lets cluster administrators manage environment-specific configuration independently of workload configuration, and enforce configuration best practices across their clusters. You can use it to scan existing resources for best-practice violations, or to enforce them by blocking or mutating API requests.
A short list of things Kyverno does well:
-
policies are Kubernetes resources (plain YAML manifests);
-
it can validate, mutate or generate k8s resources;
-
container image verification for software supply chain security;
-
image metadata verification;
-
configuration synchronisation across namespaces;
-
blocking non-compliant resources through admission control, or reporting policy violations instead;
-
policy-as-code with familiar tooling such as
gitandkustomize; -
a steadily growing library of ready-made policies (244 at the time of writing).
The alternatives worth knowing about are OPA/Gatekeeper, Kubewarden and jsPolicy.
There is plenty of comparison material online — for example, this write-up compares Kyverno and Gatekeeper.
Why did we need it?
PodSecurityPolicy was deprecated in Kubernetes 1.21 and removed outright in 1.25.
That left us with the question of how to carry on. We tested several options and settled on Kyverno. It made us very happy and covered everything we needed from PSP.
Against the alternatives, Kyverno stood out for how much it does out of the box and for its simple vocabulary — ordinary YAML — which means new team members get productive with it far faster.
How does it work?
Kyverno runs as a dynamic admission controller in the Kubernetes cluster. It receives validating and mutating admission webhooks from kube-apiserver, then applies the matching policies and returns results that either enforce admission policy or reject the request.

What do we use it for?
Initially we planned to use Kyverno purely as a PodSecurityPolicy replacement. We started by rewriting our existing PSP manifests as Kyverno policy manifests with Validate rules.
Validate Resources
For instance, we wanted to forbid privileged containers straight away, because a privileged pod can reach host resources and kernel capabilities.
The policy we use
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged-containers
annotations:
policies.kyverno.io/title: Disallow Privileged Containers
policies.kyverno.io/category: Pod Security Standards (Baseline)
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Pod
kyverno.io/kyverno-version: 1.6.0
kyverno.io/kubernetes-version: "1.22-1.23"
policies.kyverno.io/description: >-
Privileged mode disables most security mechanisms and must not be allowed. This policy
ensures Pods do not call for privileged mode.
spec:
validationFailureAction: audit
background: true
rules:
- name: privileged-containers
match:
any:
- resources:
kinds:
- Pod
validate:
message: >-
Privileged mode is disallowed. The fields spec.containers[*].securityContext.privileged
and spec.initContainers[*].securityContext.privileged must be unset or set to `false`.
pattern:
spec:
=(ephemeralContainers):
- =(securityContext):
=(privileged): "false"
=(initContainers):
- =(securityContext):
=(privileged): "false"
containers:
- =(securityContext):
=(privileged): "false"
Mutating Resources
Next we decided to set the securityContext we want on every resource created in our cluster. Here is what we ended up with:
Policy that adds a securityContext to resources
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-default-securitycontext
annotations:
policies.kyverno.io/title: Add Default securityContext
policies.kyverno.io/category: Sample
policies.kyverno.io/subject: Pod
policies.kyverno.io/description: >-
A Pod securityContext entry defines fields such as the user and group which should be used to run the Pod.
Sometimes choosing default values for users rather than blocking is a better alternative to not impede
such Pod definitions. This policy will mutate a Pod to set default securityContext
spec:
background: false
rules:
- name: add-default-securitycontext-containers
match:
any:
- resources:
kinds:
- Pod
preconditions:
all:
- key: "{{request.operation}}"
operator: In
value:
- CREATE
- UPDATE
mutate:
foreach:
- list: "request.object.spec.containers"
patchStrategicMerge:
spec:
securityContext:
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: "{{ element.name }}"
securityContext:
runAsUser: 10001
runAsGroup: 10001
capabilities:
drop:
- ALL
runAsNonRoot: true
allowPrivilegeEscalation: false
privileged: false
seccompProfile:
type: RuntimeDefault
- name: add-default-securitycontext-initContainers
match:
any:
- resources:
kinds:
- Pod
preconditions:
all:
- key: "{{request.operation}}"
operator: In
value:
- CREATE
- UPDATE
- key: "{{ request.object.spec.initContainers[] || '' | length(@) }}"
operator: GreaterThanOrEquals
value: 1
mutate:
foreach:
- list: "request.object.spec.initContainers"
patchStrategicMerge:
spec:
securityContext:
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
initContainers:
- name: "{{ element.name }}"
securityContext:
runAsUser: 10001
runAsGroup: 10001
capabilities:
drop:
- ALL
runAsNonRoot: true
allowPrivilegeEscalation: false
privileged: false
seccompProfile:
type: RuntimeDefault
Generate Resources
The obvious use for generate policies is copying a private registry credentials secret (dockerconfigjson) into every namespace.
We found another use for it: copying the secret holding the self-signed root certificate that cert-manager generates into every namespace, so another policy can then inject it into containers. Note synchronize — it lets Kyverno automatically update the contents of every resource it created whenever the source changes. In our case, that means the certificate is refreshed in every secret after cert-manager reissues it.
Copying a secret across namespaces
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: clone-cert
annotations:
policies.kyverno.io/title: Clone Certificate's secret
policies.kyverno.io/category: Cert-Manager
policies.kyverno.io/subject: Certificate, Namespace
policies.kyverno.io/description: >-
Clone certificate's secret to every namespace
spec:
background: true
rules:
- name: clone-cert
match:
any:
- resources:
kinds:
- Namespace
exclude:
any:
- resources:
namespaces:
- "kube-system"
- "kube-public"
- "default"
generate:
synchronize: true
apiVersion: v1
kind: Secret
name: base-cert
namespace: "{{request.object.metadata.name}}"
clone:
namespace: cert-manager
name: base-cert
The policy that performs the injection
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-certificates-volume
annotations:
policies.kyverno.io/title: Add Certificates as a Volume
policies.kyverno.io/category: Sample
policies.kyverno.io/subject: Pod,Volume
kyverno.io/kyverno-version: 1.5.2
kyverno.io/kubernetes-version: "1.21"
policies.kyverno.io/minversion: 1.5.0
pod-policies.kyverno.io/autogen-controllers: DaemonSet,Deployment,Job,StatefulSet
policies.kyverno.io/description: >-
In some cases you would need to trust custom CA certificates for all the containers of a Pod.
It makes sense to be in a Secret so that you can automount them by only setting an annotation.
This policy adds a volume to all containers in a Pod containing the certificate if the annotation
called `inject-certs` with value `enabled` is found.
spec:
background: true
rules:
- name: add-ssl-certs
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- "kube-system"
- "kube-public"
- "default"
preconditions:
all:
- key: "{{request.operation}}"
operator: In
value:
- CREATE
- UPDATE
mutate:
foreach:
- list: "request.object.spec.containers"
patchStrategicMerge:
spec:
containers:
- name: "{{ element.name }}"
volumeMounts:
- name: ssl-certs
mountPath: /etc/ssl/certs
volumes:
- name: ssl-certs
secret:
secretName: base-cert
Verify Image
Kyverno together with Sigstore Cosign can also verify and sign container images. We are only just rolling these features out. A collection of articles on the topic is available in the k8s (in)security Telegram channel.
Conclusion
Kyverno is excellent because it lets us govern our clusters consistently, at the cluster level, without writing real code. Non-compliant resources can be blocked with an error message that actually reaches the user. Misconfigured resources can be fixed on the fly, and new resources can be created dynamically. Managing Kubernetes clusters with this tool has been a genuinely pleasant experience, and we will keep using it.