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. :::