Terraform Security Scanning: Gate IaC Misconfigurations in the PR
A public S3 bucket is not a cloud problem. It is a code problem that became a cloud problem when someone ran terraform apply. The misconfiguration was sitting in a pull request, readable by anyone who looked, before a single resource existed.
That is the case for Terraform security scanning at the pull request. Infrastructure as code turned infrastructure changes into diffs. Diffs can be reviewed, gated and fixed with the same discipline as application code, at the point where fixing them costs one commit instead of an incident.
This post covers what IaC scanning should catch, why the findings belong next to your code findings rather than in a separate cloud console, how to prioritize them without drowning in low-value rules, and how to roll out a PR gate that platform teams will accept.
Why IaC misconfigurations are code findings
Cloud security posture tools look at what is already running. That is useful, and it is also late. By the time a posture tool flags an open security group, the group has been open for as long as it took the next scan to run, and the fix now has to go back through the same pipeline that created it.
An IaC finding points at the declaration: the file, the line and the resource block that will build the misconfigured resource. That has three consequences:
- The owner is obvious. Whoever owns the repository owns the definition. No mapping from cloud account to team required.
- The fix is a change to the definition. Not a console click that the next
applywill silently revert. - It can be stopped before it exists. A gate on the pull request prevents the resource from ever being created wrong.
The OWASP Infrastructure as Code Security Cheat Sheet makes the same argument: treat templates as code, and scan them in the pipeline.
What Terraform security scanning should catch
Most real IaC risk falls into a handful of classes:
- Public exposure. Storage buckets with public ACLs or policies, security groups and firewall rules open to
0.0.0.0/0, internet-facing load balancers in front of internal services. - Missing encryption. Unencrypted volumes, databases and buckets, or encryption without customer-managed keys where policy requires them.
- Over-broad identity. IAM policies with wildcard actions or resources, roles assumable by any principal.
- Weak workload security. Containers running as root, privileged pods, host network or host path mounts in Kubernetes manifests.
- Disabled logging and protection. Access logging turned off, versioning disabled, deletion protection off on production databases.
The formats vary. Terraform and OpenTofu, CloudFormation templates, Pulumi programs in general-purpose languages and Kubernetes manifests all declare the same kinds of resources, and all carry the same kinds of defects.
A worked example: the public bucket
Here is a Terraform definition that makes a bucket publicly readable:
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = false
block_public_policy = false
}
Paired with a bucket policy that grants s3:GetObject to "Principal": "*", this bucket serves its contents to anyone. A scanner reading the definition sees both halves in the same pull request. The fix is equally visible:
block_public_acls = true, block_public_policy = true, ignore_public_acls = true, restrict_public_buckets = true, and a policy scoped to the principals that actually need access.
AWS documents the four settings in Blocking public access to your Amazon S3 storage. The point is not that this is a hard fix. The point is that it is a trivial fix in review and an expensive one after the data has been indexed.
The open security group follows the same pattern. An ingress rule on port 22 with cidr_blocks = ["0.0.0.0/0"] is easy to spot in a diff and easy to scope down to a bastion range. Once applied, it is one of the first things an internet scanner finds.
Prioritizing IaC findings without chasing every rule
IaC rule sets are large, and a repository with hundreds of modules can produce a long list on day one. Severity alone is a poor guide. A low-severity rule on a public, production resource is often more urgent than a high-severity rule on a sandbox. Three questions do most of the prioritization work:
- What does this definition build, and where? A definition for a tier-one production service matters more than one for a developer sandbox. Non-production definitions should rarely page anyone.
- Does the definition declare exposure? A rule that asserts public access, such as an open ingress or a public ACL, is a different class from a rule about a missing tag.
- Can it pivot? A misconfiguration in a low-value component that grants a path into a critical service deserves the critical service's weight.
Where a definition can be matched to a live cloud resource, score it on what that resource actually looks like, not on the template alone. Templates drift from reality through manual changes, and the running resource is what an attacker sees.
Rolling out an IaC PR gate
Platform teams are right to be wary of a gate that blocks infrastructure changes. A bad gate can stop an urgent scaling change during an incident. A rollout that works:
- Baseline first. Compare each pull request against the default branch, so only misconfigurations the change introduces can fire. The existing backlog should never block an unrelated change.
- Observe for two weeks. Record what would have been blocked. Review the list with the platform team and agree which rules are real.
- Scope by framework. Block on Terraform where definitions are mature and reviewed. Warn on Kubernetes manifests that are still churning. Same policy, different confidence.
- Gate specific rules first. Start with the handful of rules that describe direct exposure. Widen later.
- Show the declaration. Every violation should render on the pull request with the file, the line and the misconfiguration, so the reviewer sees the offending block rather than a rule name.
- Keep an exception path. An override with a reason and an expiry, not an informal "just merge it".
Helm and templated manifests
Templated Kubernetes manifests cause false positives when a scanner treats a templated value as missing. A securityContext injected by toYaml at install time is not absent, it is unknown until render. A scanner that treats templated values as unknown, rather than as a violation, avoids flooding Helm-heavy repositories with findings for settings the chart supplies.
How Heeler handles IaC security
Heeler analyzes infrastructure definitions in your repositories and reports each misconfiguration as a finding, tied to the resource it configures. Coverage includes Terraform, OpenTofu, CloudFormation, Pulumi and Kubernetes Workload, Service, Ingress and RBAC manifests. In Kubernetes, each pod spec, container, volume and RBAC rule is assessed on its own terms, and Helm template values are treated as unknown rather than absent.
IaC findings share the same finding model as SAST, the same lifecycle and the same detail view, and sit in their own section so infrastructure work goes to infrastructure owners. Every finding records the resource type it evaluated, and the list opens unfiltered because a low-severity rule on a production resource can outrank a high one in a sandbox. Each finding carries an Autotriage band, Urgent, Plan or Defer, built from business impact (the service's tier and environment), environment impact (declared exposure and pivot to a tier-one service) and threat. Non-production definitions never reach Urgent. Where a declared resource matches a live cloud resource, Heeler scores it on what that resource actually looks like. See the IaC docs for the full model.
At the pull request, IaC guardrails gate on misconfigurations the branch introduces, compared against the default-branch baseline. You filter by severity, confidence, framework and rule ID, and apply Observe, Warn or Block across the standard scope model. Violations render inline on the pull request with the check that fired and the definition file and line. See IaC Guardrails and PR Guardrails.
A checklist for Terraform security scanning
- Scan every repository that contains
.tf, CloudFormation, Pulumi or Kubernetes files, including the ones the platform team forgot about. - Tie each finding to the resource type it builds and the service it belongs to.
- Set service tiers and environments so sandbox definitions do not compete with production.
- Gate new public-exposure and wildcard-IAM misconfigurations on production repositories first.
- Run the gate in observe mode before blocking.
- Review the most widespread failing rules and fix them at the module level, not resource by resource.
FAQ
What is Terraform security scanning?
Terraform security scanning is static analysis of Terraform configuration for misconfigurations such as public storage, open network rules, missing encryption and over-broad IAM, before the infrastructure is applied.
Should IaC findings block pull requests?
Yes, for misconfigurations the change introduces and that describe real exposure. Start in observe mode, scope by framework and gate a small set of high-value rules first.
How is IaC scanning different from cloud posture management?
IaC scanning reads the definition before it is applied and points to the file and line that will build the resource. Posture tools read what is already running.
Which IaC formats need scanning?
Terraform and OpenTofu, CloudFormation, Pulumi and Kubernetes manifests, including Helm charts, cover most infrastructure teams ship.
How do you prioritize IaC misconfigurations?
Weight them by what the definition builds and where: tier, environment, declared exposure and whether the resource can pivot to something critical. Rule severity alone is a weak guide.
Gate infrastructure at the diff
Every misconfigured bucket and open security group was a line in a pull request first. See how Heeler finds IaC misconfigurations in your repositories, scores them against what they actually build and gates new ones before they are applied. Get a demo.


.jpg)
