Skip to content

Portal | Level: L2: Operations | Topics: Open Policy Agent | Domain: Security

Open Policy Agent — Primer

Why OPA Matters

Policy enforcement is usually baked into application code — a tangle of if/else checks spread across services that each team re-implements in their own way. When the policy changes, every service changes. When you want to audit what policy is in effect, there is no single place to look.

Open Policy Agent (OPA) decouples policy from code entirely. You write policy in a purpose-built language (Rego), feed it structured input (a JSON document), and get a decision back. Your application just makes an HTTP call — it doesn't own the policy logic. OPA is a general-purpose engine: the same tool enforces Kubernetes admission control, API authorization, Terraform guardrails, and SSH access decisions.


Architecture

OPA can be deployed three ways depending on your latency and availability needs:

Sidecar (per-service, low latency)
  [Your Service] --HTTP--> [OPA sidecar] --> decision

Library (in-process, no network hop)
  [Your Go/Java app] --SDK call--> [OPA embedded] --> decision

Centralized service (shared, easier to manage)
  [Service A]  \
  [Service B]   --HTTP--> [OPA cluster] --> decision
  [Service C]  /

Sidecar is the Kubernetes-native pattern. OPA runs as a container in the same pod, policy is kept in sync via bundles, and latency is microseconds over localhost.

Centralized works well for organizations that want one policy service for everything, at the cost of making that service a high-availability dependency.

Library (via the OPA SDK for Go, or WASM) eliminates the network entirely and is used in edge or high-throughput scenarios.


Rego Language

Rego is a declarative query language designed specifically for policy. It is not imperative — there are no if/else chains, no loops that mutate state. You define what is true, and Rego derives all consequences.

Basic Rule

package authz

default allow = false

allow {
    input.user.role == "admin"
}

allow {
    input.method == "GET"
    input.path[0] == "public"
}

Multiple allow blocks are implicitly OR'd. All conditions inside a single block are AND'd. This mirrors logical conjunction/disjunction naturally.

Partial Rules (Set/Object Comprehensions)

Partial rules build collections incrementally. Each rule body that evaluates to true contributes an element.

# Collect all violations (partial set rule)
violation[msg] {
    input.pod.spec.containers[_].securityContext.privileged == true
    msg := "privileged container not allowed"
}

violation[msg] {
    not input.pod.spec.securityContext.runAsNonRoot
    msg := "must run as non-root"
}

Comprehensions

Build sets, arrays, or objects inline:

# Set of all image registries used
registries := {r | r := input.pod.spec.containers[_].image; parts := split(r, "/"); count(parts) > 1}

# Object: label -> value for all "env" labels
env_labels := {k: v | v := input.resource.metadata.labels[k]; startswith(k, "env")}

Unification and Negation

Variables in Rego are unified, not assigned. x := 1 succeeds only if x is unbound or already equals 1. Negation uses not:

# deny if user is NOT in the allowed list
deny {
    not data.users.allowed[input.user]
}

not is negation-as-failure: the condition is considered false if it cannot be proven true.


Input and Data Model

OPA evaluates policy against two sources of structured JSON:

  • input: the query document sent by your application (the request being evaluated)
  • data: background data loaded into OPA (user directories, allow-lists, config)
// input example (Kubernetes admission webhook)
{
  "apiVersion": "admission.k8s.io/v1",
  "request": {
    "object": {
      "metadata": { "name": "nginx", "namespace": "default" },
      "spec": {
        "containers": [{ "image": "nginx:latest" }]
      }
    }
  }
}

External data (e.g., a list of approved registries) lives in data and is loaded separately from policy, either via the bundle API or the PUT /v1/data REST endpoint.


OPA Gatekeeper for Kubernetes

Gatekeeper is the official Kubernetes integration. It implements the Kubernetes admission webhook using OPA as the policy engine, but wraps policy in Kubernetes CRDs for a more native experience.

