Skip to main content

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.8xlarge in dev")
  • platform invariants (e.g., "every resource must carry an Owner tag")
  • 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

NeedOPASentineltfsec / Trivy
Plan-time policy over Terraform JSONBestBest (within HCP)Limited
Static rules on Terraform sourcePossible (Conftest)LimitedBest
Multi-platform (K8s, custom APIs)BestNoK8s yes, not general
Ships with sensible defaultsNo -- you write rulesNo -- you write rulesYes -- 200+ rules built in
Learning curveRego is distinctSentinel is distinctNearly none
Enforcement modelExternal eval, you wire itIntegrated in HCP TerraformExternal eval, you wire it
Free / open-sourceYesNo (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

  1. Add tfsec / Trivy to every Terraform repo's CI today. Accept the noise; silence rules you genuinely disagree with (explicitly, in a config file).
  2. 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 in customer-data-region", "RDS must have deletion_protection = true").
  3. Put policies in version control next to the infra they gate. A repo called policies/ with a README is fine.
  4. When a policy blocks a legitimate plan, treat the fix as a policy PR, not a bypass. Bypassing is how policies lose authority.
  5. 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

  1. Why is a plan-time policy engine sometimes stronger than a static-source scanner, and sometimes weaker?
  2. 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?
  3. Give one policy you would add in your team today that no vendor scanner ships by default.

Mini Drill or Application

In 30 minutes:

  1. Take any .tf you have and run tfsec . (or trivy config .). Read every finding. Decide: fix, ignore (with annotation), or accept and document.
  2. Then terraform plan -out=tfplan; terraform show -json tfplan > plan.json.
  3. Write one Rego rule that fails if any resource is missing an Owner tag. Run it with opa eval -d policy.rego -i plan.json "data.yourpkg.deny".

Capture both outputs into policies/README.md as your starting point.

See also (external)


Source Backbone

Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.