Back to Distributed Systems & Kubernetes Series

OPA/Gatekeeper Track Part 2: Gatekeeper & Constraints

June 6, 2026 Wasil Zafar 35 min read

OPA Gatekeeper is the Kubernetes-native distribution of Open Policy Agent. It installs as a validating admission webhook and introduces two CRDs — ConstraintTemplate (defines policy logic in Rego) and Constraint (applies that logic to specific resources). This two-layer model separates policy authors from policy consumers.

Table of Contents

  1. Install Gatekeeper
  2. ConstraintTemplates
  3. Constraints
  4. Policy Library
  5. Exercises
  6. Key Takeaways

Install Gatekeeper

Gatekeeper runs as a set of Pods in the gatekeeper-system namespace. It registers a ValidatingAdmissionWebhook so the API server sends every CREATE/UPDATE request to Gatekeeper before persisting it to etcd.

Helm Installation

# Add the Gatekeeper Helm chart repository
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update

# Install Gatekeeper into its own namespace
helm install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system \
  --create-namespace \
  --set replicas=3 \
  --set auditInterval=60

# Verify Pods are running
kubectl get pods -n gatekeeper-system
# NAME                                            READY   STATUS    RESTARTS   AGE
# gatekeeper-audit-6b9f8d7c5b-xk2lm              1/1     Running   0          45s
# gatekeeper-controller-manager-7f8c4d9b6-abc12   1/1     Running   0          45s
# gatekeeper-controller-manager-7f8c4d9b6-def34   1/1     Running   0          45s
# gatekeeper-controller-manager-7f8c4d9b6-ghi56   1/1     Running   0          45s

Verify the Webhook

# Confirm the ValidatingWebhookConfiguration exists
kubectl get validatingwebhookconfiguration
# NAME                                      WEBHOOKS   AGE
# gatekeeper-validating-webhook-configuration   2      1m

# Inspect the webhook — it targets CREATE and UPDATE operations
kubectl get validatingwebhookconfiguration \
  gatekeeper-validating-webhook-configuration -o yaml | \
  grep -A5 "rules:"
# - apiGroups: ["*"]
#   apiVersions: ["*"]
#   operations: ["CREATE", "UPDATE"]
#   resources: ["*"]
Namespace Exemptions: By default, Gatekeeper exempts kube-system and gatekeeper-system from policy enforcement to prevent lockout. You can configure additional exemptions via the Helm chart's exemptNamespaces value.

ConstraintTemplates

A ConstraintTemplate is a CRD that defines:

  1. The schema of parameters that Constraints can provide (via OpenAPI v3 schema)
  2. The Rego policy that evaluates admission requests using those parameters

Think of a ConstraintTemplate as a "policy class" and a Constraint as an "instance" of that class with specific configuration.

Template: Require Labels

This template enforces that specified labels exist on resources:

# constraint-template-require-labels.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        # Schema for the 'parameters' field in Constraint instances
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              description: "List of required label keys"
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        # Violation is populated when a required label is missing
        violation[{"msg": msg}] {
          # Get the set of provided label keys
          provided := {label | input.review.object.metadata.labels[label]}

          # Get the set of required label keys from parameters
          required := {label | label := input.parameters.labels[_]}

          # Find missing labels
          missing := required - provided

          # If any are missing, generate a violation message
          count(missing) > 0
          msg := sprintf("Resource %v is missing required labels: %v", [
            input.review.object.metadata.name,
            missing
          ])
        }
Key Pattern: Gatekeeper expects the Rego to define a violation rule (not deny like standalone OPA). Each element in the violation set must be an object with a "msg" key. Gatekeeper collects all violation messages and returns them to the user.

Template: Allowed Registries

This template restricts which container image registries are permitted:

# constraint-template-allowed-registries.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedregistries
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRegistries
      validation:
        openAPIV3Schema:
          type: object
          properties:
            registries:
              type: array
              description: "List of allowed container registry prefixes"
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedregistries

        violation[{"msg": msg}] {
          # Iterate over all containers (regular + init)
          container := input_containers[_]
          
          # Check if the image starts with any allowed registry
          not registry_allowed(container.image)
          
          msg := sprintf("Container '%v' uses image '%v' which is not from an allowed registry. Allowed: %v", [
            container.name,
            container.image,
            input.parameters.registries
          ])
        }

        # Helper: collect all containers (containers + initContainers)
        input_containers[c] {
          c := input.review.object.spec.containers[_]
        }
        input_containers[c] {
          c := input.review.object.spec.initContainers[_]
        }

        # Helper: check if image starts with an allowed prefix
        registry_allowed(image) {
          allowed := input.parameters.registries[_]
          startswith(image, allowed)
        }
# Apply the ConstraintTemplates
kubectl apply -f constraint-template-require-labels.yaml
kubectl apply -f constraint-template-allowed-registries.yaml

# Verify they are installed (STATUS should be "created: true")
kubectl get constrainttemplates
# NAME                    AGE
# k8sallowedregistries    5s
# k8srequiredlabels       10s

# Check template status for errors
kubectl get constrainttemplate k8srequiredlabels -o jsonpath='{.status}'

Constraints

