Securing Kubernetes Pods with OPA Gatekeeper
A pod spec can ask for a lot: privileged mode, host namespaces, host paths, root user, any image from anywhere. Kubernetes accepts all of it unless something in admission says no. This post shows how to use OPA Gatekeeper for that, and where it fits next to the built-in Pod Security Admission.
Start with Pod Security Admission
PodSecurityPolicy was removed in Kubernetes 1.25. Its built-in replacement is Pod Security Admission, which applies the Pod Security Standards (privileged, baseline, restricted) per namespace through labels:
kubectl label namespace my-app \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted
That covers privileged containers, host namespaces, host paths, capabilities, running as root and seccomp. Use it first. It is built in, fast and needs nothing to maintain.
Gatekeeper is for what PSA cannot express:
- Allowed image registries.
- Required CPU and memory limits.
- Required labels or annotations.
- Exceptions narrower than a whole namespace.
- Rules on objects other than pods (Ingress hosts, Service types and so on).
How Gatekeeper works
Gatekeeper runs as a validating admission webhook plus an audit controller. Policies come in two parts:
- A ConstraintTemplate holds the Rego logic and defines a new CRD kind.
- A Constraint is an instance of that kind: which objects it applies to, its parameters and its enforcement action.
The audit controller periodically checks existing objects against all constraints and writes violations into each constraint's status. That catches resources created before the policy existed.
Install
Pick a release from the Gatekeeper releases page and install it with Helm:
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper gatekeeper/gatekeeper \
--namespace gatekeeper-system --create-namespace
kubectl -n gatekeeper-system get pods
You should see the controller manager pods and the audit pod running.
A template: allowed image registries
The rego field in a ConstraintTemplate uses Rego v0 syntax. Gatekeeper 3.19 and later can also accept Rego v1 through the code field with source.version: "v1". The example below sticks to the classic form, which matches the Gatekeeper policy library.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input_containers[_]
not image_allowed(container.image)
msg := sprintf("container <%v> uses image <%v>, which is not from an allowed registry", [container.name, container.image])
}
image_allowed(image) {
startswith(image, input.parameters.repos[_])
}
input_containers[c] {
c := input.review.object.spec.containers[_]
}
input_containers[c] {
c := input.review.object.spec.initContainers[_]
}
input_containers[c] {
c := input.review.object.spec.ephemeralContainers[_]
}
Two details matter here. The rule checks init and ephemeral containers, not only containers, because those are the obvious way around a check that looks at containers only. And the "is it allowed" test is a separate rule: the image is allowed if it matches any prefix. Writing not startswith(image, repos[_]) inline means "not matching some prefix", which flags every image as soon as there is more than one allowed registry.
A constraint in dry run
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-registries
spec:
enforcementAction: dryrun
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
parameters:
repos:
- "registry.example.com/"
enforcementAction accepts deny (the default), dryrun and warn. With dryrun, nothing is blocked and the audit controller records what would be. End each registry prefix with /, otherwise registry.example.com also matches registry.example.com.attacker.net/image.
Read audit results
kubectl get k8sallowedrepos allowed-registries -o yaml
Look at status.totalViolations and status.violations. The list is capped (20 entries per constraint by default, configurable with --constraint-violations-limit), so use totalViolations for the real count and the Gatekeeper metrics for trends. Fix the violations or agree on exceptions, then switch to warn and finally deny.
Test policies before they reach a cluster
The gator CLI evaluates templates and constraints offline.
# check manifests against templates and constraints
gator test -f manifests/ -f policies/
# run a suite of expected results
gator verify policies/...
A suite file lists cases with expected outcomes:
kind: Suite
apiVersion: test.gatekeeper.sh/v1alpha1
tests:
- name: allowed-repos
template: k8sallowedrepos-template.yaml
constraint: allowed-registries.yaml
cases:
- name: internal-image
object: samples/pod-internal.yaml
assertions:
- violations: no
- name: docker-hub-image
object: samples/pod-dockerhub.yaml
assertions:
- violations: yes
Run gator verify in the pipeline of the policy repository, so a broken template never reaches a cluster.
Use the library
Before writing Rego, check the Gatekeeper policy library. It has tested templates for allowed repositories, required labels, container limits, and a set that maps the old PodSecurityPolicy controls one by one. Those are useful if you need finer exceptions than PSA namespace labels allow.
Rules that match only pods
A constraint on Pod rejects pods created by a Deployment controller, not the Deployment. kubectl apply succeeds and the error appears in the ReplicaSet's events. Gatekeeper's expansion feature (ExpansionTemplate) can validate the pod template inside workloads at admission time, so the error reaches the person who applied the manifest.
Operational notes
- Gatekeeper is in the admission path. Run several replicas, watch webhook latency and decide on its failure policy deliberately.
- Keep constraints in Git and deploy them like any other code.
- Every constraint needs an owner and a reason; delete the ones nobody can explain.
Checklist
- Pod Security Admission on every namespace first.
- Gatekeeper only for rules PSA cannot express.
- Templates check containers, init containers and ephemeral containers.
- New constraints start with
dryrun, thenwarn, thendeny. - Audit results reviewed before enforcement.
gator verifyin CI for every policy change.
