Compliance, Governance & DevSecOps Maturity

Policy as Code with OPA & Rego

4 min read

Policy as Code (PaC) treats security and compliance policies as version-controlled code. Open Policy Agent (OPA) and its Rego language have become the standard for defining and enforcing policies across the software development lifecycle.

Why Policy as Code?

Traditional Approach Policy as Code
Manual policy reviews Automated enforcement
Documentation-based Executable policies
Inconsistent application Consistent across environments
Slow approval cycles Real-time validation
Hard to audit Version-controlled, auditable

Open Policy Agent (OPA) Overview

┌─────────────────────────────────────────────────────────┐
│                   Policy Enforcement                     │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  ┌──────────────┐    ┌──────────────┐                  │
│  │ Input Data   │───▶│     OPA      │                  │
│  │ (JSON)       │    │              │                  │
│  └──────────────┘    │  ┌────────┐  │                  │
│                      │  │ Rego   │  │───▶ Decision     │
│  ┌──────────────┐    │  │ Policy │  │     (allow/deny) │
│  │   Query      │───▶│  └────────┘  │                  │
│  └──────────────┘    └──────────────┘                  │
│                                                          │
└─────────────────────────────────────────────────────────┘

Getting Started with OPA

Installation

# macOS
brew install opa

# Linux
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
chmod +x opa
sudo mv opa /usr/local/bin/

# Verify installation
opa version

Basic Rego Policy

# policy.rego
package authz

# Default deny
default allow := false

# Allow if user is admin
allow if {
    input.user.role == "admin"
}

# Allow if user owns the resource
allow if {
    input.user.id == input.resource.owner_id
}

Testing the Policy

# Input data (input.json)
{
  "user": {
    "id": "user123",
    "role": "developer"
  },
  "resource": {
    "owner_id": "user123",
    "type": "document"
  }
}

# Evaluate policy
opa eval -i input.json -d policy.rego "data.authz.allow"
# Result: true (user owns the resource)

Kubernetes Admission Control with OPA

Gatekeeper: OPA for Kubernetes

# Install Gatekeeper
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml

Constraint Template

# template.yaml
apiVersion: templates.gatekeeper.sh/v1
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}] {
          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

# constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: pods-must-have-owner
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    labels: ["app", "owner", "environment"]

CI/CD Policy Enforcement

Terraform Policy Example

# terraform_policy.rego
package terraform

# Deny public S3 buckets
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    resource.change.after.acl == "public-read"
    msg := sprintf("S3 bucket '%s' cannot be public", [resource.name])
}

# Require encryption on RDS
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_db_instance"
    not resource.change.after.storage_encrypted
    msg := sprintf("RDS instance '%s' must have encryption enabled", [resource.name])
}

# Enforce minimum instance size
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    not valid_instance_type(resource.change.after.instance_type)
    msg := sprintf("Instance '%s' uses disallowed type '%s'", [resource.name, resource.change.after.instance_type])
}

valid_instance_type(type) {
    allowed := {"t3.micro", "t3.small", "t3.medium", "m5.large"}
    allowed[type]
}

GitHub Actions Integration

# .github/workflows/policy-check.yml
name: Policy Check

on:
  pull_request:
    paths:
      - '**/*.tf'

jobs:
  opa-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Terraform Plan
        run: |
          terraform init
          terraform plan -out=tfplan
          terraform show -json tfplan > tfplan.json

      - name: Setup OPA
        uses: open-policy-agent/setup-opa@v2

      - name: Run OPA Policy Check
        run: |
          opa eval -i tfplan.json \
            -d policies/ \
            --format pretty \
            "data.terraform.deny"

Common Policy Patterns

Resource Tagging

package tags

required_tags := {"environment", "owner", "cost-center"}

deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    tags := object.get(resource.change.after, "tags", {})
    missing := required_tags - {tag | tags[tag]}
    count(missing) > 0
    msg := sprintf("Resource '%s' missing tags: %v", [resource.name, missing])
}

Network Security

package network

# Deny overly permissive security groups
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_security_group_rule"
    resource.change.after.type == "ingress"
    resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
    resource.change.after.from_port == 0
    resource.change.after.to_port == 65535
    msg := "Security group allows all traffic from the internet"
}

Testing Rego Policies

# policy_test.rego
package terraform

test_deny_public_s3 {
    deny["S3 bucket 'test' cannot be public"] with input as {
        "resource_changes": [{
            "type": "aws_s3_bucket",
            "name": "test",
            "change": {"after": {"acl": "public-read"}}
        }]
    }
}

test_allow_private_s3 {
    count(deny) == 0 with input as {
        "resource_changes": [{
            "type": "aws_s3_bucket",
            "name": "test",
            "change": {"after": {"acl": "private"}}
        }]
    }
}
# Run tests
opa test policies/ -v

Next, we'll explore compliance automation for SOC2, HIPAA, and PCI-DSS. :::

Quiz

Module 6: Compliance, Governance & DevSecOps Maturity

Take Quiz