A Constraint is an instance of a ConstraintTemplate. It specifies:

  • Parameters — values passed to the Rego policy (e.g., which labels are required)
  • Match — which resources and namespaces the policy applies to
  • EnforcementActiondeny (block), dryrun (audit only), or warn (allow but warn)

Parameters & Match

# constraint-require-team-label.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
      - apiGroups: ["apps"]
        kinds: ["Deployment", "StatefulSet"]
  parameters:
    labels:
      - "team"
      - "cost-center"
# constraint-allowed-registries.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
  name: prod-allowed-registries
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
      - apiGroups: ["apps"]
        kinds: ["Deployment", "StatefulSet", "DaemonSet"]
  parameters:
    registries:
      - "gcr.io/my-org/"
      - "docker.io/library/"
      - "registry.k8s.io/"
# Apply the Constraints
kubectl apply -f constraint-require-team-label.yaml
kubectl apply -f constraint-allowed-registries.yaml

# Test: create a Deployment missing the "team" label
kubectl create deployment nginx-test --image=nginx -n default
# Error from server (Forbidden): admission webhook "validation.gatekeeper.sh"
# denied the request: [require-team-label] Resource nginx-test is missing
# required labels: {"cost-center", "team"}

# Test: create a Pod from an unauthorized registry
cat <

Namespace Scoping

The match block supports fine-grained scoping with namespaces, excludedNamespaces, and label selectors:

# constraint-registries-prod-only.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
  name: registries-prod-namespaces
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]
    # Only enforce in these namespaces
    namespaces:
      - "production"
      - "staging"
    # Never enforce in these namespaces (takes precedence)
    excludedNamespaces:
      - "kube-system"
      - "gatekeeper-system"
    # Optional: only match resources with specific labels
    labelSelector:
      matchExpressions:
        - key: "environment"
          operator: "In"
          values: ["prod", "staging"]
  parameters:
    registries:
      - "gcr.io/my-org/"
      - "registry.k8s.io/"
# Use enforcementAction: dryrun for testing without blocking
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-labels-dryrun
spec:
  enforcementAction: dryrun   # Audit only — does NOT block requests
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]
  parameters:
    labels:
      - "team"

# Check audit results:
# kubectl get k8srequiredlabels require-labels-dryrun -o yaml
# Look at status.violations[] for non-compliant resources
Enforcement Actions:
  • deny — Rejects the request (default). The resource is NOT created.
  • dryrun — Allows the request but records violations in the Constraint's status.violations field. Perfect for rollout testing.
  • warn — Allows the request but returns a warning message to the client (shown in kubectl output).

Policy Library

The Gatekeeper Library (open-policy-agent/gatekeeper-library) provides a curated set of production-ready ConstraintTemplates:

  • General — Required labels, allowed repos, unique ingress hosts, block NodePort services
  • Pod Security — Disallow privilege escalation, restrict host namespaces, require read-only root filesystem, drop capabilities
  • PSP Migration — Templates that replicate PodSecurityPolicy controls for clusters migrating from PSP
# Clone and explore the library
git clone https://github.com/open-policy-agent/gatekeeper-library.git
ls gatekeeper-library/library/
# general/
# pod-security-policy/

# Install a library template (e.g., block privileged containers)
kubectl apply -f gatekeeper-library/library/pod-security-policy/privileged-containers/template.yaml

# Create a constraint using the library template
cat <
Best Practice: Start with enforcementAction: dryrun when deploying new policies. Monitor status.violations for a few days to identify existing non-compliant resources. Once clean, switch to deny. This prevents breaking existing workloads.

Exercises

Exercise 1 — Write a "Disallow Latest Tag" Template: Create a ConstraintTemplate called K8sDisallowLatestTag that denies any Pod or Deployment where a container image uses the :latest tag (or no tag at all). Hint: use Rego's endswith(image, ":latest") and check for images with no : character.
Exercise 2 — Namespace-Scoped Registry Constraint: Using the K8sAllowedRegistries template from this article, create two Constraints: one for production namespace (only gcr.io/prod-org/) and another for development namespace (allows gcr.io/prod-org/ plus docker.io/library/). Verify each works by deploying test Pods.
Exercise 3 — Dryrun Rollout: Deploy the require-team-label Constraint with enforcementAction: dryrun. Create several Deployments — some with the label, some without. Inspect kubectl get k8srequiredlabels require-team-label -o yaml and confirm violations appear in status.violations[] without blocking creation.

Key Takeaways

Summary:
  • Gatekeeper installs as a ValidatingAdmissionWebhook — every API request passes through it
  • ConstraintTemplates define reusable policy logic (Rego) with a parameter schema
  • Constraints instantiate templates with specific parameters and resource/namespace scoping
  • Use match.kinds, match.namespaces, and match.excludedNamespaces for precise targeting
  • enforcementAction: dryrun lets you test policies without blocking — always start here
  • The gatekeeper-library provides production-ready templates for common security policies
  • Rego in Gatekeeper uses violation[{"msg": msg}] (not deny) as the rule head

Next in This Track

In Part 3: Audit, Mutation & Testing, we cover Gatekeeper's audit controller for detecting existing violations, mutation webhooks for automatically modifying resources, and unit-testing Rego policies with opa test and Gatekeeper's test framework.