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.
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
| Aspect | OPA | Sentinel | AWS Policy |
|---|---|---|---|
| Cost | Free, open-source | Commercial (HashiCorp Enterprise) | Free (native AWS) |
| Scope | Multi-cloud, Kubernetes, CI/CD | HashiCorp tools only | AWS only |
| Language | Rego | Sentinel (DSL) | IAM JSON/YAML |
| Testing | Built-in test framework | Built-in test framework | IAM Access Analyzer |
| CI/CD integration | Native via conftest | Native via Terraform Cloud | Native 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
| Failure | Impact | Mitigation |
|---|---|---|
| Overly strict policies | Developers bypass policies or disable enforcement | Start permissive, tighten incrementally |
| Policy conflicts | Impossible to satisfy two policies simultaneously | Audit policy interactions before deployment |
| Slow policy evaluation | CI pipeline delays | Cache OPA decisions, optimize Rego queries |
| Policies not enforced in CI | Violations reach production | Make policy checks mandatory in pipeline |
| Rego bugs allow violations | False sense of compliance | Write 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.
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.
Audit Trails: Building Complete Data Accountability
Learn how to implement comprehensive audit trails that track data changes, access, and lineage for compliance and debugging.