September 18, 2026

IaC Security Best Practices: Policy Enforcement in Terraform Workflows

A clean terraform plan doesn't mean secure infrastructure. See where shift left security actually needs to run, and how policy enforcement closes the gap

~ min read
~0 min read
Akshat Tandon

TL;DR

  • Shift-left security means catching misconfigurations at the point a Terraform plan is written, not after the resource is already running in production.
  • A clean terraform plan only confirms syntax and provider schema are correct; it says nothing about whether the resulting infrastructure is secure.
  • Real enforcement needs to run at three points: pre-commit, plan-time, and runtime, since each one catches a different class of problem the others miss.
  • Policy as code, through engines like Tirith and OPA, is what turns shift left from a checklist into something a pipeline actually enforces on every run.
  • StackGuardian builds policy evaluation directly into the workflow engine, so shift left enforcement isn't a separate tool bolted onto the pipeline afterward.

Security used to mean a review at the end of the pipeline: infrastructure got built, then someone with a checklist looked at it before it went live. Shift left moved that review earlier, catching problems at the point code gets written rather than after it's deployed. Infrastructure as code is where this idea matters most, since a single misconfigured resource block in a shared module doesn't create one insecure server; it can replicate that mistake across hundreds of resources the moment the plan is applied.

This post covers what shift left security actually requires for Terraform specifically, why a clean plan doesn't guarantee a secure deployment, where policy enforcement needs to sit in the pipeline, and how StackGuardian builds that enforcement into the workflow engine itself rather than treating it as an add-on step.

Shift Left Security in Infrastructure as Code

Shift left security means catching a problem at the point of authorship or plan, not after the resource is already running. Applied to infrastructure as code, that means a misconfigured security group or an unencrypted storage bucket gets flagged while it's still a block of HCL in a pull request, not after terraform apply has already created it.

The contrast with a traditional review model is mostly about timing:

Traditional Review

Shift Left

When it runs

After deployment, during a periodic audit

Before deployment, at commit or plan time

Who catches the issue

A security team, often weeks later

The pipeline, before merge

Cost of a violation

Remediation on live infrastructure

A rejected pull request

Coverage

Whatever the audit happened to sample

Every plan, every time

Infrastructure as code raises the stakes of getting this wrong. A misconfigured module isn't a single mistake; it's a template. If that module gets reused across ten environments, the same open port or missing encryption setting exists in all ten the moment each one applies, and a traditional periodic audit might not catch the pattern until long after the tenth deployment.

Why a Clean Terraform Plan Can Still Deploy Insecure Infrastructure

terraform plan and terraform validate check two things: that the HCL syntax is correct, and that resource arguments match what the provider schema expects. Neither one evaluates whether the resulting infrastructure is secure. A plan can complete with zero errors and still describe a security group open to the entire internet, an S3 bucket with no encryption configured, or an IAM role with far more permission than the workload actually needs.

None of these are syntax problems. cidr_blocks = ["0.0.0.0/0"] is valid HCL. An aws_s3_bucket resource with no server_side_encryption_configuration block is a complete, plan-clean resource. Terraform's plan and validate commands were built to confirm a configuration will apply as written, not to judge whether what it applies is a good idea.

That gap is exactly what policy enforcement exists to close. Something has to sit between "this plan is syntactically valid" and "this plan is safe to apply," evaluating the plan against rules that have nothing to do with HCL correctness and everything to do with what the infrastructure will actually expose once it's running.

Where Security Policy Needs to Run in a Terraform Pipeline

No single checkpoint catches everything. Three distinct points in a pipeline each catch a different class of problem, and skipping any one of them leaves a real gap.

Pre-commit and IDE-level checks run the fastest and catch the most obvious issues before code ever reaches a pull request. A linter or static analysis tool flagging an open security group as someone types it gives the cheapest possible fix, before a review, before a plan, before anything gets shared. What it misses is context: a pre-commit check evaluating a single file in isolation can't see how that resource interacts with the rest of a live environment.

Plan-time enforcement is the real gate. This is where a policy engine evaluates the actual terraform plan output, the full picture of what will change, against a defined ruleset before terraform apply runs. This catches what pre-commit checks miss, since it evaluates the complete plan rather than an isolated file, and it's the last point before infrastructure actually changes.

Runtime and drift detection catches everything that bypassed the pipeline entirely: a manual console change, a resource created outside the approved workflow, or a policy violation introduced after the fact that the original plan never contained. This is the safety net for changes that never went through plan-time evaluation in the first place.

