Policy as Code: OPA, Sentinel, tfsec
What This Concept Is
Policy as code = rules about infrastructure (security, cost, compliance) expressed in a machine-checkable language and enforced against plans or live systems. The rules live in version control, review like code, and run in CI. "You can't deploy a public-read S3 bucket" becomes a test, not a wiki page.
The field has three main players plus a supporting cast.
- OPA (Open Policy Agent) -- cloud-native, general-purpose policy engine with the Rego language. Works against JSON input (Terraform plan JSON, Kubernetes admission, API request bodies). CNCF graduated.
- HashiCorp Sentinel -- proprietary policy-as-code framework integrated with Terraform Cloud / HCP Terraform, Vault, Consul, Nomad. Uses a language purpose-built for policy.
- tfsec / Trivy -- static analysis of Terraform source code, not plan JSON. Ships built-in rules for common cloud misconfig (public buckets, unencrypted volumes, wide-open security groups). tfsec is being folded into Aqua's Trivy; both scan Terraform out of the box.
Related: Checkov (Python-based, covers Terraform + CFN + ARM + Kubernetes), Conftest (wraps OPA for generic config files), AWS Config / GCP Policy Intelligence (live-system policies, not plan-time).
Why It Matters Here
Policy as code is how you turn "we should not deploy public S3 buckets" into a rule that a teammate cannot accidentally bypass at 11 p.m. on a Friday.
Specific wins:
- security rules enforced uniformly across all teams and stacks
- cost controls (e.g., "no non-spot instances larger than
m5.8xlargein dev") - platform invariants (e.g., "every resource must carry an
Ownertag") - plan-time rejection instead of apply-time mistakes or post-hoc cleanups
You want this running in CI on every IaC PR, as a required check.
Concrete Example: OPA / Rego Rejecting Public S3 Buckets
Given a terraform plan -out=tfplan converted to JSON (terraform show -json tfplan > plan.json), the following Rego policy rejects any public-ACL bucket:
package terraform.s3
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.actions[_] == "create"
acl := resource.change.after.acl
public_acls := {"public-read", "public-read-write", "authenticated-read"}
public_acls[acl]
msg := sprintf(
"S3 bucket %q has public ACL %q; public access is forbidden.",
[resource.address, acl],
)
}
Run it:
opa eval -d policy.rego -i plan.json "data.terraform.s3.deny"
If the array is empty, the plan passes. If the array contains violation messages, fail the CI job and print the messages. The public-S3 kata in practice/04-iac-katas.md asks you to write and extend this exact policy.
Concrete Example: Sentinel Enforcing a Tag Convention
import "tfplan/v2" as tfplan
required_tags = ["Owner", "Env", "CostCenter"]
mandatory_tags = rule {
all tfplan.resource_changes as _, rc {
rc.type is "aws_instance" implies
all required_tags as t {
rc.change.after.tags contains t
}
}
}
main = rule {
mandatory_tags
}
Sentinel integrates into HCP Terraform runs. Pass = run continues; fail = run stops with the policy message in the UI.
Concrete Example: tfsec Catching a Public Bucket in Source
$ tfsec ./modules/s3
Result #1 CRITICAL Bucket has a public ACL of 'public-read'.
────────────────────────────────────────────────────
modules/s3/main.tf
────────────────────────────────────────────────────
12 resource "aws_s3_bucket_acl" "example" {
13 ] acl = "public-read"
14 }
────────────────────────────────────────────────────
ID aws-s3-no-public-access-with-acl
Impact The contents of the bucket can be accessed publicly.
Resolution Apply a more restrictive bucket ACL.
More Information
https://aquasecurity.github.io/tfsec/latest/checks/aws/s3/no-public-access-with-acl/
tfsec reads your .tf files directly, with no plan required. Fast, free, and catches the ~200 most common misconfigs out of the box. Aqua is migrating tfsec into Trivy, which supports the same rule set plus many other scanners.
Which Tool for Which Job
| Need | OPA | Sentinel | tfsec / Trivy |
|---|---|---|---|
| Plan-time policy over Terraform JSON | Best | Best (within HCP) | Limited |
| Static rules on Terraform source | Possible (Conftest) | Limited | Best |
| Multi-platform (K8s, custom APIs) | Best | No | K8s yes, not general |
| Ships with sensible defaults | No -- you write rules | No -- you write rules | Yes -- 200+ rules built in |
| Learning curve | Rego is distinct | Sentinel is distinct | Nearly none |
| Enforcement model | External eval, you wire it | Integrated in HCP Terraform | External eval, you wire it |
| Free / open-source | Yes | No (commercial) | Yes |
A common production stack: tfsec/Trivy for cheap static rules on every commit + OPA for plan-time custom policy + AWS Config / GCP Policy Intelligence for live-system drift.
Common Confusion / Misconception
"Policy as code means I don't need reviewers." No. Policies are guardrails; reviewers are judges. Policies catch known-bad patterns; reviewers catch semantic mistakes the policy author did not anticipate.
"tfsec / Trivy replace OPA." They do not. tfsec has excellent built-in rules; OPA lets you express arbitrary custom rules your org cares about. You usually want both.
"Sentinel is free." It is only available with HCP Terraform paid tiers. If you use open-source Terraform, OPA + Conftest is the practical equivalent.
"I'll add policy when the code is stable." By then, you have a pile of violations. Add policy from day one; start with five rules and grow.
How To Use It
- Add tfsec / Trivy to every Terraform repo's CI today. Accept the noise; silence rules you genuinely disagree with (explicitly, in a config file).
- Use
terraform show -json+ OPA for three to five organization-specific rules per year. Focus on patterns a generic scanner misses ("all S3 buckets must be incustomer-data-region", "RDS must havedeletion_protection = true"). - Put policies in version control next to the infra they gate. A repo called
policies/with a README is fine. - When a policy blocks a legitimate plan, treat the fix as a policy PR, not a bypass. Bypassing is how policies lose authority.
- Review your policy hits in retrospective -- the ones firing most often are either signal (real risk) or bugs (false positives you should fix).
Check Yourself
- Why is a plan-time policy engine sometimes stronger than a static-source scanner, and sometimes weaker?
- A tfsec rule fails on a resource you deliberately want to create (say, an intentionally public S3 bucket for static website hosting). What is the correct response?
- Give one policy you would add in your team today that no vendor scanner ships by default.
Mini Drill or Application
In 30 minutes:
- Take any
.tfyou have and runtfsec .(ortrivy config .). Read every finding. Decide: fix, ignore (with annotation), or accept and document. - Then
terraform plan -out=tfplan; terraform show -json tfplan > plan.json. - Write one Rego rule that fails if any resource is missing an
Ownertag. Run it withopa eval -d policy.rego -i plan.json "data.yourpkg.deny".
Capture both outputs into policies/README.md as your starting point.
See also (external)
- Open Policy Agent: Documentation -- Rego language,
opa eval, and integrations with Kubernetes / CI. - HashiCorp Sentinel: Documentation -- the purpose-built policy-as-code language for HCP Terraform and the HashiCorp stack.
- tfsec (migrating to Trivy) -- static Terraform scanner with hundreds of default rules; historical home for the tfsec ruleset now part of Trivy.
Source Backbone
Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.
- Pro Git - versioned infrastructure changes, branching, review, and rollback habits.
- Git from the Bottom Up - mental model for stateful change history.
- The Linux Command Line - shell and automation grounding for infrastructure work.