Policy as Code: Guardrails and Compliance with OPA and Sentinel

Enforce infrastructure compliance and governance automatically using Policy as Code with Open Policy Agent (OPA), HashiCorp Sentinel, or AWS Policy.

published: reading time: 10 min read

Policy as Code: Guardrails and Compliance with OPA and Sentinel

Policy as code brings governance and compliance into your automated workflows. Instead of reviewing infrastructure changes manually or hoping naming conventions are followed, you write policies that automatically validate configurations before deployment. Bad configurations get rejected with clear explanations, and compliance becomes a side effect of the deployment process rather than an afterthought.

Three main approaches dominate the landscape. Open Policy Agent (OPA) is an open-source general-purpose policy engine. HashiCorp Sentinel is a commercial product tightly integrated with HashiCorp tools. AWS Policy is the native option for AWS environments. Each has different strengths depending on your stack and requirements.

When to Use / When Not to Use

When policy as code makes sense

Policy as code pays off when compliance failures are expensive. If you operate in regulated industries—finance, healthcare, government—or when security incidents from misconfiguration carry significant financial or reputational risk, automated policy enforcement catches problems before they reach production.

Use it when you have multiple teams deploying infrastructure independently. Without automated policy checks, each team might interpret standards differently. A policy library enforced in CI creates consistency without requiring a central approver for every change.

Policy as code also helps when you need audit evidence. Regulated environments often need to prove that controls existed before deployment, not just that someone reviewed a checklist. A policy evaluation log showing which rules passed and which failed is stronger evidence than a sign-off sheet.

When to skip it

If your infrastructure is small and a single team reviews every change manually, policy as code adds overhead without much benefit. The cost of writing and maintaining policies may exceed the cost of occasional misconfigurations.

If your organization is still iterating on standards, premature policy enforcement can slow down experimentation. Locking down configurations before the team has settled on what “good” looks like creates friction without improving outcomes.

Policy as Code Introduction

The core idea is straightforward. You write rules that describe what valid infrastructure looks like. During deployment, a policy engine evaluates your proposed configuration against those rules. If the configuration violates a rule, the deployment stops and you get an error explaining what went wrong and why.

Policies can enforce anything you can express programmatically. Common use cases include requiring encryption at rest, enforcing naming conventions, restricting which regions resources can be deployed to, ensuring cost controls like budget alerts exist, and preventing public exposure of sensitive resources.

The alternative—manual policy review—does not scale. Human reviewers get tired, miss details, and apply rules inconsistently. Automated policy enforcement removes the variability and catches issues before they reach production.

OPA Rego Language Basics

OPA uses a custom language called Rego for writing policies. Rego is declarative—you describe what makes a configuration valid rather than how to validate it.

package terraform.analysis

import future.keywords.if
import future.keywords.contains

# Deny S3 buckets without encryption
deny_s3_unencrypted if {
    input.resource_changes[_].type == "aws_s3_bucket"
    not input.resource_changes[_].change.after.server_side_encryption_configuration
}

# Deny EC2 instances without tags
deny_ec2_missing_tags if {
    some rc in input.resource_changes
    rc.type == "aws_instance"
    not rc.change.after.tags
}

# Deny RDS instances that are publicly accessible
deny_rds_public_access if {
    some rc in input.resource_changes
    rc.type == "aws_db_instance"
    rc.change.after.publicly_accessible == true
}

Rego policies operate on input documents. For Terraform, the input is a JSON representation of the plan or state. You write rules that traverse this document and return violations when conditions are met.

The deny prefix is convention—OPA does not require it. You can name your rules anything. The important part is that when a rule evaluates to true, that represents a violation.

Writing and Testing Policies

OPA ships with a testing framework that makes policy development iterative rather than trial-and-error.

package terraform.analysis

import future.keywords.if

deny_s3_unencrypted if {
    some bucket in input.resource_changes
    bucket.type == "aws_s3_bucket"
    not bucket.change.after.server_side_encryption_configuration
}

# Tests
Test_deny_s3_unencrypted_violation {
    not deny_s3_unencrypted with input as {
        "resource_changes": [{
            "type": "aws_s3_bucket",
            "change": {
                "after": {
                    "bucket": "my-bucket",
                    "server_side_encryption_configuration": null
                }
            }
        }]
    }
}