ConstraintTemplate

Defines a new CRD (a constraint kind) and its Rego logic:

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}] {
          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

An instance of a ConstraintTemplate — specifies parameters and scope:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels: ["team", "env"]

Audit Mode

Gatekeeper continuously audits existing resources against all active constraints (not just new admissions). Violations accumulate in the .status.violations field of each Constraint object. This lets you enforce gradually: deploy a constraint in audit mode first, observe violations, fix them, then switch to enforcement.


Decision Logging and Monitoring

OPA emits decision logs for every policy query: input, output, policy version, and latency. Ship these to your SIEM or log aggregator for audit and debugging.

# opa config.yaml — send decision logs to a remote endpoint
decision_logs:
  service: my-log-sink
  reporting:
    min_delay_seconds: 5
    max_delay_seconds: 10

Key metrics exposed at /metrics (Prometheus format): opa_rego_query_eval_ns_*, opa_runtime_*, bundle download success/failure.


Bundle API

Bundles are tarballs containing policy (.rego files) and data (.json files). OPA polls a bundle server (S3, GCS, nginx, OCI registry) and hot-reloads policy without a restart.

# config.yaml bundle section
bundles:
  authz:
    service: bundle-server
    resource: /bundles/authz.tar.gz
    polling:
      min_delay_seconds: 30
      max_delay_seconds: 60

The bundle manifest (/.manifest) pins a revision string so OPA can detect staleness and roll back on corruption.


Testing Policies

OPA has a built-in test framework. Test files are Rego files whose rule names start with test_:

package authz_test

import data.authz

test_allow_admin {
    authz.allow with input as {"user": {"role": "admin"}, "method": "GET"}
}

test_deny_guest_write {
    not authz.allow with input as {"user": {"role": "guest"}, "method": "POST"}
}

Run tests:

opa test ./policies/          # run all tests recursively
opa test -v ./policies/       # verbose: show test names and pass/fail
opa test --coverage ./policies/  # coverage report

CI/CD Integration

# Syntax check + type checking
opa check --strict ./policies/

# Auto-format (modifies in place)
opa fmt --write ./policies/

# Benchmark a specific query
opa bench --data ./policies/ 'data.authz.allow'

# Gatekeeper-specific test runner (validates ConstraintTemplates)
gator test -f ./constraints/

Add these to your CI pipeline before merging policy changes. A failing opa test should block the merge.


Common Use Cases

Use case How OPA is deployed
Kubernetes admission control Gatekeeper webhook (ConstraintTemplate + Constraint)
API authorization (microservices) Sidecar or centralized service; app calls /v1/data/authz/allow
Terraform plan validation Conftest reads terraform plan -out=plan.json and evaluates Rego
SSH / sudo access PAM module calls OPA; policy checks user groups and target host
Data filtering (row-level security) OPA returns a filter expression; app applies it to the query

Conftest

Conftest is a CLI wrapper around OPA for testing configuration files (YAML, JSON, Dockerfile, HCL). Write Rego policies in policy/ and run:

conftest test deployment.yaml          # single file
conftest test --policy ./policy k8s/   # directory of manifests
conftest test plan.json                # Terraform plan output

Conftest automatically loads policies from the policy/ directory and evaluates deny, warn, and violation rules.


Key Takeaways

  • OPA decouples policy from application code — policy changes deploy independently
  • Rego is declarative: define what is true, not what to do
  • Gatekeeper brings OPA to Kubernetes via CRDs, preserving Kubernetes-native workflow
  • Always write test_ rules for your policies and run opa test in CI
  • Bundles enable hot-reload of policy and data across a fleet of OPA instances
  • Decision logs are your audit trail — ship them to a log sink from day one
  • Conftest makes OPA accessible for config-file testing without HTTP infrastructure

Wiki Navigation

  • Open Policy Agent Flashcards (CLI) (flashcard_deck, L1) — Open Policy Agent