Cloud-Native Security / Infrastructure Automation

Infrastructure as Code (IaC) & Immutable Security

Infrastructure as Code moves provisioning into version-controlled templates (Terraform, CloudFormation, Bicep, Helm). The security consequence is leverage in both directions: a reviewed, scanned template applies a correct configuration to hundreds of environments, and a single bad default propagates just as widely. Securing it means scanning templates before merge, evaluating the resolved plan for what those templates will actually create, treating state and plan files as secret-bearing artifacts, and replacing drifted infrastructure rather than patching it in place.

IaC Security diagram showing Checkov and Trivy static scanning, plan-stage policy evaluation, drift detection, and immutable infrastructure replacement.

Infrastructure as Code Security Architecture: Template Scans ↔ Plan-Stage Policy Evaluation ↔ Drift Detection & Immutable Replacement

Static IaC security scanning

Static IaC tools analyze infrastructure code manifests before deployment, catching security defects at review time rather than after provisioning:

A left-to-right pipeline. A Terraform commit opens a pull request; static analyzers such as Checkov and Trivy read the template source; terraform plan produces a saved plan which terraform show converts to JSON for a policy engine to evaluate against Rego rules; the gate then either fails the build or allows terraform apply to reach the cloud provider. Two callouts distinguish the template scan, which reads the code as written, from the plan evaluation, which reads the resolved resource set after variables and modules are expanded. A footer notes that a pipeline gate is bypassed by anyone applying locally, so drift detection is what catches changes that never passed the gate.

Template scan and plan evaluation catch different things, and the gate binds only what passes through it

Tool status matters here. Checkov and Trivy are actively developed. tfsec has been folded into Trivy: Aqua consolidated its scanning efforts into Trivy, tfsec receives no new misconfiguration checks, and its final release was v1.28.14 in May 2025. Existing tfsec checks still run, so an established pipeline is not broken, but resources and rules published since then are not covered — prefer trivy config for new work.

Common IaC misconfiguration patterns

Policy-as-Code with OPA and Rego

Policy-as-Code expresses compliance rules as executable code using Open Policy Agent (OPA) and Rego. The critical detail is which input the policy is written against, because the two scanning stages above expose completely different structures.

A policy evaluating parsed template source (the shape Conftest produces from .tf files) sees resources under input.resource:

# Template-level policy, evaluated by Conftest against parsed .tf source.
# OPA 1.0 syntax: `contains` for partial sets, `if` before the rule body.
package terraform.template

deny contains msg if {
    some name
    resource := input.resource.aws_s3_bucket[name]
    not resource.server_side_encryption_configuration
    msg := sprintf("S3 bucket '%v' has no server-side encryption configuration", [name])
}

A policy evaluating the resolved plan sees a different document entirely — planned resources live under planned_values, and proposed changes under resource_changes:

# Plan-level policy, evaluated against `terraform show -json` output.
package terraform.plan

deny contains msg if {
    some change in input.resource_changes
    change.type == "aws_s3_bucket"
    "create" in change.change.actions
    not change.change.after.server_side_encryption_configuration
    msg := sprintf("S3 bucket '%v' would be created without encryption", [change.address])
}

Two things routinely go wrong here:

terraform plan -out=tfplan
terraform show -json tfplan > plan.json
opa eval --input plan.json --data policy/ 'data.terraform.plan.deny'

State files and saved plans are secret-bearing artifacts

This is the exposure most easily missed, because it is created by Terraform itself rather than written by an engineer.

Terraform records resolved attribute values in state, and its documentation states plainly that local state is a plaintext file that includes any secret values defined in the configuration. A database password fetched cleanly from a secret manager still lands in state as plaintext once it is an attribute of a managed resource. Saved plan files have the same property — Terraform warns that a plan file contains sensitive data in cleartext and should be treated accordingly.

Practical consequences:

Module and provider supply chain

IaC templates pull in third-party code the same way an application does. A module sourced from a public registry runs with the pipeline’s cloud credentials at apply time, and a provider binary executes on the runner.

The same reasoning applied to application dependencies is covered in Software Bill of Materials (SBOM) & VEX, and pipeline identity in CI/CD Pipeline Security & Workload Identity.

Drift detection and GitOps reconciliation

Infrastructure Drift occurs when cloud resources are modified out-of-band (e.g. an engineer editing a security group rule in the console) without updating the underlying IaC code:

Drift: actual runtime cloud state ≠ declared IaC repository state

Drift detection is not a nicety bolted onto the pipeline — it is the control that covers the pipeline’s own blind spot. A pipeline gate binds only the changes that pass through the pipeline. Anyone with credentials who runs terraform apply locally, or edits a resource in the console, bypasses every scan and policy check described above. Nothing in CI can prevent that; only detection after the fact catches it.

IaC security review checklist

The checklist below is a journal working model, not a published audit standard. When auditing Infrastructure as Code repositories and deployment pipelines, evaluate these six criteria:

Diagnostic Area Architectural Evaluation Question Verification & Audit Evidence
CI/CD IaC Static Scanning Are all Terraform/Helm changes scanned automatically on pull requests by a maintained scanner? CI/CD build scripts & PR status check logs.
Policy-as-Code Enforcement Are compliance rules enforced against the resolved plan JSON before terraform apply, using current Rego syntax? OPA policy repository, terraform show -json step, CI evaluation output.
State and plan handling Is state held in an encrypted, access-controlled remote backend, with state and plan files excluded from Git and expired from CI artifacts? Backend configuration, .gitignore, bucket policy, artifact retention settings.
Dependency pinning Are module and provider versions pinned and is .terraform.lock.hcl committed? Module source blocks, version constraints, lock file in the repository.
Automated Drift Detection Is runtime infrastructure continuously monitored for out-of-band change, given that the gate cannot prevent it? Drift job schedule and alerts, GitOps sync policy showing self-heal state.
Git Branch Access Control Are direct pushes to main infrastructure branches blocked by mandatory code review gates? Repository branch protection rule settings.
What I need to remember

A template scan reads the code as written; only a plan evaluation sees what variables and modules actually resolve to — and terraform plan -out emits an opaque file, so terraform show -json is the step that produces policy input. Write Rego in OPA 1.0 form (deny contains msg if) against the input shape the stage actually provides. State and saved plan files hold secrets in cleartext, so the backend is a credential store. And a pipeline gate binds only what goes through the pipeline: a local apply bypasses it entirely, which is the whole reason drift detection exists.

Primary references