IaC security & policy as code
Across repeated industry incident analyses, misconfiguration — not a novel zero-day — is consistently named the leading cause of cloud security incidents: a storage bucket left public, a security group open to 0.0.0.0/0, encryption left off by default. Every one of those misconfigurations exists as a line of text in an infrastructure-as-code definition before it ever exists in a running cloud account, which means it can be caught before deployment instead of after. This page covers how to scan IaC for known-bad patterns, how to express security rules as policy as code instead of a wiki page, and why the check can't stop at deploy time.
A building department doesn't wait for a house to be built and occupied before checking whether the electrical plan violates code — it reviews the blueprint first, because it's far cheaper to redline a drawing than to tear out finished wiring. IaC scanning is the blueprint review: the "blueprint" is a Terraform file, and the reviewer is a program that already knows a hundred ways blueprints go wrong. Policy as code is what turns "the inspector's private judgment" into "a published, versioned building code anyone can read and any inspector applies the same way" — the rule stops living in one person's head and starts living in a file everyone can check against.
Misconfiguration is the incident, not the exception
It's tempting to picture cloud breaches as the result of sophisticated exploitation — a novel bug in a hypervisor, a chained zero-day. In practice, the dominant pattern is far more mundane: a resource was provisioned with the wrong defaults and nobody caught it before it went live. A storage bucket created with public-read access, a security group with an inbound rule spanning all ports and all source IPs, a database instance with encryption-at-rest left disabled because it wasn't explicitly turned on — none of these require an attacker to find a bug. They only require the attacker to notice the door was left open, and internet-wide scanners exist specifically to notice that within minutes of a resource going live.
Infrastructure as code (IaC) — Terraform, CloudFormation, Pulumi, Bicep — didn't create this problem, but it changes where it can be caught. Before IaC, a misconfigured security group was a state that existed only inside a cloud console, invisible to review until someone went looking for it. As a Terraform file, that same security group is a text diff in a pull request: reviewable, diffable, and — critically — scannable by a program before terraform apply ever runs. The DevOps course's infrastructure-as-code page covers the mechanics of defining infrastructure this way; this page covers the security layer that sits on top of it once the infrastructure is defined as code.
Scanning IaC for known-bad patterns
IaC scanners are essentially static analysis for infrastructure definitions: tools like Checkov, tfsec, Terrascan, and Snyk IaC parse a Terraform plan or CloudFormation template and check each resource block against a library of rules for known-bad patterns. A typical rule set flags an S3 bucket or Cloud Storage bucket with public read/write access, a security group or NSG rule with an unrestricted source CIDR, an RDS or Cloud SQL instance provisioned without encryption at rest, an IAM policy granting a wildcard Action: "*" on a wildcard resource, and a load balancer or API listener accepting plaintext HTTP where TLS should be mandatory. These checks map closely to benchmarks like the CIS Benchmarks for AWS/Azure/GCP, so a scanner failing a check is usually failing a specific, citable line item, not an arbitrary opinion.
The value of this scanning depends entirely on where it runs. Run once, manually, before a big migration, it catches a snapshot of problems and lets new ones accumulate again immediately after. Run as a required step in the pipeline — alongside the SAST, DAST, and SCA stages covered in security in CI/CD — it catches every misconfiguration before it's ever applied, on every pull request that touches infrastructure, at the exact moment fixing it is cheapest: a one-line diff instead of a live incident. Most teams wire the scanner in at two points — a fast pass on terraform plan output in the pull request, and a slower, more exhaustive pass against the full IaC repository on a schedule — because a plan-time scan only sees what that specific change touches, not the accumulated state of everything already deployed.
Policy as code: rules that are versioned and machine-checkable
A scanner's built-in rule library covers common, generic misconfigurations, but every organization also has rules that are specific to it — "no resource may be tagged without a cost-center label," "only the platform-eu team may provision resources in eu-west-1," "no S3 bucket may be created without a matching CloudTrail data event rule." Historically, rules like this lived in a security wiki page, a PDF standard, or an architecture review board's institutional memory — none of which a computer can check, and none of which a developer reliably reads before opening a pull request.
Policy as code is the practice of expressing those organizational rules as versioned, executable code instead. A general-purpose policy engine — the Open Policy Agent (OPA) and its Rego language are the reference implementation most tools converge on, alongside newer alternatives like HashiCorp Sentinel — evaluates a structured input (a Terraform plan JSON, a Kubernetes admission request, an API call) against a policy written as code and returns an allow/deny decision. Because the policy is a file in a repository, it gets the same treatment as any other code: reviewed in a pull request, versioned so you can see exactly when a rule changed and who changed it, tested with unit tests against known-good and known-bad inputs, and applied identically in every environment instead of depending on which reviewer happened to be paying attention that day.
package terraform.s3
import future.keywords.in
# Deny creating an S3 bucket if its ACL grants public read access.
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_acl"
resource.change.after.acl in ["public-read", "public-read-write"]
msg := sprintf(
"resource %q: S3 bucket ACL %q is publicly readable; use a private ACL and grant access via bucket policy instead",
[resource.address, resource.change.after.acl],
)
}That rule is deliberately unremarkable — a resource type check, a value comparison, a formatted message — and that's the point. Policy as code isn't a research problem; it's a way of taking a rule an engineer can already state in one sentence and making it something CI can enforce on every plan, every time, without a human remembering to look.
Soft guardrails vs. hard gates
Not every policy violation should stop a deploy, and conflating the two is a common way policy-as-code programs lose credibility. A soft guardrail evaluates the policy, surfaces a warning — in the pull request, in a Slack notification, as an annotation on the plan output — and lets an engineer proceed anyway, typically with a required justification or an approval from a second reviewer. A hard gate evaluates the same kind of policy but blocks the pipeline outright: the merge is disallowed, or the apply step fails, and there is no path to production without either fixing the resource or going through an explicit, logged exception process outside the normal pipeline.
- Use a soft guardrail for rules with legitimate exceptions or a real false-positive rate — a wildcard IAM action that's actually scoped correctly via a condition block the scanner doesn't parse, or a new rule category still being tuned against the existing infrastructure estate.
- Use a hard gate for rules with no legitimate exception in your organization — publicly writable storage holding customer data, disabled encryption at rest for a regulated data store, a database exposed directly to the internet.
The practical failure mode runs in both directions. Making everything a hard gate on day one, before rules are tuned against real infrastructure, trains engineers to route around the pipeline entirely — a local terraform apply with hand-edited state, or a manual console change nobody reviews. Making everything a soft guardrail forever means the highest-severity findings pile up as warnings that get dismissed by habit. The workable pattern most mature programs converge on: launch new rules as guardrails, watch the finding rate and false-positive rate for a few weeks, then promote the rules that hold up to hard gates — see compliance & governance for how that promotion decision typically gets formalized and who signs off on it.
A hard gate only blocks what goes through the pipeline. Anyone with standing cloud console access or a local set of credentials can still run terraform apply directly, or click a change into existence in the AWS console, completely bypassing every policy check you've written. Hard gates in CI are necessary but not sufficient — they need to be paired with restricting direct write access to the cloud account itself, ideally down to just the pipeline's own service identity, or the gate is security theater with a very convincing UI.
Pre-deploy scanning isn't the whole story: drift detection
Everything above catches problems before a resource is created. It says nothing about a resource that was compliant at deploy time and stopped being compliant afterward — a security group rule added manually during an incident and never removed, a bucket policy loosened by an engineer debugging something at 2 a.m., a resource that fell out of Terraform's management entirely. This gap is exactly what configuration drift is: the live state of a cloud environment diverging from the IaC definition that's supposed to describe it.
Drift detection runs continuously against already-deployed infrastructure rather than against a pending change — tools like terraform plan run on a schedule against existing state, AWS Config rules, or a dedicated cloud security posture management (CSPM) platform periodically re-evaluate every live resource against the same policy set used at deploy time, and alert or auto-remediate when something no longer matches. Cloud security posture covers this ongoing monitoring layer in depth; the point to take from this page is narrower: pre-deploy IaC scanning and policy-as-code gates catch what's about to be created, drift detection catches what already exists and quietly changed. A program that only runs the pre-deploy check has a false sense of coverage — the environment can still fail every rule it started out passing.
Pre-deploy scanning, policy as code, and drift detection are the same rule set applied at three different points in a resource's lifecycle: before it's created, at the moment it's created, and for as long as it continues to exist. A misconfiguration caught at any one of those three points is far cheaper to fix than the same misconfiguration discovered by an attacker or an auditor.
1. What does the evidence say is the leading cause of cloud security incidents, and why does defining infrastructure as code make that easier to catch early? 2. What's the practical difference between a soft guardrail and a hard gate, and when would you choose each? 3. Why does an organization need policy as code in addition to a scanner's built-in rule library? 4. Why isn't pre-deploy IaC scanning sufficient on its own, even paired with hard gates?
Check your answers
- Misconfiguration — things like public storage buckets, overly open security groups, and disabled encryption — rather than novel exploits. Because that misconfiguration exists as a reviewable, scannable text diff in the IaC definition before the resource is ever created, it can be caught in a pull request instead of discovered after it's live.
- A soft guardrail warns but allows an engineer to proceed (with justification or approval); a hard gate blocks the pipeline outright with no path to production short of an explicit exception process. Use guardrails for rules with legitimate exceptions or an unproven false-positive rate, and hard gates for violations with no legitimate exception in the organization.
- A scanner's built-in library covers generic, industry-common misconfigurations, but it doesn't know an organization's own rules — team ownership boundaries, tagging requirements, region restrictions. Policy as code lets those organization-specific rules be written, versioned, reviewed, and enforced the same way any other code is, instead of living in a wiki page nobody reads.
- Because it only catches problems at the moment of creation. A resource that passed every check at deploy time can still drift out of compliance afterward through a manual console change or an out-of-band edit, and only ongoing drift detection re-evaluating live state catches that.