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: 19 min read author: GeekWorkBench

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.

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.

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.

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]

Trade-off Analysis

Policy Engine Selection

FactorOPASentinelAWS Policy
CostFree, open-sourceCommercial (Terraform Cloud Enterprise)Free (native AWS)
Multi-cloudFull supportHashiCorp onlyAWS only
Language paradigmRego (declarative, functional)DSL (imperative-style)IAM JSON/YAML
Learning curveSteeper (new language)Moderate (familiar patterns)Gentle for AWS users
Policy reusePackage system for sharingModule system for sharingPolicy templates
Test frameworkBuilt-in, strongBuilt-in, strongLimited to Access Analyzer
Runtime evaluationAny stage (plan, apply, admission)Terraform Cloud onlyAWS Config, SCPs
CI/CD integrationNative (conftest,opa)Native via TFCNative via AWS config

OPA Rego Complexity vs Simplicity

Simple policies (allow/deny) are straightforward to write and maintain. The deny_s3_unencrypted pattern—checking a single attribute and returning true if violated—is easy to read and debug.

Complex policies (exceptions, conditions, multi-resource) require careful Rego design. Using helper functions and separating data transformation from logic evaluation keeps policies readable. Writing all logic inline makes policies impossible to test in isolation.

The tradeoff is between brevity and maintainability. Inline policies are shorter but harder to test. Helper functions add indirection but enable unit testing of individual logical components.

Policy Scope Decisions

Global policies (applied to every resource) catch everything but generate noise when teams have legitimate exceptions. A global “all resources must have cost center tag” creates violations for resources that genuinely cannot have tags.

Resource-type policies target specific risky resources. “Deny S3 buckets without encryption” and “Deny RDS instances publicly accessible” focus on high-impact cases without flagging every untagged resource.

Exception-based policies start with a deny and add exceptions for known legitimate cases. This approach works well but requires maintenance—every legitimate exception becomes a permanent exception unless someone periodically reviews them.

For most organizations, resource-type policies with specific targeting strike the right balance between coverage and noise.

Policy Evaluation Timing Decisions

StageBenefitRisk
Pre-commit (local)Fast feedback, no CI queueDevelopers may skip
Pull request CIMandatory gate, consistentPipeline latency
Plan-time (in Terraform)Context-aware, sees actual valuesSlows down apply
Admission (Kubernetes)Blocks bad deployments at runtimeLatency impact

The standard approach is pre-merge CI checks with OPA or conftest. This catches violations before they reach any environment. For critical policies, add a second gate at plan-time that evaluates against the actual Terraform plan JSON.

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.

Interview Questions

1. How does OPA evaluate Terraform plan files?

Expected answer points:

  • OPA operates on input documents—for Terraform, plan.json or state.json
  • Terraform plan converts to JSON via `terraform show -json plan.tfplan`
  • OPA Rego policies traverse the JSON document and return violations when conditions are met
  • `opa eval --fail-defined` exits non-zero if any policy returns a violation
2. What is the difference between OPA, Sentinel, and AWS Policy?

Expected answer points:

  • OPA: free, open-source, multi-cloud, uses Rego language
  • Sentinel: commercial, HashiCorp-only, DSL with imperative-style policies
  • AWS Policy: free, native AWS, IAM JSON/YAML for SCPs and Access Analyzer
  • OPA is vendor-neutral; Sentinel locks to HashiCorp; AWS Policy locks to AWS
3. Why should you start with permissive policies and tighten incrementally?

Expected answer points:

  • Overly strict policies cause developers to bypass or disable enforcement
  • Starting permissive identifies actual violations before fining-tune rules
  • Too strict creates noise, trains teams to ignore violations, defeats purpose
  • Incrementally tighten as policy library matures and exceptions get documented
4. How do you debug a policy that is not catching violations it should?

Expected answer points:

  • Run OPA locally with verbose output: `opa eval -v -d policy.rego plan.json`
  • Check if the violation is real—if yes, fix infrastructure code; if no, fix policy Rego
  • Use `opa check` to verify Rego syntax without running
  • Write comprehensive tests for every policy—Rego bugs cause false passes or false denies
5. Why are hardcoded exceptions in policies problematic?

Expected answer points:

  • When a legitimate case violates a policy, instinct is to add exception
  • Over time exceptions accumulate, policy becomes meaningless
  • Instead: fix the policy to handle the legitimate case, or formalize risk acceptance
  • Exception-based policies require ongoing maintenance to stay relevant
6. How does conftest integrate OPA with configuration files?

Expected answer points:

  • conftest is a tool that applies OPA policies to configuration files
  • Runs outside Terraform, validates HCL/Terraform code directly without plan
  • Useful for pre-merge checks that do not require running actual plan
  • `conftest test . --policy policy.rego` validates configs against policies
7. What is the tradeoff between global policies and resource-type policies?

Expected answer points:

  • Global policies catch everything but generate noise when teams have legitimate exceptions
  • Resource-type policies target specific risky resources (S3 encryption, RDS public access)
  • Global "deny untagged resources" is noisy; resource-type is actionable
  • For most organizations, resource-type policies strike right balance between coverage and noise
8. How do you structure Rego policies for maintainability?

Expected answer points:

  • Separate data transformation from logic evaluation using helper functions
  • Inline policies are shorter but harder to test in isolation
  • Use packages to organize policies by domain (terraform.analysis, kubernetes.admission)
  • Test each logical component independently, not just end-to-end
9. What monitoring metrics should you track for policy enforcement?

