Skip to content

OPA and Gatekeeper

OPA (Open Policy Agent) is a general-purpose policy engine. Gatekeeper is its Kubernetes-native integration - it runs OPA as an admission controller and gives you CRDs to manage policies as Kubernetes objects.

The problem they solve: Kubernetes admission control lets you intercept any API request and decide allow or deny. Without a policy engine, you write and maintain webhooks for each enforcement rule. Gatekeeper centralizes this: write a policy in Rego, deploy it as a CRD, and it's automatically enforced by the admission webhook.

Architecture

flowchart TD
    User[kubectl apply] --> APIServer[Kubernetes API Server]
    APIServer --> |ValidatingWebhook| Gatekeeper[Gatekeeper\nadmission webhook]
    Gatekeeper --> |evaluate| OPA[OPA engine]
    OPA --> |query| CT[ConstraintTemplates\n+ data cache]
    OPA --> |returns| Decision{Allow / Deny}
    Decision --> |denied| Reject[Rejected with message]
    Decision --> |allowed| APIServer2[Object admitted]
    subgraph CRDs
        CT2[ConstraintTemplate\ndefines the policy schema]
        Con[Constraint\napplies the policy with params]
    end
    CT2 --> OPA
    Con --> OPA

Gatekeeper runs as a Deployment and registers as both a ValidatingAdmissionWebhook and a MutatingAdmissionWebhook. Every API request matching the webhook rules is sent to Gatekeeper for evaluation before being admitted.

OPA evaluates Rego rules against the request object plus any replicated data from the cluster.

ConstraintTemplate and Constraint

The two-CRD model separates policy definition from policy application:

  • ConstraintTemplate: defines the Rego logic and the schema for parameters
  • Constraint: applies a ConstraintTemplate with specific parameters to specific resource scopes

This lets you write a policy once and instantiate it multiple times with different parameters (e.g., require labels, but with different label sets for different namespaces).