Enforcement Point

Catches

Misses

Pre-commit / IDE

Obvious issues, fast feedback

Anything requiring full plan context

Plan-time

Complete plan evaluation before applying

Changes made outside the pipeline

Runtime/drift

Manual changes, bypassed workflows

Anything already remediated before the next check

Policy as Code as the Enforcement Layer Behind Shift Left

Policy as code means that security and compliance rules are written as structured, version-controlled files rather than as a document someone references manually. A rule lives in a repository, is reviewed through the same pull request process as any other code change, and is evaluated automatically by a pipeline rather than relying on someone to remember to check it.

Two engine types dominate this space today. OPA, using the Rego language, is a general-purpose policy engine that can evaluate the same rule across Terraform plans, Kubernetes manifests, and API requests, since it doesn't care what kind of system is making the request. JSON-based frameworks like Tirith take a more structured approach, purpose-built for evaluating IaC plans specifically, without requiring a team to learn a dedicated policy language.

This is what actually makes shift left enforceable rather than aspirational. A checklist that depends on someone remembering to run it gets skipped the first time a deadline is tight. A policy engine wired into the pipeline evaluates every single plan, every time, with no dependence on anyone remembering anything.

Writing Terraform Security Policies With Tirith and OPA

A Tirith policy blocking a publicly open security group looks like this:

{
  "meta": {
    "required_provider": "stackguardian/terraform_plan",
    "version": "v1"
  },
  "evaluators": [
    {
      "id": "no-open-ingress-cidr",
      "description": "Block security group rules allowing 0.0.0.0/0 ingress",
      "provider_args": {
        "operation_type": "attribute",
        "terraform_resource_type": "aws_security_group",
        "terraform_resource_attribute": "ingress.cidr_blocks"
      },
      "condition": {
        "type": "NotEquals",
        "value": "0.0.0.0/0",
        "error_tolerance": 1
      }
    }
  ],
  "eval_expression": "no-open-ingress-cidr"
}

Three parts do all the work here. meta sets which provider evaluates the plan, stackguardian/terraform_plan in this case. evaluators define the individual check, pointing at a specific resource type and attribute, with a condition that fails if the value matches an open CIDR block. eval_expression combines evaluator results using &&, ||, and !, letting a single policy require multiple checks to pass at once.

error_tolerance controls how the evaluator handles a resource that doesn't exist in a given plan. A value of 0 requires every field to be present and correct, or the policy fails outright. A value of 1 skips the evaluator entirely if the specified resource type isn't in the plan, rather than failing a plan that never touched a security group in the first place.

The same rule in OPA's Rego looks structurally different but does the same job:

package terraform.security_group

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_security_group"
  ingress := resource.change.after.ingress[_]
  ingress.cidr_blocks[_] == "0.0.0.0/0"
  msg := sprintf("Security group '%v' allows unrestricted ingress", [resource.address])
}

Rego's advantage shows up when the same rule needs to run somewhere other than a Terraform plan, a Kubernetes admission controller checking a NetworkPolicy for the same open-ingress pattern, for instance, using the same policy language and largely the same logic.

Enforcing Policy Without Blocking Every Deployment

Setting every policy to hard-block on the first day is a common mistake. A brand-new policy running against months of existing infrastructure will surface false positives, edge cases the rule didn't account for, and legitimate exceptions nobody thought to exempt. Blocking every deployment on day one either halts the team's work entirely or results in the policy being disabled within a week out of frustration.

Graduated enforcement solves this. A policy can warn instead of block, logging a violation and notifying the team without stopping the deployment. It may require explicit approval, routing the plan to a named reviewer rather than blocking it or allowing it silently. Only once a policy's false-positive rate is well understood does it make sense to switch it to a hard fail.

A reasonable rollout looks like this: enable a new policy in warn mode, review what it flags over one or two weeks, tune the rule against real false positives, then switch it to fail once the signal is clean. Skipping straight to fail with an unproven policy is how shift-left security earns a reputation for slowing teams down instead of protecting them.

How StackGuardian Enforces Shift Left Security in Terraform Workflows

StackGuardian runs two policy engines side by side rather than forcing a choice between them. Tirith, the JSON-based framework shown earlier, and native OPA support, sourcing Rego policies directly from a Git repository, both evaluate against the same Workflow.

Policies scope at three levels: organization-wide, a specific Workflow Group, or an individual Connector tied to a cloud account. This maps directly onto the graduated enforcement problem above: a new policy can be scoped to a single Workflow Group in warn mode while it's being tuned, then widened to organization-wide once it's proven, without touching production environments during the tuning period.

