Policy Automation with Open Policy Agent

Enforce security, compliance, and operational policies as code using Open Policy Agent. Covers Rego language, Kubernetes admission control, Terraform policy validation, CI/CD gate enforcement, and building a policy library that scales across teams.

Policy Automation with Open Policy Agent enables the declarative enforcement of operational, security, and compliance rules across cloud infrastructure, Kubernetes clusters, and service configurations. It matters when teams need to consistently apply and audit policies across heterogeneous environments without writing custom code for every check, especially in dynamic, multi-team, multi-tenant systems. Policies are written once and enforced automatically at build, deploy, and runtime, reducing drift, improving auditability, and enabling self-service infrastructure.

Define Policies in Rego

Rego is the policy language used by Open Policy Agent (OPA), and it is the core of policy automation. A policy is a named set of rules that evaluate input data and return decisions (e.g., allow, deny, reason). Policies are stored as .rego files and can be versioned in Git.

A minimal policy file policies/require-namespace.rego:

package policies.require_namespace

import input as request

default allow = false

allow {
    request.kind == "Pod"
    request.namespace == "prod"
}

reasons {
    request.kind == "Pod"
    not request.namespace
    reason = "Pod must be in the prod namespace"
}

reasons {
    request.kind == "Pod"
    request.namespace != "prod"
    reason = "Pod is in " ++ request.namespace ++ ", but production requires 'prod'"
}

This policy checks that all Pod resources in the input data have a namespace field set to "prod". If not, it returns allow = false and a list of reasons.

Load Policies and Evaluate Inputs

OPA runs as a standalone service or embedded in an application. It exposes a REST API at /v1/policies and /v1/decision endpoints. Policies are loaded via POST /v1/policies/{name} with a JSON body containing the Rego code.

To load the policy above:

curl -X POST http://opa:8181/v1/policies/require-namespace \
  -H "Content-Type: application/json" \
  -d @policies/require-namespace.rego

A policy can be updated by re-posting the same name, overwriting the previous version.

To evaluate a policy, send a request to /v1/decision with an input JSON object:

{
  "kind": "Pod",
  "namespace": "staging",
  "name": "frontend-7f8c9d"
}
curl -X POST http://opa:8181/v1/decision \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "kind": "Pod",
      "namespace": "staging",
      "name": "frontend-7f8c9d"
    },
    "decision": "policies.require_namespace.allow"
  }'

Response:

{
  "result": false,
  "reasons": [
    "Pod is in staging, but production requires 'prod'"
  ]
}

The decision field selects the rule to evaluate, and the result is the value of that rule in the policy. A decision may be a simple boolean, a list, a map, or a complex nested structure.

Integrate OPA with Kubernetes

OPA can be integrated directly into a Kubernetes cluster via OPA Gatekeeper, a controller that watches Kubernetes API objects and enforces policies. Gatekeeper runs as a Kubernetes operator and uses OPA as its policy engine.

A ConstraintTemplate defines the schema of a policy. For example, to enforce that all Pod resources must have resources.limits.memory set:

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredresources
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredResources
      validation:
        openAPIV3Schema:
          properties:
            requiredResources:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredresources

        violation[msg] {
          input.review.object.kind == "Pod"
          input.review.object.spec.containers[_].resources.limits.memory == null
          msg = "Missing memory limit for container " ++ input.review.object.spec.containers[_].name
        }

        violation[msg] {
          input.review.object.kind == "Pod"
          input.review.object.spec.containers[_].resources.limits.memory == null
          msg = "Memory limit must be specified for all containers in Pod " ++ input.review.object.metadata.name
        }

This template is applied once to the cluster:

kubectl apply -f constraint-template.yaml

Then, a K8sRequiredResources instance enforces the rule:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
  name: require-pod-memory
spec:
  requiredResources: ["memory"]

Gatekeeper evaluates this constraint for every Pod created or updated. If a Pod lacks resources.limits.memory, it returns a K8sValidation object in the admission review:

{
  "apiVersion": "admission.k8s.gatekeeper.sh/v1beta1",
  "kind": "K8sValidation",
  "metadata": {
    "name": "require-pod-memory"
  },
  "spec": {
    "requiredResources": ["memory"]
  },
  "status": {
    "conditions": [
      {
        "type": "Valid",
        "status": "False",
        "reason": "MissingMemoryLimit",
        "message": "Missing memory limit for container frontend"
      }
    ]
  }
}

The admission controller rejects the Pod creation with a detailed error, and the violation is logged in the K8sValidation object.

Evaluate Multiple Decisions in One Request

OPA supports batch evaluation of multiple decisions in a single request. This is useful for real-time policy evaluation across many resources.

{
  "input": {
    "pods": [
      {
        "kind": "Pod",
        "namespace": "staging",
        "name": "frontend-1"
      },
      {
        "kind": "Pod",
        "namespace": "prod",
        "name": "backend-2"
      }
    ]
  },
  "decisions": [
    {
      "name": "require-namespace",
      "decision": "policies.require_namespace.allow",
      "input": "pods[0]"
    },
    {
      "name": "require-resources",
      "decision": "k8srequiredresources.violation",
      "input": "pods[1]"
    }
  ]
}

Request:

curl -X POST http://opa:8181/v1/decisions \
  -H "Content-Type: application/json" \
  -d @batch-request.json