Test_allow_s3_encrypted {
    deny_s3_unencrypted with input as {
        "resource_changes": [{
            "type": "aws_s3_bucket",
            "change": {
                "after": {
                    "bucket": "my-bucket",
                    "server_side_encryption_configuration": {
                        "rule": {
                            "apply_server_side_encryption_by_default": {
                                "sse_algorithm": "AES256"
                            }
                        }
                    }
                }
            }
        }]
    }
}

Run tests with opa test. Good test coverage catches regressions when you modify policies and validates that new policies behave as intended.

opa test ./policies/ -v

Integrating with CI/CD Pipelines

The real value of policy as code emerges in automated pipelines. Every infrastructure change gets evaluated against policies before deployment proceeds.

# GitHub Actions example
name: Terraform Policy Check

on:
  pull_request:
    paths:
      - "**.tf"
      - "policies/**"

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

      - name: Install OPA
        run: |
          curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64
          chmod +x opa

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

      - name: Evaluate policies
        run: |
          opa eval --fail-defined -d policies.rego -f pretty -I -d plan.json "data.terraform.analysis.deny"

If any policy returns a violation, the fail-defined flag causes OPA to exit with a non-zero code, failing the pipeline. Developers see the violation details in the pull request comments.

Terraform Validation with OPA

Beyond CI/CD integration, OPA can validate Terraform code directly without applying changes. This is useful for pre-merge checks that do not require running an actual plan.

The conftest tool integrates OPA with configuration files.

# .github/workflows/tf-validation.yml
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install conftest
        run: |
          curl -L -o conftest https://github.com/open-policy-agent/conftest/releases/download/v0.55.0/conftest_0.55.0_Linux_x86_64.tar.gz
          tar xzf conftest.tar.gz

      - name: Validate Terraform
        run: |
          conftest test . --policy policy.rego

For teams using Terraform Cloud, OPA integration happens through the Sentinel policy engine’s Rego-like syntax, which is compatible enough to port policies between the two systems.

Real-World Policy Examples

Budget enforcement policies prevent runaway infrastructure costs.

package terraform.analysis

import future.keywords.if
import future.keywords.contains

deny_missing_cost_alert if {
    not input.resource_changes[_] {
        .type == "aws_budgets_cost_notification"
        .change.after.notification
    }
}

deny_unlimited_budget if {
    some rc in input.resource_changes
    rc.type == "aws_budgets_cost_notification"
    rc.change.after.budget_type == "USAGE"
}

Tagging policies enforce organizational naming standards.

package terraform.analysis

import future.keywords.if

required_tags := {"Environment", "Team", "CostCenter", "Application"}

deny_missing_required_tags if {
    some rc in input.resource_changes
    is_compute_resource(rc.type)
    missing_tags := required_tags - get_tags(rc)
    count(missing_tags) > 0
}

is_compute_resource(resource_type) if {
    resource_type == "aws_instance"
}

is_compute_resource(resource_type) if {
    resource_type == "aws_ecs_service"
}

get_tags(rc) := tags if {
    tags := {k | rc.change.after.tags[k]}
}

Network policies prevent accidental public exposure.

package terraform.analysis

deny_public_rds if {
    some rc in input.resource_changes
    rc.type == "aws_db_instance"
    rc.change.after.publicly_accessible == true
}

deny_public_load_balancer if {
    some rc in input.resource_changes
    rc.type == "aws_lb"
    rc.change.after.scheme == "internet-facing"
    not has_waf_access_logging(rc)
}

OPA vs Sentinel vs AWS Policy Comparison

AspectOPASentinelAWS Policy
CostFree, open-sourceCommercial (HashiCorp Enterprise)Free (native AWS)
ScopeMulti-cloud, Kubernetes, CI/CDHashiCorp tools onlyAWS only
LanguageRegoSentinel (DSL)IAM JSON/YAML
TestingBuilt-in test frameworkBuilt-in test frameworkIAM Access Analyzer
CI/CD integrationNative via conftestNative via Terraform CloudNative via AWS config

Policy Enforcement Flow

flowchart TD
    A[Terraform Plan] --> B[OPA evaluates policies]
    B --> C{All policies pass?}
    C -->|Yes| D[Apply proceeds]
    C -->|No| E[Show violations]
    E --> F[Fix code]
    F --> A
    D --> G[State updated]

Production Failure Scenarios

Common Policy Failures

FailureImpactMitigation
Overly strict policiesDevelopers bypass policies or disable enforcementStart permissive, tighten incrementally
Policy conflictsImpossible to satisfy two policies simultaneouslyAudit policy interactions before deployment
Slow policy evaluationCI pipeline delaysCache OPA decisions, optimize Rego queries
Policies not enforced in CIViolations reach productionMake policy checks mandatory in pipeline
Rego bugs allow violationsFalse sense of complianceWrite comprehensive tests for every policy

