Why OPA Gatekeeper
Open Policy Agent (OPA) Gatekeeper is a Kubernetes admission controller that uses Rego — a purpose-built policy language — to define and enforce policies. If your organization already uses OPA for API authorization, Terraform policy checks, or microservice authorization, Gatekeeper gives you a unified policy language across your entire stack.
For Kyverno policies (YAML-native, no Rego required), see: Kyverno Policies
OPA Gatekeeper has several key strengths. Rego is expressive and handles complex conditional logic, external data, and aggregation. It provides a shared language across Kubernetes, Terraform (Sentinel/Conftest), CI/CD, and APIs. The ConstraintTemplate pattern separates policy logic from parameters. Dry-run mode and audit capabilities are built-in. There is a large community library (gatekeeper-library) available for reference implementations.
Installing Gatekeeper
# Install Gatekeeper via Helmhelm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/chartshelm repo update helm install gatekeeper gatekeeper/gatekeeper \ --namespace gatekeeper-system \ --create-namespace \ --set replicas=3 \ --set audit.replicas=1 \ --set audit.logLevel=INFO # Verify installationkubectl get pods -n gatekeeper-system# Expected: 3 gatekeeper-controller pods + 1 gatekeeper-audit pod # Verify webhookkubectl get validatingwebhookconfigurations | grep gatekeeperHow Gatekeeper Works: Templates + Constraints
Gatekeeper uses a two-layer pattern to separate policy definition from configuration:
- ConstraintTemplate — Defines the reusable policy logic in Rego
- Constraint — Instantiates the template with specific parameters
The template serves as the rule engine, while the constraint provides the configuration. For example, a ConstraintTemplate called "Allowed Registries" contains the Rego logic to check whether an image registry is in the allowed list. You then create individual constraints from this template, such as "CleanStart Only" (which allows only registry.cleanstart.com) or "CleanStart + Istio" (which allows both registry.cleanstart.com and docker.io/istio). This separation allows you to reuse the same policy logic across multiple configurations.
Policy 1: Require CleanStart Registry
ConstraintTemplate
# template-allowed-registries.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8sallowedregistries annotations: description: Restricts container images to approved registriesspec: crd: spec: names: kind: K8sAllowedRegistries validation: openAPIV3Schema: type: object properties: registries: type: array description: List of allowed container image registries items: type: string exemptImages: type: array description: Images exempt from registry check (for sidecars) items: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8sallowedregistries violation[{"msg": msg}] { container := input_containers[_] not is_exempt(container.image) not registry_allowed(container.image) msg := sprintf( "Container '%s' uses image '%s' which is not from an approved registry. Approved registries: %v", [container.name, container.image, input.parameters.registries] ) } registry_allowed(image) { registry := input.parameters.registries[_] startswith(image, registry) } is_exempt(image) { exempt := input.parameters.exemptImages[_] startswith(image, exempt) } 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[_] }Constraint
# constraint-cleanstart-registry.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sAllowedRegistriesmetadata: name: require-cleanstart-registryspec: enforcementAction: deny # deny | dryrun | warn match: kinds: - apiGroups: [""] kinds: ["Pod"] excludedNamespaces: - kube-system - gatekeeper-system - monitoring parameters: registries: - "registry.cleanstart.com/" exemptImages: # Add sidecars and infrastructure images here # - "docker.io/istio/" # - "gcr.io/datadoghq/"Apply and test:
kubectl apply -f template-allowed-registries.yamlkubectl apply -f constraint-cleanstart-registry.yaml # Wait for template to syncsleep 5 # Test: CleanStart image (should SUCCEED)kubectl run test-pass \ --image=registry.cleanstart.com/python3:3.12.5-prod \ --dry-run=server# Output: pod/test-pass created (server dry run) # Test: Docker Hub image (should FAIL)kubectl run test-fail --image=nginx:latest --dry-run=server# Output: Error: admission webhook "validation.gatekeeper.sh" denied the request:# Container 'test-fail' uses image 'nginx:latest' which is not from an approved registry.Policy 2: Enforce Pod Security Standards
ConstraintTemplate
# template-cleanstart-pod-security.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8scleanstartpodsecurity annotations: description: Enforces CleanStart pod security requirementsspec: crd: spec: names: kind: K8sCleanStartPodSecurity validation: openAPIV3Schema: type: object properties: allowedRunAsUser: type: integer description: Required UID for containers (default 65532) requireReadOnlyRootFilesystem: type: boolean description: Whether read-only root FS is required requireDropAllCapabilities: type: boolean description: Whether dropping ALL capabilities is required targets: - target: admission.k8s.gatekeeper.sh rego: | package k8scleanstartpodsecurity # Rule 1: Must run as non-root violation[{"msg": msg}] { container := input_containers[_] not has_field(container, "securityContext") msg := sprintf("Container '%s' must define securityContext with runAsNonRoot: true", [container.name]) } violation[{"msg": msg}] { container := input_containers[_] container.securityContext.runAsNonRoot != true msg := sprintf("Container '%s' must set securityContext.runAsNonRoot to true", [container.name]) } # Rule 2: Read-only root filesystem violation[{"msg": msg}] { input.parameters.requireReadOnlyRootFilesystem == true container := input_containers[_] not container.securityContext.readOnlyRootFilesystem msg := sprintf( "Container '%s' must set readOnlyRootFilesystem: true. Use emptyDir volumes for writable paths (/tmp, /var/run, etc.)", [container.name] ) } # Rule 3: No privilege escalation violation[{"msg": msg}] { container := input_containers[_] container.securityContext.allowPrivilegeEscalation == true msg := sprintf("Container '%s' must not allow privilege escalation", [container.name]) } # Rule 4: No privileged containers violation[{"msg": msg}] { container := input_containers[_] container.securityContext.privileged == true msg := sprintf("Container '%s' must not run as privileged", [container.name]) } # Rule 5: Drop ALL capabilities violation[{"msg": msg}] { input.parameters.requireDropAllCapabilities == true container := input_containers[_] not has_drop_all(container) msg := sprintf( "Container '%s' must drop ALL capabilities. Set securityContext.capabilities.drop: [\"ALL\"]", [container.name] ) } # Rule 6: No hostPath volumes violation[{"msg": msg}] { volume := input.review.object.spec.volumes[_] has_field(volume, "hostPath") msg := sprintf("Volume '%s' uses hostPath which is not allowed", [volume.name]) } # Rule 7: No host networking violation[{"msg": msg}] { input.review.object.spec.hostNetwork == true msg := "Host networking is not allowed" } violation[{"msg": msg}] { input.review.object.spec.hostPID == true msg := "Host PID namespace sharing is not allowed" } # Helper functions has_drop_all(container) { caps := container.securityContext.capabilities.drop caps[_] == "ALL" } has_field(obj, field) { _ = obj[field] } input_containers[c] { c := input.review.object.spec.containers[_] } input_containers[c] { c := input.review.object.spec.initContainers[_] }Constraint
# constraint-cleanstart-pod-security.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sCleanStartPodSecuritymetadata: name: enforce-cleanstart-pod-securityspec: enforcementAction: deny match: kinds: - apiGroups: [""] kinds: ["Pod"] excludedNamespaces: - kube-system - gatekeeper-system parameters: allowedRunAsUser: 65532 requireReadOnlyRootFilesystem: true requireDropAllCapabilities: trueTest:
kubectl apply -f template-cleanstart-pod-security.yamlkubectl apply -f constraint-cleanstart-pod-security.yaml # Compliant pod (should SUCCEED)cat <<'EOF' | kubectl apply --dry-run=server -f -apiVersion: v1kind: Podmetadata: name: secure-podspec: securityContext: runAsNonRoot: true runAsUser: 65532 containers: - name: app image: registry.cleanstart.com/python3:3.12.5-prod securityContext: runAsNonRoot: true readOnlyRootFilesystem: true allowPrivilegeEscalation: false privileged: false capabilities: drop: ["ALL"] volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: {}EOF # Non-compliant pod (should FAIL with multiple violations)cat <<'EOF' | kubectl apply --dry-run=server -f -apiVersion: v1kind: Podmetadata: name: insecure-podspec: hostNetwork: true containers: - name: app image: registry.cleanstart.com/python3:3.12.5-prod securityContext: privileged: true allowPrivilegeEscalation: true volumes: - name: host hostPath: path: /etcEOF# Output: multiple violations (privileged, hostNetwork, hostPath, no drop ALL, etc.)Policy 3: Block Latest and Mutable Tags
# template-block-latest-tag.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8sblocklatestagspec: crd: spec: names: kind: K8sBlockLatestTag validation: openAPIV3Schema: type: object properties: blockedTags: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8sblocklatestag violation[{"msg": msg}] { container := input_containers[_] tag := get_tag(container.image) blocked := input.parameters.blockedTags[_] tag == blocked msg := sprintf( "Container '%s' uses blocked tag ':%s'. Use a specific version tag (e.g., :3.12.5-prod) or digest (@sha256:...).", [container.name, tag] ) } violation[{"msg": msg}] { container := input_containers[_] not contains(container.image, ":") not contains(container.image, "@") msg := sprintf( "Container '%s' uses image '%s' without a tag or digest. Specify a version tag or digest.", [container.name, container.image] ) } get_tag(image) = tag { contains(image, ":") not 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[_] }---# constraint-block-latest-tag.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sBlockLatestTagmetadata: name: block-mutable-tagsspec: enforcementAction: deny match: kinds: - apiGroups: [""] kinds: ["Pod"] parameters: blockedTags: - "latest" - "dev" - "nightly" - "edge"Policy 4: Require Resource Limits
# template-require-resources.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8srequireresourcesspec: crd: spec: names: kind: K8sRequireResources validation: openAPIV3Schema: type: object properties: maxMemoryLimit: type: string description: Maximum memory limit allowed maxCpuLimit: type: string description: Maximum CPU limit allowed targets: - target: admission.k8s.gatekeeper.sh rego: | package k8srequireresources violation[{"msg": msg}] { container := input_containers[_] not has_resources(container) msg := sprintf( "Container '%s' must define resource requests and limits. CleanStart images are minimal — start with cpu: 100m/500m and memory: 128Mi/512Mi.", [container.name] ) } violation[{"msg": msg}] { container := input_containers[_] has_resources(container) not container.resources.requests.cpu msg := sprintf("Container '%s' must define resources.requests.cpu", [container.name]) } violation[{"msg": msg}] { container := input_containers[_] has_resources(container) not container.resources.requests.memory msg := sprintf("Container '%s' must define resources.requests.memory", [container.name]) } violation[{"msg": msg}] { container := input_containers[_] has_resources(container) not container.resources.limits.memory msg := sprintf("Container '%s' must define resources.limits.memory", [container.name]) } has_resources(container) { _ = container.resources } input_containers[c] { c := input.review.object.spec.containers[_] }---# constraint-require-resources.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequireResourcesmetadata: name: require-resource-definitionsspec: enforcementAction: deny match: kinds: - apiGroups: [""] kinds: ["Pod"] excludedNamespaces: - kube-system - gatekeeper-systemPolicy 5: Require Image Digest in Production
# template-require-digest.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8srequiredigestspec: crd: spec: names: kind: K8sRequireDigest targets: - target: admission.k8s.gatekeeper.sh rego: | package k8srequiredigest violation[{"msg": msg}] { container := input_containers[_] not contains(container.image, "@sha256:") msg := sprintf( "Container '%s' uses image '%s' without a digest. In production, use digest references (e.g., registry.cleanstart.com/python3@sha256:abc...). Get digest: crane digest %s", [container.name, container.image, container.image] ) } input_containers[c] { c := input.review.object.spec.containers[_] } input_containers[c] { c := input.review.object.spec.initContainers[_] }---# constraint-require-digest-production.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequireDigestmetadata: name: require-digest-in-productionspec: enforcementAction: deny match: kinds: - apiGroups: [""] kinds: ["Pod"] namespaces: - production - prod-*Auditing and Reporting
View All Violations
# List all constraints and their violation countskubectl get constraints -o wide # Example output:# NAME ENFORCEMENT-ACTION TOTAL-VIOLATIONS# require-cleanstart-registry deny 3# enforce-cleanstart-pod-security deny 12# block-mutable-tags deny 1 # Get details on specific violationskubectl get K8sAllowedRegistries require-cleanstart-registry -o yaml # The status section shows every violation:# status:# violations:# - enforcementAction: deny# kind: Pod# name: legacy-app# namespace: default# message: "Container 'legacy' uses image 'nginx:latest'..."Audit Existing Resources
Gatekeeper automatically audits existing resources against new policies:
# Check audit resultskubectl get constraints -o json | \ jq '.items[] | {name: .metadata.name, violations: (.status.violations // [] | length)}' # Output:# {"name": "require-cleanstart-registry", "violations": 3}# {"name": "enforce-cleanstart-pod-security", "violations": 12}# {"name": "block-mutable-tags", "violations": 1}Export Violations for Compliance
# Export all violations as JSON (for SIEM, compliance dashboards)kubectl get constraints -o json | \ jq '[.items[] | .status.violations[]? | { policy: .constraintRef.name, kind: .kind, namespace: .namespace, name: .name, message: .message }]' > violations-report.json # Count violations by namespacekubectl get constraints -o json | \ jq '[.items[].status.violations[]?.namespace] | group_by(.) | map({namespace: .[0], count: length})'Rollout Strategy
Phase 1: Observe (Week 1)
Deploy all policies in dryrun mode:
# All constraints with:spec: enforcementAction: dryrun # Monitor violations without blocking anythingkubectl get constraints -o wide# Fix what you can, document what needs exceptionsPhase 2: Warn (Week 2)
Switch to warn mode — kubectl shows warnings but allows the request:
spec: enforcementAction: warn # Users see: Warning: [require-cleanstart-registry] Container uses non-approved registry# But the pod is still createdPhase 3: Enforce (Week 3+)
Switch to deny mode for each policy individually:
# Start with least disruptivespec: enforcementAction: deny # Order of enforcement:# 1. block-mutable-tags (low disruption — teams should already be tagging)# 2. require-resource-definitions (medium — add defaults to existing manifests)# 3. enforce-cleanstart-pod-security (medium — may need securityContext additions)# 4. require-cleanstart-registry (high — blocks non-CleanStart images entirely)# 5. require-digest-in-production (high — requires workflow changes)Gatekeeper vs Kyverno: Quick Comparison
Feature | OPA Gatekeeper | Kyverno |
|---|---|---|
Policy language | Rego (custom) | YAML (Kubernetes-native) |
Learning curve | Steep (Rego) | Low (YAML) |
Image verification | Requires external webhook | Built-in Cosign support |
Mutation policies | Limited (assign/modify) | Full strategic merge patch |
Audit existing resources | Built-in | Built-in (policy reports) |
Community policies | gatekeeper-library (100+) | kyverno-policies (200+) |
Cross-stack reuse | Yes (Terraform, APIs, etc.) | Kubernetes only |
Performance at scale | Excellent (compiled Rego) | Good |
Policy testing |
|
|
Use Kyverno if you're Kubernetes-only and want the fastest path to enforcement. Use Gatekeeper if you need Rego across multiple systems or have complex policy logic.
What to Read Next
Kyverno Policies — YAML-native alternative to Gatekeeper. Image Signing with Sigstore — How CleanStart signs images. CIS Hardening — CIS controls that these policies enforce. Network Policies — Network-level enforcement. Runtime Evidence: Falco — Runtime policy enforcement.
