Overview
Infrastructure as code security means checking a proposed change before it's provisioned, not auditing it afterward. Policy-as-code frameworks like HashiCorp's Sentinel evaluate against a Terraform plan, the fully resolved set of resources a run is actually about to create or modify, and can block the run from proceeding to apply at all if a rule is violated. That timing is the entire point: a resource that violates policy is stopped before it exists, rather than discovered days later already running, already costing money, or already exposed. Static scanning of the Terraform source itself is complementary but not a substitute, only the plan reflects what variables, data sources, and module composition actually resolve to.
Quick Reference
| Check point | Sees | Can prevent provisioning? |
|---|---|---|
| Source scanning (linting) | Raw Terraform files | Only obvious, statically-visible issues |
| Plan-based policy (Sentinel, OPA/Conftest) | Fully resolved planned resources | Yes, blocks the run before apply |
| Post-apply audit (cloud config scanning) | Already-provisioned live resources | No, detective only, after the fact |
Syntax
# sentinel.hcl - attach a policy to every plan for this workspace
policy "no-public-s3-buckets" {
source = "./no-public-s3-buckets.sentinel"
enforcement_level = "hard-mandatory"
}Examples
# no-public-s3-buckets.sentinel - evaluated against the plan, not the
# source, so it sees actually-resolved config. Checks the legacy inline
# acl argument, the modern separate aws_s3_bucket_acl resource, and
# public-access-block settings that would otherwise allow a public
# bucket policy through; it does not evaluate bucket policy documents
# themselves.
import "tfplan/v2" as tfplan
public_acl_buckets = filter tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket" and rc.change.after.acl is "public-read"
}
public_acl_resources = filter tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket_acl" and rc.change.after.acl is "public-read"
}
open_access_blocks = filter tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket_public_access_block" and
(rc.change.after.block_public_acls is false or
rc.change.after.restrict_public_buckets is false)
}
main = rule {
length(public_acl_buckets) is 0 and
length(public_acl_resources) is 0 and
length(open_access_blocks) is 0
}Check the plan, not just the source
Static scanning of .tf files can't see values that only resolve once variables, data sources, and modules are evaluated. Policy-as-code tools evaluate the plan itself, the concrete, fully-resolved set of resources about to be created, which is the only place a runtime-dependent misconfiguration is actually visible.
Visual Diagram
Common Mistakes
- Relying only on static source scanning and assuming it catches everything a plan-based policy check would, missing runtime-resolved misconfigurations entirely.
- Writing policy-as-code rules but setting them to advisory rather than mandatory enforcement, so a violation is logged but never actually blocks the run.
- Discovering a misconfigured, publicly-exposed resource only via a post-apply cloud security scan, after it's already been live and potentially exploited.
- Treating policy-as-code as a one-time setup instead of updating rules as new resource types and misconfiguration classes are adopted.
Performance
- Plan-based policy evaluation adds a bounded check to the
plan-to-applyworkflow, proportional to the number of resources in the plan and the number of policies evaluated, not the size of the entire infrastructure. - Preventive, plan-stage checks are unambiguously cheaper than the alternative: remediating an already-provisioned, possibly-exploited misconfiguration costs far more than blocking it before it exists.
Best Practices
- Evaluate policy against the plan, not just the source, so runtime-resolved values are actually checked before anything is provisioned.
- Set genuinely important policies to mandatory (hard-blocking) enforcement, not advisory, or they function as a report nobody has to act on.
- Pair plan-based policy checks with post-apply cloud configuration scanning as defense in depth, not as the primary control.
- Keep policy rules current as new resource types are adopted; an IaC security process that never adds new checks quietly falls behind what's actually being provisioned.