Policy Debug Flow

flowchart TD
    A[Policy violation in CI] --> B[Run OPA locally]
    B --> C[Verbose output: opa eval -v]
    C --> D{Is violation real?}
    D -->|Yes| E[Fix infrastructure code]
    D -->|No| F[Fix policy Rego]
    E --> G[Retry pipeline]
    F --> G

Observability Hooks

Track policy enforcement to catch systemic issues and measure compliance.

What to monitor:

  • Policy violation rate per team or repository
  • Most commonly violated policies
  • Policy evaluation duration in CI
  • Policy change frequency (too many changes may indicate instability)
  • Exception request rate
# Run OPA with verbose output to debug
opa eval --fail-defined -d policy.rego -v "data.terraform.analysis.deny"

# Test policies against a plan file
opa eval --fail-defined -d policy.rego -f pretty \
  -I -d plan.json "data.terraform.analysis.deny"

# List all policies and their pass/fail status
opa eval --explain=full -d policies/ -f pretty plan.json

# Check Rego syntax without running
opa check policy.rego

# Run policy tests
opa test ./policies/ -v

Common Pitfalls / Anti-Patterns

Writing policies before understanding the data

OPA policies operate on input documents—for Terraform, that means understanding the structure of plan.json or state.json. Writing policies without first examining the actual data structure leads to policies that never match or miss edge cases.

Making policies too broad

A policy that flags every resource creates noise and trains teams to ignore violations. Instead, target specific risky resources. “Deny S3 buckets without encryption” is actionable. “Deny resources without tags” is noisy when many resources legitimately lack tags.

Not testing policies

Rego policies are code and can have bugs. A policy with a logic error might pass everything when it should deny, or deny everything when it should pass. OPA’s test framework exists—use it.

Hardcoding exceptions in policies

When a legitimate use case violates a policy, the instinct is to add an exception. Over time, exceptions accumulate and the policy becomes meaningless. Instead, fix the policy to handle the legitimate case, or escalate to accepting the risk formally.

Quick Recap

Key Takeaways

  • Policy as code shifts compliance from manual review to automated enforcement
  • OPA is vendor-neutral and works with Terraform, Kubernetes, and more
  • Sentinel is tightly integrated with HashiCorp tools but requires commercial licensing
  • AWS Policy Engine covers AWS-only environments with no additional cost
  • Start with high-impact policies: encryption, tagging, public exposure restrictions
  • Test policies as thoroughly as application code

Policy Health Checklist

# Run policy tests
opa test ./policies/ -v

# Evaluate policies against a plan
opa eval --fail-defined -d policies.rego -f pretty -I plan.json "data.terraform.analysis.deny"

# Check policy syntax
opa check policy.rego

# Debug policy evaluation
opa eval --explain=full -d policy.rego plan.json

# List all violations in CI
opa eval --fail-defined -d policies.rego -f pretty -I plan.json "data.terraform.analysis"

Conclusion

Policy as code shifts compliance from manual review to automated enforcement. OPA provides a vendor-neutral, general-purpose policy engine that integrates with Terraform, Kubernetes, and other infrastructure tools. Writing policies in Rego takes practice, but the resulting automated governance is worth the investment.

Start with a few high-impact policies—encryption requirements, tagging enforcement, publicly accessible resource restrictions. Expand coverage as your policy library grows and your team’s confidence with the tooling increases.

For more on DevOps practices, see our post on Cost Optimization which covers cloud cost governance patterns. For securing your infrastructure and IAM, see Cloud Security. For monitoring policy changes and compliance over time, see Observability Engineering.

Category

Related Posts

Compliance Automation: SOC 2, PCI-DSS, and Audit Trails

Automate compliance evidence collection and continuous compliance monitoring for SOC 2, PCI-DSS, and other frameworks using policy-as-code.

#compliance #soc2 #pci-dss

Alerting in Production: Building Alerts That Matter

Build alerting systems that catch real problems without fatigue. Learn alert design principles, severity levels, runbooks, and on-call best practices.

#data-engineering #alerting #monitoring

Audit Trails: Building Complete Data Accountability

Learn how to implement comprehensive audit trails that track data changes, access, and lineage for compliance and debugging.

#data-engineering #audit #audit-trails