Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Infrastructure as Code (IaC) tools like Terraform, CloudFormation, and Pulumi let teams define cloud infrastructure declaratively and apply it repeatedly and predictably. That same property is what makes IaC dangerous from a security standpoint: a single flawed module — an open security group, an unencrypted storage bucket, a hardcoded credential — is no longer a one-off mistake, it is a template that gets copy-pasted, module-sourced, and re-applied across every environment that consumes it. A misconfiguration that would once have been confined to one server now ships to staging, production, and every future account created from the same module.
Because Terraform state is a full, unredacted snapshot of every resource attribute — including plaintext secrets passed in as arguments — IaC introduces a second exposure surface beyond the .tf source itself: the terraform.tfstate file. Anyone who can read state can read every secret ever passed into a resource, regardless of whether the source code looks clean. Combined with over-privileged CI/CD runner credentials, IaC misconfiguration is one of the most consistent findings in cloud security assessments.
Attack Prerequisites
Exploiting Terraform misconfigurations generally requires one of the following positions:
- Read access to the Terraform source (a public or leaked private repo) to identify the misconfigured resource before it is even deployed.
- Network reachability to a resource the code exposed — e.g. a security group opened to
0.0.0.0/0on SSH/RDP/DB ports, or apublicly_accessible = trueRDS instance. - Read access to the state backend — an S3 bucket, Terraform Cloud workspace, or local
.tfstatefile — which stores every resource attribute, including secrets, in plaintext JSON. - CI/CD or IAM access to the pipeline identity that runs
terraform apply, which is often over-privileged relative to what a single pipeline should need.
How It Works
Terraform compiles .tf configuration into a dependency graph, diffs it against the current state, and calls the relevant cloud provider APIs to reconcile the two. Nothing in this pipeline evaluates whether a resource is *safe* — only whether it is syntactically valid and whether the provider accepts the API call. A security group rule of cidr_blocks = ["0.0.0.0/0"] on port 22 is just as valid to Terraform and AWS as a rule scoped to a corporate CIDR; the tool has no opinion on blast radius. That gap is exactly why a separate class of tools — static IaC scanners — exists to sit in the pipeline and apply security opinions Terraform itself does not.
The second mechanism worth understanding is state. Every resource block’s final attributes, including ones that never appear in the .tf file itself (auto-generated IDs, ARNs, and any argument passed as a literal or resolved variable — including passwords), are serialized into terraform.tfstate as plaintext JSON so future runs can compute diffs. Terraform intentionally does not encrypt or redact this file at rest by default; it relies on the backend (S3, Terraform Cloud, Consul) to provide encryption and access control. A state file is therefore a higher-value target than the source code that produced it, because it contains the *resolved* values, not variable references.
Modules compound both problems: a team that wraps a vulnerable pattern into a shared module — say, an RDS module that defaults storage_encrypted to false — propagates that default to every team calling module "db" { source = "git::...//rds" }, usually without anyone re-reviewing the defaults at the call site.
Vulnerable Code / Configuration
The following module combines three of the most common real-world IaC findings: an internet-exposed administrative port, an unencrypted data store, and a hardcoded secret passed directly as a resource argument.
resource "aws_security_group" "web" {
name = "web-sg"
description = "Allow app traffic"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH from anywhere" # VULNERABLE
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "RDP from anywhere" # VULNERABLE
from_port = 3389
to_port = 3389
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_s3_bucket" "data" {
bucket = "corp-app-data"
# No aws_s3_bucket_server_side_encryption_configuration attached -> VULNERABLE
}
resource "aws_db_instance" "app" {
identifier = "app-db"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 20
storage_encrypted = false # VULNERABLE
publicly_accessible = true # VULNERABLE
username = "admin"
password = "SuperSecretPassw0rd!" # VULNERABLE: hardcoded, ends up in state
}
HCLEach line marked above is independently exploitable and independently flagged by static scanners: the two 0.0.0.0/0 ingress rules expose SSH and RDP to the entire internet; storage_encrypted = false with publicly_accessible = true puts an unencrypted database directly on the public internet; and the literal password argument means the plaintext credential is written into terraform.tfstate on every apply, regardless of whether it is later moved to a variable.
Walkthrough / Exploitation
Step one is what should have happened before this ever merged: run static scanners against the module. Each tool catches a slightly different subset, which is why real pipelines run more than one.
# tfsec - fast, purpose-built Terraform scanner (now part of Trivy)
tfsec .
# Checkov - broad multi-framework scanner (Terraform, CFN, k8s, Dockerfile)
checkov -d . --framework terraform
# Terrascan - OPA/Rego-backed policy engine for IaC
terrascan scan -i terraform -d .
# Representative findings:
# Result 1: SECURITY GROUP RULE ALLOWS INGRESS FROM 0.0.0.0/0 [aws_security_group.web]
# Result 2: S3 bucket does not have encryption enabled [aws_s3_bucket.data]
# Result 3: RDS instance is publicly accessible with encryption disabled [aws_db_instance.app]
# Result 4: Potential hardcoded secret in aws_db_instance.app.password
BashIf the misconfiguration ships anyway, the next step is simply confirming exposure — from the outside, this looks like any other unauthenticated recon against a target IP range:
nmap -Pn -p 22,3389 203.0.113.10
# 22/tcp open ssh
# 3389/tcp open ms-wbt-server
BashThe higher-value path is the state file. If the Terraform backend (commonly an S3 bucket) is itself misconfigured for public or overly broad read access — a very common companion finding — the plaintext RDS credential can be pulled straight out of it without touching the database at all:
aws s3 ls s3://corp-tfstate-prod/ --no-sign-request
aws s3 cp s3://corp-tfstate-prod/env/prod/terraform.tfstate . --no-sign-request
jq -r '.resources[] | select(.type=="aws_db_instance") | .instances[0].attributes.password' terraform.tfstate
# SuperSecretPassw0rd!
psql -h app-db.xxxxxxxx.us-east-1.rds.amazonaws.com -U admin -d appdb
BashNote:
terraform plan -out=tf.planfollowed byterraform show -json tf.planis worth reviewing in CI even when scanners pass, because it shows the fully resolved, post-interpolation values that will actually be applied — catching cases where a variable default or afor_eachexpansion introduces a misconfiguration that the raw.tfsource doesn’t show.
Opsec: Never commit real
.tfstatefiles,.tfvarswith secrets, orterraform planoutput to version control, even in a private repo — state has leaked via public repos, CI artifact caches, and misconfigured backup buckets far more often than through direct backend compromise.
Detection and Defense
IaC security is best enforced as a pipeline gate, not a one-time audit, since the whole point of IaC is that the same flawed pattern gets reapplied continuously:
- Scan in CI, before apply — run
tfsec/checkov/terrascanas a required check on every pull request, and fail the build on high-severity findings rather than relying on manual review. - Policy as code — enforce organization-wide rules with OPA/Conftest, Sentinel, or checkov custom policies (e.g. “no ingress from 0.0.0.0/0 on ports 22/3389”, “all S3 buckets must have SSE enabled”) so the rule is structural, not reviewer-dependent.
- Encrypt and lock down state — use an S3 backend with
encrypt = true, a customer-managed KMS key, a bucket policy denying public access, and DynamoDB state locking, or use Terraform Cloud/Enterprise which handles this by default. - Never pass secrets as literals — source credentials from AWS Secrets Manager, SSM Parameter Store, or Vault via data sources, and prefer IAM roles / managed identities over static credentials entirely.
- Least-privilege pipeline identities — scope the IAM role that runs
terraform applyto only the resource types and accounts it actually needs, so a compromised runner cannot pivot cluster-wide.
On the detection side, CloudTrail on the state bucket (GetObject on terraform.tfstate from unfamiliar principals or IP ranges), VPC Flow Logs showing inbound connections to the exposed ports from unexpected sources, and drift-detection runs (terraform plan in CI comparing live state to source) all surface exploitation attempts or silent manual changes.
Real-World Impact
Publicly exposed, misconfigured storage and databases originating from IaC templates are among the most frequently reported findings in cloud breach post-mortems and in commercial cloud security posture management (CSPM) reports — open security groups and unencrypted S3 buckets consistently rank at the top of these findings because a single vulnerable module or reused example snippet gets deployed unmodified across dozens of accounts. The pattern is rarely a novel exploit; it is a known-bad default that shipped because no automated gate caught it before terraform apply.
Conclusion
Terraform and other IaC tools do not introduce new vulnerability classes so much as they multiply old ones — an open security group or an unencrypted datastore is not new, but IaC turns one bad line into an organization-wide default. Treating tfsec/checkov/terrascan as a mandatory CI gate, encrypting and access-controlling state, and keeping secrets out of resource arguments entirely closes the gap between what Terraform will happily apply and what is actually safe to run.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments