Cloud Tech by Victor
SecurityIntermediate

Infrastructure as Code Security

How policy-as-code catches a non-compliant infrastructure change at the plan stage, before anything is provisioned, and why that timing is what makes it a real control rather than a post-incident audit trail.

Updated 2026-07-243 min read

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 pointSeesCan prevent provisioning?
Source scanning (linting)Raw Terraform filesOnly obvious, statically-visible issues
Plan-based policy (Sentinel, OPA/Conftest)Fully resolved planned resourcesYes, blocks the run before apply
Post-apply audit (cloud config scanning)Already-provisioned live resourcesNo, detective only, after the fact

Syntax

hcl
# 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

python
# 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-apply workflow, 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.

Interview questions

What problem does policy-as-code solve that a manual infrastructure change review does not?

A manual review depends on a human noticing a specific misconfiguration, an open security group, an unencrypted storage bucket, in a plan diff that may span hundreds of resources, and that scrutiny has to be repeated consistently by every reviewer on every change. Policy-as-code encodes the same rule once as executable logic and runs it automatically against every plan, so an overly permissive security group is caught the same way on the hundredth change as the first, without depending on which reviewer happened to be paying attention that day.

At what point in the Terraform workflow are Sentinel (or similar policy-as-code) checks evaluated, and why does that timing matter?

Policy checks evaluate against the plan, the output of `terraform plan`, before `terraform apply` actually provisions anything, which means a policy violation blocks the run from proceeding to apply at all. Evaluating against the plan rather than the already-applied state is what makes this a preventive control instead of a detective one; the non-compliant resource is stopped before it exists, not flagged for cleanup afterward once it's already live and potentially already been exploited or has already incurred cost.

Why is scanning IaC source (Terraform files) not sufficient on its own, without also checking the plan?

Static scanning of Terraform source can catch some misconfigurations (a hardcoded insecure default) but can't see values that only exist after variables, data sources, and module composition are actually resolved, an insecure setting could depend on a variable supplied at runtime that static source scanning alone can't evaluate. The plan is the fully resolved, concrete set of resources Terraform is actually about to create or change, which is why policy-as-code tools evaluate the plan, not just the source, as the authoritative point to check against before anything is provisioned.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement