OPA Gatekeeper: Policy as Code for Kubernetes
Kubernetes lets you deploy anything. That’s powerful - and dangerous. OPA Gatekeeper acts as a policy checkpoint, validating resources before they’re admitted to the cluster.
This guide covers Gatekeeper installation, constraint templates, and practical policies for production clusters.
TL;DR
- OPA = Open Policy Agent, general-purpose policy engine
- Gatekeeper = OPA integration for Kubernetes admission control
- Constraint Templates = reusable policy definitions (Rego)
- Constraints = instantiated policies applied to resources
- Full examples for security, compliance, and best practices
What is OPA Gatekeeper?
Gatekeeper runs as a validating admission webhook. Every create, update, or delete request passes through it for policy evaluation.
┌──────────┐ ┌──────────────┐ ┌────────────────┐ ┌──────────┐
│ kubectl │────▶│ API Server │────▶│ Gatekeeper │────▶│ etcd │
│ apply │ │ │ │ (Admission) │ │ │
└──────────┘ └──────────────┘ └────────────────┘ └──────────┘
│
▼
┌──────────────┐
│ Constraint │
│ Evaluation │
│ (Rego) │
└──────────────┘
│
┌─────┴─────┐
▼ ▼
ALLOWED DENIED
(with reason)
OPA vs Gatekeeper
COMPONENT PURPOSE LANGUAGE
========= ======= ========
OPA General policy engine Rego
Gatekeeper K8s admission controller Rego (via templates)
Conftest CLI policy testing Rego
Install Gatekeeper
# Using Helm
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm upgrade --install gatekeeper gatekeeper/gatekeeper \
--namespace gatekeeper-system --create-namespace \
--set replicas=3 \
--set audit.replicas=1
# Verify
kubectl get pods -n gatekeeper-system
Helm Values for Production
# gatekeeper-values.yaml
replicas: 3
audit:
replicas: 1
# How often to audit existing resources
auditInterval: 60
# Maximum violations to report per constraint
constraintViolationsLimit: 20
# Resource limits
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
# Exempt namespaces from all policies
exemptNamespaces:
- kube-system
- gatekeeper-system
# Emit events for violations
emitAdmissionEvents: true
emitAuditEvents: true
# Mutation support (optional)
mutatingWebhook: enabled
Constraint Templates and Constraints
Gatekeeper uses two resources:
- ConstraintTemplate: Defines the policy logic (Rego)
- Constraint: Applies the template with specific parameters
Example: Required Labels
# template-required-labels.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
---
# constraint-required-labels.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-team-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaceSelector:
matchExpressions:
- key: gatekeeper.sh/exempt
operator: DoesNotExist
parameters:
labels:
- "team"
- "app"
Security Policies
Block Privileged Containers
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spsprivilegedcontainer
spec:
crd:
spec:
names:
kind: K8sPSPPrivilegedContainer
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spsprivilegedcontainer
violation[{"msg": msg}] {
c := input_containers[_]
c.securityContext.privileged == true
msg := sprintf("Privileged container not allowed: %v", [c.name])
}
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[_]
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
name: block-privileged
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
Block Host Namespace
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spsphostnamespace
spec:
crd:
spec:
names:
kind: K8sPSPHostNamespace
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spsphostnamespace
violation[{"msg": msg}] {
input.review.object.spec.hostNetwork == true
msg := "hostNetwork is not allowed"
}
violation[{"msg": msg}] {
input.review.object.spec.hostPID == true
msg := "hostPID is not allowed"
}
violation[{"msg": msg}] {
input.review.object.spec.hostIPC == true
msg := "hostIPC is not allowed"
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPHostNamespace
metadata:
name: block-host-namespace
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
Read-Only Root Filesystem
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sreadonlyrootfilesystem
spec:
crd:
spec:
names:
kind: K8sReadOnlyRootFilesystem
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sreadonlyrootfilesystem
violation[{"msg": msg}] {
c := input_containers[_]
not c.securityContext.readOnlyRootFilesystem == true
msg := sprintf("Container %v must have readOnlyRootFilesystem: true", [c.name])
}
input_containers[c] {
c := input.review.object.spec.containers[_]
}
input_containers[c] {
c := input.review.object.spec.initContainers[_]
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sReadOnlyRootFilesystem
metadata:
name: require-readonly-root
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
Resource Policies
Require Resource Limits
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sresourcelimits
spec:
crd:
spec:
names:
kind: K8sResourceLimits
validation:
openAPIV3Schema:
type: object
properties:
cpu:
type: string
memory:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sresourcelimits
violation[{"msg": msg}] {
c := input_containers[_]
not c.resources.limits.cpu
msg := sprintf("Container %v must have CPU limits", [c.name])
}
violation[{"msg": msg}] {
c := input_containers[_]
not c.resources.limits.memory
msg := sprintf("Container %v must have memory limits", [c.name])
}
violation[{"msg": msg}] {
c := input_containers[_]
not c.resources.requests.cpu
msg := sprintf("Container %v must have CPU requests", [c.name])
}
violation[{"msg": msg}] {
c := input_containers[_]
not c.resources.requests.memory
msg := sprintf("Container %v must have memory requests", [c.name])
}
input_containers[c] {
c := input.review.object.spec.containers[_]
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sResourceLimits
metadata:
name: require-limits
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- monitoring
Block Latest Tag
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sdisallowedtags
spec:
crd:
spec:
names:
kind: K8sDisallowedTags
validation:
openAPIV3Schema:
type: object
properties:
tags:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sdisallowedtags
violation[{"msg": msg}] {
c := input_containers[_]
tag := get_tag(c.image)
tag == input.parameters.tags[_]
msg := sprintf("Container %v uses disallowed tag: %v", [c.name, tag])
}
violation[{"msg": msg}] {
c := input_containers[_]
not contains(c.image, ":")
msg := sprintf("Container %v has no tag (implies latest)", [c.name])
}
get_tag(image) = tag {
contains(image, ":")
parts := split(image, ":")
tag := parts[count(parts) - 1]
}
input_containers[c] {
c := input.review.object.spec.containers[_]
}
input_containers[c] {
c := input.review.object.spec.initContainers[_]
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowedTags
metadata:
name: block-latest-tag
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
tags:
- "latest"
Registry Restrictions
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}] {
c := input_containers[_]
not image_allowed(c.image)
msg := sprintf("Container %v uses non-approved registry: %v", [c.name, c.image])
}
image_allowed(image) {
repo := input.parameters.repos[_]
startswith(image, repo)
}
input_containers[c] {
c := input.review.object.spec.containers[_]
}
input_containers[c] {
c := input.review.object.spec.initContainers[_]
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: approved-registries
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "gcr.io/company-project/"
- "ghcr.io/company/"
- "docker.io/library/"
Audit Existing Resources
Gatekeeper audits existing resources and reports violations:
# List all violations
kubectl get constraints -o json | jq '.items[].status.violations'
# Get violations for specific constraint
kubectl get k8srequiredlabels require-team-label -o yaml
# Example output:
# status:
# violations:
# - enforcementAction: deny
# kind: Deployment
# name: my-app
# namespace: default
# message: "Missing required labels: {team}"
Dry Run Mode
Test policies before enforcing:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-team-label-dryrun
spec:
enforcementAction: dryrun # warn | deny | dryrun
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
labels:
- "team"
Mutation Policies
Gatekeeper can also mutate resources (add defaults):
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: add-default-securitycontext
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
excludedNamespaces:
- kube-system
location: "spec.securityContext"
parameters:
assign:
value:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
---
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: add-default-resource-requests
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
location: "spec.containers[name:*].resources.requests.memory"
parameters:
assign:
value: "64Mi"
pathTests:
- subPath: "spec.containers[name:*].resources.requests.memory"
condition: MustNotExist
Testing Policies
Use Gator CLI or Conftest to test policies:
# Install gator
go install github.com/open-policy-agent/gatekeeper/cmd/gator@latest
# Test constraint template
gator test -f template.yaml -f constraint.yaml -f test-resources/
# Or use conftest
conftest test deployment.yaml -p policies/
Troubleshooting
Constraint not enforcing:
# Check constraint status
kubectl get constraint <name> -o yaml
# Check webhook config
kubectl get validatingwebhookconfiguration gatekeeper-validating-webhook-configuration
# Check gatekeeper logs
kubectl logs -n gatekeeper-system -l control-plane=controller-manager
Audit not running:
# Check audit controller
kubectl logs -n gatekeeper-system -l control-plane=audit-controller
# Verify audit interval
kubectl get deploy -n gatekeeper-system gatekeeper-audit -o yaml | grep -A5 audit
References
- OPA Docs: https://www.openpolicyagent.org/docs
- Gatekeeper Docs: https://open-policy-agent.github.io/gatekeeper
- Rego Playground: https://play.openpolicyagent.org
- Policy Library: https://github.com/open-policy-agent/gatekeeper-library