Pod Security Policies are a deprecated Kubernetes feature, but Rancher v2.x brought its own implementation of this concept, often referred to as "Pod Security Policies v2" or "Rancher Policies," which uses the Gatekeeper admission controller. These policies are designed to enforce security standards on pods running within your Kubernetes clusters, preventing misconfigurations that could lead to security vulnerabilities.
Let’s see a simple policy in action. Imagine we want to ensure all pods are running as non-root users.
Here’s a basic Gatekeeper constraint template that defines what we’re looking for:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedusers
annotations:
description: "Restrict running as root user."
spec:
crd:
spec:
names:
kind: K8sRequiredNoRoot
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequirednoroot
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := "container " + container.name + " must run as non-root user"
}
violation[{"msg": msg}] {
init_container := input.review.object.spec.initContainers[_]
not init_container.securityContext.runAsNonRoot
msg := "init container " + init_container.name + " must run as non-root user"
}
violation[{"msg": msg}] {
ephemeral_container := input.review.object.spec.ephemeralContainers[_]
not ephemeral_container.securityContext.runAsNonRoot
msg := "ephemeral container " + ephemeral_container.name + " must run as non-root user"
}
This template, k8sallowedusers, uses Rego, Gatekeeper’s policy language, to define the logic. It checks each container (including init and ephemeral containers) within a pod’s specification. If runAsNonRoot is not set to true in the securityContext, it triggers a violation.
Now, we create a constraint that uses this template. This is where we actually enforce the policy.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredNoRoot
metadata:
name: require-non-root
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pods"]
excludedNamespaces: ["kube-system", "rancher-system"]
This constraint, require-non-root, of kind K8sRequiredNoRoot (which matches our template’s kind), tells Gatekeeper to apply the logic defined in the k8sallowedusers template to all pods, except those in the kube-system and rancher-system namespaces.
When you try to deploy a pod that violates this rule, Gatekeeper, acting as an admission controller, will intercept the request and deny it. For example, if you tried to create a deployment with a container that doesn’t specify runAsNonRoot: true in its security context, you’d see an error like this:
Error from server (Forbidden): error when creating "my-pod.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [require-non-root] container my-container must run as non-root user
The beauty of this system is its flexibility. You can define templates for a vast array of security checks, from restricting host mounts and privileged containers to enforcing specific image registries or requiring resource limits. The constraints then selectively apply these templates across your cluster, allowing for granular control.
The core problem these policies solve is the inherent trust in Kubernetes’ default behavior. By default, Kubernetes is quite permissive. It allows pods to run as root, access host network interfaces, mount sensitive host paths, and more, unless explicitly restricted. This permissive nature is great for development and ease of use but can be a significant security risk in production environments. Rancher’s implementation, leveraging Gatekeeper, provides a robust mechanism to shift from this permissive default to a secure-by-default posture. You define what is allowed, and anything else is rejected.
A common point of confusion is the distinction between a ConstraintTemplate and a Constraint. The template is the schema for a policy – it defines the Rego logic and the parameters it accepts. The constraint is the instance of that policy, where you specify which resources (kinds, namespaces, labels) the template should be applied to. You can have multiple constraints based on a single template, each with different matching rules.
What many users don’t immediately grasp is how match and exclude clauses in constraints interact with the Rego logic. The match and exclude directives in the constraint are evaluated before the Rego code. If a resource doesn’t match the match criteria or is excluded, the Rego code for that constraint simply won’t run for that resource, regardless of its content. This can sometimes lead to unexpected behavior if you’re expecting a policy to trigger but it’s being silently bypassed by a broad exclude rule.
The next step after mastering these basic enforcement policies is exploring how to integrate them with CI/CD pipelines, ensuring that security checks are performed before code even reaches your cluster.