intermediate40 min readgetting-startedUpdated: 2025-06-28

Open Policy Agent (OPA) and Rego Tutorial

An in-depth, hands-on guide to learning Open Policy Agent and the Rego language for unified policy enforcement across your stack.

๐Ÿ“‹ Prerequisites

  • Basic understanding of JSON, YAML, and REST APIs
  • Familiarity with the command line
  • Docker installed (for running OPA server easily)
  • A use case in mind (e.g., microservices, Kubernetes, or Terraform)

๐Ÿท๏ธ Topics Covered

opa rego tutorial for beginnersopen policy agent getting started guidehow to write rego policies step by stepopa kubernetes integration tutorialrego language examples and syntaxopen policy agent best practices

What is Open Policy Agent (OPA)? Complete Beginner's Guide to Policy Engines

Open Policy Agent (OPA) is an open-source, general-purpose policy engine. It unifies policy enforcement across different technologies and systems. Think of it as a central brain for making decisions. Instead of scattering authorization and policy logic throughout your application code, you offload those decisions to OPA.

Your service queries OPA with a JSON object (the input), and OPA returns a JSON object (the decision). This makes your services simpler and your policies easier to manage, test, and audit.

OPA Engine

The core component that evaluates policies. You can run it as a standalone binary, a Docker container, or as a library embedded in your Go application.

Rego Language

The high-level declarative language used to write policies for OPA. Rego was inspired by Datalog and is designed specifically for querying complex data structures.

Policy & Data Caching

OPA can be configured to pull policy and data from remote bundles, keeping its local cache up-to-date for fast, local decisions without network latency.

Rego Language Tutorial: Master OPA Policy Syntax and Functions

To master OPA, you must learn Rego. Let's start with the basics. Create a directory for our project, and add two files: policy.rego and data.json.

data.json

{
    "roles": {
        "alice": ["admin", "developer"],
        "bob": ["developer"]
    },
    "servers": [
        {"id": "app_server_1", "protocol": "https", "ports": [80, 443]},
        {"id": "db_server_1", "protocol": "tcp", "ports": [5432]}
    ]
}

policy.rego

package tutorial

# By default, deny access
default allow = false

# Allow if the user is an admin
allow = true {
    input.user == "alice"
}

# Also allow if the input server uses https
allow = true {
    some i
    server := data.servers[i]
    server.id == input.server_id
    server.protocol == "https"
}

# Generate a list of all server IDs
server_ids = [server.id | some i; server := data.servers[i]]

# Check if a user has a specific role
user_has_role(user, role) {
    data.roles[user][_] == role
}

Now, let's use the opa eval command to query our policy. The --input flag provides the query context, and the --data flag loads our data file.

Run OPA Evaluation

# Test if alice is allowed (should be true)
$ opa eval --data data.json --input '{"user": "alice", "server_id": "app_server_1"}' 'data.tutorial.allow'
{
  "result": [
    {
      "expressions": [
        {
          "value": true,
          "text": "data.tutorial.allow",
          "location": { "row": 1, "col": 1 }
        }
      ]
    }
  ]
}

# Test if bob is allowed on the db server (should be false)
$ opa eval --data data.json --input '{"user": "bob", "server_id": "db_server_1"}' 'data.tutorial.allow'
{
  "result": [
    {
      "expressions": [
        {
          "value": false,
          "text": "data.tutorial.allow",
          "location": { "row": 1, "col": 1 }
        }
      ]
    }
  ]
}

# Test our custom function
$ opa eval --data data.json 'data.tutorial.user_has_role("bob", "developer")'
# ... returns true

OPA API Authorization Tutorial: Secure Microservices with Rego Policies

Let's use OPA to authorize requests to a fictional API. We'll run OPA as a server and query it from our application (simulated with curl).

First, create the policy file for our API.

api_authz.rego

package http.authz

default allow = false

# Admins can do anything
allow {
    input.user.roles[_] == "admin"
}

# Viewers can only perform GET requests on /reports
allow {
    input.user.roles[_] == "viewer"
    input.method == "GET"
    startswith(input.path, "/reports")
}

Now, run OPA as a server, telling it to load our policy file.

Run OPA Server

opa run --server api_authz.rego

In another terminal, we can now send JSON requests to OPA's data API endpoint to get decisions. The path corresponds to the package name in our Rego file.

Query OPA for Decisions

# 1. Admin tries to POST to /reports. Should be ALLOWED.
curl -X POST localhost:8181/v1/data/http/authz/allow \
    --data-binary '{
        "input": {
            "user": {"name": "alice", "roles": ["admin"]},
            "method": "POST",
            "path": "/reports/quarterly"
        }
    }'

# OPA Response: {"result":true}


# 2. Viewer tries to GET /reports. Should be ALLOWED.
curl -X POST localhost:8181/v1/data/http/authz/allow \
    --data-binary '{
        "input": {
            "user": {"name": "bob", "roles": ["viewer"]},
            "method": "GET",
            "path": "/reports/daily"
        }
    }'

# OPA Response: {"result":true}


# 3. Viewer tries to POST to /users. Should be DENIED.
curl -X POST localhost:8181/v1/data/http/authz/allow \
    --data-binary '{
        "input": {
            "user": {"name": "bob", "roles": ["viewer"]},
            "method": "POST",
            "path": "/users"
        }
    }'

# OPA Response: {"result":false}

Kubernetes Policy Enforcement with OPA Gatekeeper: Step-by-Step Guide

A primary use for OPA is enforcing policies on Kubernetes clusters via OPA Gatekeeper. Gatekeeper uses two custom resources: ConstraintTemplate (which contains the Rego logic) and a Constraint (which applies the template to specific resources).

Here is a ConstraintTemplate that ensures all namespaces have an owner label.

namespace-label-template.yaml

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          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("You must provide labels: %v", [missing])
        }

Now, we create a Constraint to apply this logic, requiring the owner label on all namespaces.

owner-label-constraint.yaml

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: ns-must-have-owner
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels:
      - "owner"

After applying these with kubectl apply -f ., any attempt to create a namespace without the owner label will be rejected by the Kubernetes API server.

Terraform Policy Validation with OPA: Prevent Infrastructure Misconfigurations

You can use OPA to validate a Terraform plan file to catch misconfigurations before they are applied. First, generate a plan and convert it to JSON.

Generate Terraform Plan JSON

terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json

Now, write a Rego policy to check the plan. This policy denies any AWS S3 bucket that doesn't have block public access enabled.

terraform_aws.rego

package terraform.aws

deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    resource.mode == "managed"

    # Check for both create and update actions
    actions := {"create", "update"}
    actions[resource.change.actions[_]]

    # Check if the block_public_acls is not set to true
    not resource.change.after.block_public_acls == true
    
    msg := sprintf("S3 bucket '%s' must have block_public_acls enabled.", [resource.address])
}

Finally, run OPA against the plan file. If there are any violations, OPA will return the deny messages.

Validate Plan with OPA

opa eval -f pretty -d terraform_aws.rego -i tfplan.json "data.terraform.aws.deny"

๐ŸŽ‰ Congratulations!

Tutorial Complete

You have completed this in-depth OPA tutorial. You now have the foundational skills to:

โœ…

Write Declarative Policies

Write declarative policies in the Rego language.

โœ…

Decouple Policy Decisions

Decouple policy decisions from your services.

โœ…

Enforce Custom Policies

Enforce custom policies for APIs, Kubernetes, and Terraform.

โœ…

Build Unified Strategy

Begin building a unified policy-as-code strategy for your organization.

Next Steps