From Ungated to 100% Gated: Change Control and Security Scanning for Every Production Apply

  • Terraform Cloud
  • HashiCorp Sentinel
  • Wiz
  • ServiceNow

Before

Compliance depended on whoever reviewed the merge request.

What I built

Took production infrastructure changes from ungated to 100% gated: every Terraform apply across 200+ workspaces requires a ServiceNow change record and a clean Wiz scan, backed by a HashiCorp Sentinel policy library enforcing encryption, TLS and tagging standards.

01PlanTerraform02Change recordServiceNow03PolicyHashiCorp Sentinel04ScanWiz05ApplyproductionAny failed check blocks the apply.

Production work described at the level I can share publicly. Code samples are illustrative patterns, not production code; identifiers are documentation placeholders.

The problem: production changes nobody had to justify

  • Infrastructure ran through Terraform Cloud across 200+ workspaces.
  • Nothing technical tied a production apply to change control: an engineer could apply to production without a change record.
  • Nothing technical tied it to a security scan: a misconfigured resource could land before anyone looked at it.
  • In a regulated bank, that's an audit finding waiting to happen, and an incident waiting for a misconfiguration.

The approach: put the controls inside the pipeline, not beside it

Policies that live in a wiki depend on people remembering them. Controls inside the run can't be skipped:

terraform plan
   → HashiCorp Sentinel policies   (is this configuration allowed?)
   → Wiz scan                      (is it secure?)
   → ServiceNow change record      (is it traceable?)
   → apply
   → post-apply record             (what actually happened?)

What I built

1. Change control as a mandatory run task

Every production workspace runs a ServiceNow run task that auto-creates a standard change record before the apply, from a set of domain templates (account, IAM, network, security, application).

The run tasks are mandatory at both pre-apply and post-apply: a workspace can't skip them, and the record captures what was intended and what actually happened.

2. Security scanning as a pre-apply gate

A Wiz run task is attached to every workspace. If a planned resource fails Wiz policy, the apply is blocked.

Illustrative: attaching a mandatory run task with the tfe provider.

resource "tfe_organization_run_task" "wiz" {
  organization = var.organization
  name         = "wiz-scan"
  url          = var.wiz_run_task_url
  hmac_key     = var.wiz_hmac_key
  enabled      = true
}

resource "tfe_workspace_run_task" "wiz" {
  workspace_id      = tfe_workspace.app.id
  task_id           = tfe_organization_run_task.wiz.id
  enforcement_level = "mandatory"     # a failed scan fails the run
  stages            = ["pre_apply"]
}

3. A HashiCorp Sentinel policy library

Applied organization-wide, it evaluates every plan before any API call reaches the cloud provider. What it enforces:

  • S3 buckets must be private and KMS-encrypted
  • No 0.0.0.0/0 ingress on security groups
  • No wildcard IAM actions
  • Only approved AMI owners and approved availability zones
  • Lambda functions must run in a VPC with KMS encryption
  • Each workspace may assume only its own designated IAM role, which blocks privilege escalation from one workspace into another's permissions

Illustrative: a Sentinel policy that blocks public S3 ACLs.

import "tfplan/v2" as tfplan

forbidden = ["public-read", "public-read-write", "authenticated-read"]

acls = filter tfplan.resource_changes as _, rc {
  rc.type is "aws_s3_bucket_acl" and
  (rc.change.actions contains "create" or rc.change.actions contains "update")
}

main = rule {
  all acls as _, a {
    a.change.after.acl not in forbidden
  }
}

And its enforcement level, in sentinel.hcl:

policy "restrict-s3-public-acl" {
  source            = "./restrict-s3-public-acl.sentinel"
  enforcement_level = "hard-mandatory"
}

4. Separation of duties

20+ teams on tiered permission levels (plan-only, apply, output-read), so the people who propose a change aren't automatically the people who apply it.

5. The gates themselves as code

Workspaces, teams, run tasks and policy sets are all defined in Terraform. Nobody can quietly detach a gate through the UI; removing one is a reviewed code change.

Things to watch

  • A control can break production too. A Sentinel policy once failed to match correctly and blocked applies across the entire organization. I found the root cause and fixed it. Treat a policy library like code: test it against real plans and roll out changes in advisory mode first.
  • Fail open or fail closed? A mandatory run task fails the run if the service doesn't answer. Decide in advance who can override that, and how.
  • Emergencies still need a fast path, one that leaves the same audit trail as a normal change.
  • Standard changes need honest templates. Auto-created standard changes only make sense for genuinely low-risk, repeatable work. Anything else should go through normal change review.
  • Enforcement levels matter. advisory warns, soft-mandatory can be overridden by an authorized user, hard-mandatory can't be. Choose per policy.

What it solves

  • Production changes went from ungated to 100% gated: every production apply now needs a change record and a clean security scan.
  • Misconfigurations are blocked before deployment, not found afterwards.
  • A full audit trail for every infrastructure change, created automatically.
  • Closed-loop fixes: when Wiz flagged an overly permissive role, the fix shipped in a patch release of the shared IAM module in the same sprint.

What it doesn't solve

  • Changes that never pass through Terraform. Console changes, drift and newly disclosed vulnerabilities in running resources are invisible to a pre-deploy gate. Post-deployment posture scanning and drift detection are still needed.
  • "Zero findings." A gate stops new misconfigurations; it doesn't make an estate finding-free. Don't claim it does.
  • Bad policy logic. A policy that matches the wrong thing either blocks everything (see "Things to watch") or silently allows what it should block. Test both directions.

Checklist

  • [ ] Every production workspace has mandatory change-control and security run tasks
  • [ ] Run tasks and policy sets defined as code, not toggled in the UI
  • [ ] Sentinel policies tested against real plans, rolled out advisory first
  • [ ] Enforcement level chosen deliberately for each policy
  • [ ] Workspaces restricted to their own IAM role
  • [ ] Plan, apply and output-read permissions split across teams
  • [ ] A documented emergency path that still leaves an audit trail
  • [ ] Post-deployment scanning for everything Terraform can't see

Further reading

Get in touch