Response:

{
  "results": [
    {
      "name": "require-namespace",
      "result": false,
      "reasons": [
        "Pod is in staging, but production requires 'prod'"
      ]
    },
    {
      "name": "require-resources",
      "result": [
        "Missing memory limit for container app"
      ]
    }
  ]
}

This enables high-throughput policy evaluation in CI/CD pipelines or service mesh proxies.

Use External Data Sources

OPA can load data from external sources such as PostgreSQL, JSON files, or HTTP APIs. Data is loaded at startup or on-demand via data declarations.

To load data from a JSON file at /data/teams.json:

{
  "teams": {
    "prod": {
      "owner": "team-prod",
      "slack": "#prod"
    },
    "staging": {
      "owner": "team-staging",
      "slack": "#staging"
    }
  }
}

In Rego:

package policies.team-ownership

import data.teams as teams

default allow = false

allow {
    request.kind == "Pod"
    request.namespace == "prod"
    teams[request.namespace].owner == "team-prod"
}

reason {
    request.kind == "Pod"
    request.namespace == "staging"
    teams[request.namespace].owner != "team-staging"
    reason = "Team ownership mismatch: expected team-staging, got " ++ teams[request.namespace].owner
}

Data can also be loaded dynamically via HTTP. A data source is defined in opa.yaml:

data:
  - name: teams
    source:
      type: http
      url: http://config-service:8080/teams
      polling:
        interval: 30s
        timeout: 5s

OPA periodically polls the endpoint, and the response is merged into the global data object.

If the HTTP endpoint returns:

{
  "teams": {
    "prod": { "owner": "prod-team", "slack": "#prod" },
    "staging": { "owner": "staging-team", "slack": "#staging" }
  }
}

Then the policy can use teams[request.namespace].owner immediately.

Handle Policy Evaluation Failures

OPA silently fails when a policy rule references a non-existent field. For example, if request.spec.containers[0].resources.limits.memory is evaluated but spec.containers is not an array, the entire evaluation returns false with no explicit error.

A common failure is missing input keys. If the request body is missing kind or namespace, the policy returns false for allow even when all other fields are present.

To improve visibility, use the debug endpoint:

curl http://opa:8181/v1/debug \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "kind": "Pod",
      "namespace": "staging"
    },
    "decision": "policies.require_namespace.allow"
  }'

Response includes the full evaluation trace, including which rules fired, what values were computed, and any errors:

{
  "evaluation": {
    "input": {
      "kind": "Pod",
      "namespace": "staging"
    },
    "decision": "policies.require_namespace.allow",
    "result": false,
    "steps": [
      {
        "rule": "policies.require_namespace.allow",
        "result": false,
        "reasons": [
          "Pod is in staging, but production requires 'prod'"
        ],
        "conditions": [
          {
            "name": "request.kind == 'Pod'",
            "value": true
          },
          {
            "name": "request.namespace == 'prod'",
            "value": false
          }
        ]
      }
    ],
    "errors": [
      {
        "message": "unexpected type: expected string, got null",
        "path": "request.namespace",
        "value": null
      }
    ]
  }
}

This trace helps diagnose why a policy failed and is ideal for integrating into CI/CD pipelines and observability dashboards.

Version Policies with Git and CI

Policies are stored in version control, and changes are automatically deployed via CI.

A GitHub Actions workflow:

name: Deploy OPA Policies
on:
  push:
    paths: ["policies/**/*.rego"]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Deploy to OPA
        run: |
          for file in policies/*.rego; do
            curl -X POST http://opa:8181/v1/policies/$(basename $file .rego) \
              -H "Content-Type: application/json" \
              -d "@$file"
          done

      - name: Test Policies
        run: |
          curl -X POST http://opa:8181/v1/decision \
            -H "Content-Type: application/json" \
            -d '{
              "input": {
                "kind": "Pod",
                "namespace": "staging"
              },
              "decision": "policies.require_namespace.allow"
            }' \
            -o result.json

          if ! grep -q '"result": true' result.json; then
            echo "Policy evaluation failed"
            exit 1
          fi

This ensures that every policy change is tested and deployed consistently, with failure feedback visible in the pull request.

Runtime Enforcement and Observability

OPA can be used not only for admission control but also for runtime policy enforcement. For example, a service can query OPA before starting, to verify that all required configurations are in place.

A service starts with:

curl http://opa:8181/v1/decision \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "service": "auth",
      "config": {
        "database": "postgres",
        "ssl": true,
        "timeout": 30
      }
    },
    "decision": "policies.validate-service-config"
  }'

If the service config is invalid, the service fails to start with a clear error.

OPA exposes metrics via /metrics endpoint. These include:

These metrics are scraped by Prometheus and used to trigger alerts, such as "OPA decision evaluation time > 100ms" or "policy load failure in last 5 minutes."

Summary

Policy automation with Open Policy Agent is a robust, declarative way to enforce system-wide rules without code. Policies are written in Rego, evaluated at build, deploy, and runtime, and integrated into Kubernetes via Gatekeeper. Data is loaded from external sources, decisions are batched, and evaluation is observable via metrics and debugging. The system fails gracefully, with clear error messages and traceable evaluations. It is the foundation for automated governance, compliance, and self-service infrastructure.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.