ConstraintTemplate example

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: requiredlabels
spec:
  crd:
    spec:
      names:
        kind: RequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package requiredlabels

        violation[{"msg": msg}] {
          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 applying the template

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: RequiredLabels
metadata:
  name: require-team-label
spec:
  enforcementAction: deny       # or "warn" or "dryrun"
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment", "StatefulSet", "DaemonSet"]
    namespaceSelector:
      matchExpressions:
        - key: environment
          operator: In
          values: ["production", "staging"]
  parameters:
    labels:
      - team
      - cost-center

enforcementAction: deny blocks admission. warn admits but adds a warning to the API response. dryrun only records in audit - never blocks. Use dryrun before deny when rolling out new policies.

Rego language

Rego is a declarative language purpose-built for policy. Logic is expressed as rules that evaluate to true/false or sets/objects.

Key concepts

package example

# A "violation" rule. Gatekeeper calls this; any element in the set = policy violated.
violation[{"msg": msg}] {
  # All expressions in a rule block must be true for the block to produce output.
  input.review.object.spec.template.spec.containers[_].securityContext.privileged == true
  msg := "Privileged containers are not allowed"
}

# Rules can have multiple blocks  -  any block matching = violation
violation[{"msg": msg}] {
  container := input.review.object.spec.template.spec.containers[_]
  not container.resources.limits.memory
  msg := sprintf("Container %v has no memory limit", [container.name])
}

input.review.object is the full Kubernetes object being admitted. input.parameters is the Constraint's parameters field.

Common patterns

Require image from approved registry:

package approvedregistries

violation[{"msg": msg}] {
  container := input.review.object.spec.template.spec.containers[_]
  not startswith(container.image, "registry.mycompany.com/")
  msg := sprintf("Container %v uses unapproved registry: %v", [container.name, container.image])
}

Block latest tag:

package nolatesttag

violation[{"msg": msg}] {
  container := input.review.object.spec.template.spec.containers[_]
  endswith(container.image, ":latest")
  msg := sprintf("Container %v uses :latest tag", [container.name])
}

violation[{"msg": msg}] {
  container := input.review.object.spec.template.spec.containers[_]
  not contains(container.image, ":")
  msg := sprintf("Container %v has no tag (implicitly latest)", [container.name])
}

Require resource limits:

package resourcelimits

violation[{"msg": msg}] {
  container := input.review.object.spec.template.spec.containers[_]
  not container.resources.limits.cpu
  msg := sprintf("Container %v has no CPU limit", [container.name])
}

violation[{"msg": msg}] {
  container := input.review.object.spec.template.spec.containers[_]
  not container.resources.limits.memory
  msg := sprintf("Container %v has no memory limit", [container.name])
}

Audit

Gatekeeper's audit controller periodically evaluates all existing objects against active Constraints, even objects that were admitted before the policy existed. Violations are recorded in the Constraint's status.violations field:

kubectl describe requiredlabels require-team-label
# Status:
#   Audit Timestamp: 2026-05-10T20:00:00Z
#   Violations:
#     - Enforcement Action: deny
#       Kind: Deployment
#       Message: Missing required labels: {"cost-center"}
#       Name: legacy-app
#       Namespace: production

Audit runs every 60 seconds by default (--audit-interval). Use it to assess the blast radius of a new policy before switching from dryrun to deny.

Data replication

Rego can reference cluster data beyond the admission request - for example, checking if a Service with the same name already exists, or comparing against a list of approved namespaces. Gatekeeper replicates this data into OPA via the Config CRD:

apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
  name: config
  namespace: gatekeeper-system
spec:
  sync:
    syncOnly:
      - group: ""
        version: v1
        kind: Namespace
      - group: ""
        version: v1
        kind: Pod
      - group: "apps"
        version: v1
        kind: Deployment

Replicated data is available in Rego as data.inventory.cluster[kind][name] or data.inventory.namespace[namespace][kind][name]. Use sparingly - replication increases Gatekeeper's memory footprint.

Mutation

Gatekeeper's mutation feature modifies objects at admission time (like a MutatingAdmissionWebhook). Use it to inject defaults, labels, or security context settings:

apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
  name: set-default-security-context
spec:
  applyTo:
    - groups: ["apps"]
      kinds: ["Deployment"]
      versions: ["v1"]
  match:
    scope: Namespaced
    namespaces: ["production"]
  location: "spec.template.spec.containers[name: *].securityContext.readOnlyRootFilesystem"
  parameters:
    assign:
      value: true

Mutation runs before validation. A common pattern: mutate to add required labels/annotations, then validate that they exist. This avoids breaking users who don't know the policy yet.

Exemptions

Some resources (Gatekeeper itself, system namespaces) must be exempt from policy enforcement:

spec:
  match:
    excludedNamespaces:
      - kube-system
      - gatekeeper-system
      - cert-manager

Or globally via the Config CRD:

spec:
  validation:
    traces: []
  match:
    excludedNamespaces:
      - kube-system

Testing with conftest

Conftest lets you unit-test Rego policies against YAML fixtures locally, before deploying to Gatekeeper:

# policy/required_labels.rego
# test/required_labels_test.rego
conftest test --policy policy/ test/
# test/required_labels_test.rego
package requiredlabels

test_missing_label_violation {
  violations := violation with input as {
    "review": {"object": {
      "metadata": {"labels": {"team": "platform"}},
      "spec": {}
    }},
    "parameters": {"labels": ["team", "cost-center"]}
  }
  count(violations) == 1
}

test_all_labels_present {
  violations := violation with input as {
    "review": {"object": {
      "metadata": {"labels": {"team": "platform", "cost-center": "eng-infra"}},
      "spec": {}
    }},
    "parameters": {"labels": ["team", "cost-center"]}
  }
  count(violations) == 0
}

OPA without Gatekeeper

Gatekeeper is the right choice for Kubernetes admission control. But OPA itself is useful beyond admission - policy checks in CI pipelines (conftest), API gateway authorization, data pipeline access control. The same Rego you write for Gatekeeper can be used in these contexts with opa eval or the OPA bundle server.