Expected answer points:

  • Policy violation rate per team or repository—high rate indicates policy issues or training gap
  • Most commonly violated policies—may need clearer documentation or easier to satisfy
  • Policy evaluation duration in CI—slow evaluation indicates optimization opportunity
  • Policy change frequency (too many changes = instability), exception request rate
10. When should you evaluate policies at different stages (pre-commit vs CI vs plan-time)?

Expected answer points:

  • Pre-commit (local): fast feedback but developers may skip
  • Pull request CI: mandatory gate, consistent, adds pipeline latency
  • Plan-time (in Terraform): context-aware, sees actual values, slows down apply
  • Standard approach is pre-merge CI with OPA/conftest; critical policies get second gate at plan-time
11. How does OPA's fail-defined flag work and when would you use it in CI/CD?

Expected answer points:

  • `opa eval --fail-defined` exits with non-zero code when any policy returns a violation
  • Without fail-defined, OPA returns 0 even if violations exist—pipeline continues silently
  • In CI/CD, fail-defined causes the pipeline to fail when policy violations are detected
  • Works with `-f pretty` for human-readable output or `-f json` for programmatic parsing
  • Combined with `-I` for stdin input, evaluates plan.json against policies and blocks bad deployments
12. What is the difference between deny and warn policy rules in OPA?

Expected answer points:

  • `deny` rules: when true, the policy result is a violation that blocks deployment
  • `warn` rules: when true, the policy result is a warning that does not block deployment
  • Use deny for hard requirements (encryption, no public access), warn for soft recommendations
  • You can combine both: some policies deny, others warn depending on severity
  • CI/CD pipelines can be configured to treat warnings as failures for stricter enforcement
13. How do you structure OPA policies for reuse across different resource types?

Expected answer points:

  • Use packages to organize policies by domain: `package terraform.analysis.deny`
  • Create helper functions for common checks: `is_encrypted(rc)`, `has_required_tags(rc)`
  • Package imports let you share functions across policy files
  • Data documents allow external configuration without modifying policy code
  • Test each helper function independently before composing into larger rules
14. What are the limitations of OPA policy evaluation for Terraform plans?

Expected answer points:

  • OPA evaluates plan JSON output, not HCL source—so computed values may not be available
  • Some resource attributes only appear after apply, not in plan output
  • OPA cannot validate things like exact IAM policy structure that requires evaluation
  • Terraform state must exist for import and resource attribute lookups
  • Large plans create large JSON that can slow down policy evaluation
15. How does Sentinel differ from OPA for policy enforcement in Terraform Cloud?

Expected answer points:

  • Sentinel is HashiCorp's commercial policy-as-code product, only works with Terraform Cloud
  • Sentinel uses its own DSL with imperative-style policy writing
  • OPA is open-source and vendor-neutral, can evaluate plans locally before TFC submission
  • Sentinel policies can import Terraform run details via the TFC API
  • Porting policies between OPA and Sentinel is possible but requires rewriting Rego to Sentinel DSL
16. What is the purpose of the future.keywords import in OPA Rego?

Expected answer points:

  • `future.keywords.if` enables the `if` keyword for conditional rules without parentheses
  • `future.keywords.contains` enables the `contains` keyword for set membership checks
  • `future.keywords.default` enables default rule definitions for missing cases
  • These keywords become standard in future OPA versions, future.keywords enables them early
  • Makes Rego more readable: `deny if { condition }` vs `deny { condition }`
17. How do you handle policy exceptions without hardcoding them in Rego?

Expected answer points:

  • Use external data documents to define exceptions rather than hardcoding in policy code
  • Exception list stored as JSON/YAML, imported as data.exception_list in Rego
  • Policy checks if the resource matches an exception before flagging violation
  • Exception list reviewable and manageable without modifying policy logic
  • Prevents policy code pollution from accumulated hardcoded exceptions
18. What is the tradeoff between policy evaluation speed and policy complexity?

Expected answer points:

  • Simple deny rules checking single attributes evaluate fast (milliseconds)
  • Complex rules with nested iterations, multiple helper calls, and deep data traversal are slower
  • For large Terraform plans, slow policies can add minutes to CI pipeline runtime
  • Cache frequently evaluated policy decisions to avoid repeated computation
  • Profile policy evaluation time with `opa eval --explain=full` to identify bottlenecks
19. How do you test OPA policies against mocked Terraform plan data?

Expected answer points:

  • Write test cases using the `with input as` syntax to inject mock data
  • Test positive cases: resources that should pass (no violation returned)
  • Test negative cases: resources that should fail (violation returned)
  • Run `opa test ./policies/ -v` for verbose test output with pass/fail per test
  • Cover edge cases: null values, empty lists, unexpected data structures
20. How does AWS Policy differ from SCPs and when would you use each?

Expected answer points:

  • AWS Policy (IAM) governs what a specific identity can do—user, role, or service
  • SCPs (Service Control Policies) govern what is denied at the organization level across all accounts
  • Use IAM policies for fine-grained resource-level permissions per identity
  • Use SCPs for guardrails that apply to all accounts: deny certain regions, require encryption
  • SCPs are evaluated before IAM policies—SCPs can never grant permissions, only restrict them

Further Reading

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

Choosing a Git Team Workflow: Decision Framework

Decision framework for selecting the right Git branching strategy based on team size, release cadence, and project type.

#git #version-control #branching-strategy

Git Flow: The Original Branching Strategy Explained

Master the Git Flow branching model with master, develop, feature, release, and hotfix branches. Learn when to use it, common pitfalls, and production best practices.

#git #version-control #branching-strategy