Enforcement itself splits into two distinct stages that mirror the pipeline framework covered earlier. Configuration Rules evaluate at workflow creation, update, or runtime, functioning as the plan-time gate: if a Configuration Rule fails, the run is blocked immediately; if it passes or issues a warning, execution proceeds to Runtime Rules, which evaluate during actual execution. Each rule carries one of five statuses, Pass, Fail, Warn, Approval Required, or Unevaluated, giving the graduated enforcement model from the previous section a concrete mechanism rather than just a policy.

The runtime leg of shift left doesn't stop once a resource is deployed. Automated Drift Check runs on a schedule per workflow, catching exactly the class of problem that plan-time enforcement structurally can't: a resource changed manually outside the pipeline entirely; the same gap runtime and drift detection exists to close in any shift left model.

Writing every policy by hand isn't required to get started. The Policy Marketplace has 250+ pre-built templates covering CIS benchmarks, PCI DSS, tagging requirements, and encryption enforcement, which can be subscribed to directly rather than authored from scratch. And every policy evaluation, whether it passed, failed, or triggered approval, lands in Audit Logs, exportable as JSON or CSV, which turns shift-left enforcement into something a compliance review can actually verify happened.

A Shift-Left Pipeline From Commit to Drift Check

Put the whole framework together, and the pipeline looks like this: a pre-commit check flags an obvious issue before the pull request opens. The plan runs, and Configuration Rules evaluate it against Tirith or OPA policies scoped to the relevant Workflow Group, blocking outright on a hard failure or routing to a named approver on a flagged one. Once approved and applied, Runtime Rules continue evaluating during execution. After the resource is live, Automated Drift Check runs on a schedule, catching any changes outside the pipeline going forward.

Every stage in that sequence maps to one of the three enforcement points covered earlier, pre-commit, plan-time, runtime, with nothing left as a manual step someone has to remember. That's the practical difference between a security checklist and a shift-left pipeline: one depends on discipline, the other runs the same way every time regardless of who's under deadline pressure that week.

Conclusion

Shift left security isn't a single tool or a single checkpoint; it's a description of where enforcement happens across a pipeline: as early as authorship, confirmed again at plan time, and backstopped by runtime detection for anything that slips past both. Terraform's own plan and validate commands were never built to make that judgment, which is exactly the gap that policy-as-code exists to close.

What makes the difference between shift left as a slide in a security review deck and shift left as something a team actually gets is whether the enforcement runs automatically on every plan, without anyone having to remember to trigger it. StackGuardian's Tirith and OPA support, mapped onto Configuration Rules, Runtime Rules, and Automated Drift Check, builds that enforcement directly into the workflow engine rather than treating it as a separate tool. Explore StackGuardian's policy engine to see how the three enforcement points fit into a real pipeline.

FAQs

1. What's the difference between shift left security and traditional security reviews?

Traditional security reviews happen after infrastructure is already deployed, often during a periodic audit weeks or months later. Shift-left security moves that check at the point of authorship or planning, catching a misconfiguration before it ever reaches production rather than remediating it after the fact.

2. Can Terraform's own validate and plan commands catch security misconfigurations?

No. terraform validate and terraform plan confirm HCL syntax is correct and that resource arguments match the provider schema. Neither one evaluates whether the resulting infrastructure is secure; an open security group or an unencrypted bucket can pass both commands cleanly, since neither was built to make that judgment.

3. Should security policies block every deployment or just warn?

Neither, universally. New policies should start in warn mode to surface false positives without halting work, then graduate to a hard block once the false-positive rate is well understood. A policy that blocks every deployment from day one, before it's been tuned, tends to get disabled out of frustration rather than fixed.

4. How does StackGuardian decide whether a policy violation blocks a deployment or just warns the team?

Each policy rule has two configurable outcomes: an action when it passes and an action when it errors, with five possible statuses: Pass, Fail, Warn, Approval Required, or Unevaluated. A team sets this per rule, so a tagging violation can log a warning and continue while a production security group change blocks outright or routes to a named approver.

5. Can StackGuardian's drift detection catch a security misconfiguration introduced outside Terraform entirely?

Yes, that's specifically what Automated Drift Check is for. A resource changed manually through a cloud console, outside any Terraform workflow, never goes through plan-time policy evaluation, since that evaluation only runs against changes the pipeline sees. Drift Check runs on a schedule against the live resource, independent of how it changed, closing that gap.

Share article