Disclaimer: This article is for education and authorized testing only. Only scan, modify, or attack infrastructure and code repositories you own or have explicit written permission to assess. Misusing leaked credentials or pivoting through misconfigured cloud infrastructure without authorization is illegal.
Introduction / Overview
Infrastructure as Code (IaC) turned infrastructure into text files. That is great for reproducibility, and great for attackers, because now a single careless commit can hand over an entire cloud account. Terraform is the dominant IaC tool, and it concentrates two of the most valuable things an attacker wants: credentials (in state) and a blueprint of the entire environment (in the .tf and state files).
This article walks both sides. Offensively, we look at where secrets leak, how IAM over-permissioning becomes lateral movement, and what configuration drift exposes. Defensively, we wire up tfsec and Checkov into a pipeline that catches these before they ship. The two halves carry equal weight, because finding a misconfiguration you cannot fix is just paperwork.
How it works / Background
Terraform reads .tf configuration, builds a dependency graph, and reconciles it against a state file (terraform.tfstate). The state file is JSON and it stores the actual values of every resource and data source, including resolved secrets: database passwords, generated private keys, IAM access keys, and any sensitive output. Marking an output sensitive = true only hides it from CLI display; it is still plaintext in state.
Three recurring weaknesses:
- State file secrets: state contains plaintext credentials. If it lands in a public S3 bucket, a Git repo, or a misconfigured backend, it is game over.
- Drift: the live environment diverges from code (someone clicks in the console). Drift means your security review of the
.tffiles no longer reflects reality, and emergency console changes are rarely least-privilege. - Least privilege violations:
Action: "*"onResource: "*"is fast to write and a perfect pivot for an attacker who lands on a role.
tfsec and Checkov are static analyzers that parse HCL (and tfsec also evaluates with the trivy engine now) and flag these patterns against rule packs mapped to CIS benchmarks and provider best practices.
Prerequisites / Lab setup
You need Terraform, the two scanners, and a deliberately broken module. Install on macOS/Linux:
# tfsec is now distributed under Trivy, but the standalone binary still works
brew install tfsec
# Checkov is a Python tool
pipx install checkov
# Terraform itself
brew install terraform
tfsec --version
checkov --version
terraform versionBashCreate a vulnerable module to scan:
# main.tf — intentionally insecure, lab only
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "logs" {
bucket = "yunolay-lab-logs"
}
# No encryption, no public access block, world-readable ACL
resource "aws_s3_bucket_acl" "logs" {
bucket = aws_s3_bucket.logs.id
acl = "public-read"
}
resource "aws_db_instance" "db" {
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "admin"
password = "SuperSecret123!" # hardcoded -> ends up in state
publicly_accessible = true
storage_encrypted = false
}
resource "aws_iam_policy" "admin" {
name = "lab-admin"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Action = "*", Resource = "*" }]
})
}HCLWalkthrough / PoC
1. Static scan with tfsec
tfsec . --format lovelyBashtfsec will flag the unencrypted bucket (aws-s3-enable-bucket-encryption), the missing public access block (aws-s3-block-public-acls), the public ACL, the unencrypted RDS instance (aws-rds-encrypt-instance-storage-data), and publicly_accessible. Output is rule-ID keyed so you can suppress or triage precisely.
2. Static scan with Checkov
Checkov has a deeper rule set and supports a graph-based engine that follows variable references:
checkov -d . --compact --quietBashLook for checks like CKV_AWS_16 (RDS encryption), CKV_AWS_17 (RDS not public), CKV_AWS_19/CKV_AWS_145 (S3 encryption), and CKV2_AWS_6 (S3 public access block). To fail a pipeline only on hard findings:
checkov -d . --check CKV_AWS_16,CKV_AWS_17 --soft-fail-on LOWBash3. The state file — the real prize
This is the part attackers care about. Run a plan/apply against a throwaway account, then inspect state:
terraform init && terraform apply -auto-approve
# Secrets are sitting in plaintext:
grep -i password terraform.tfstate
terraform show -json | jq '.values.root_module.resources[] | select(.type=="aws_db_instance") | .values.password'BashThe SuperSecret123! password is right there. Now imagine terraform.tfstate committed to Git or synced to a public S3 bucket. A classic attacker workflow:
# Find leaked state in a repo's history (red team / authorized review)
git log --all --full-history -- "*.tfstate" "*.tfstate.backup"
# Pull credentials and outputs out of any state file found
jq -r '.resources[].instances[].attributes | select(.access_key != null) | .access_key, .secret_key' terraform.tfstateBash4. Drift detection
Drift hides changes from your code review. Detect it with a refresh-only plan:
terraform plan -refresh-only -detailed-exitcode
# exit 0 = no drift, 2 = drift detected, 1 = errorBashIf someone opened a security group to 0.0.0.0/0 in the console, this surfaces it. Pair it with a scheduled CI job so drift is caught in hours, not after a breach. See also our notes on cloud IAM privilege escalation for what an attacker does once that open path exists.
Mermaid diagram

The diagram shows the secure path: scanning gates the PR, an encrypted remote backend protects state secrets, and scheduled drift detection loops any deviation back into review.
Detection & Defense (Blue Team)
Defense here is mostly about removing the attacker's two prizes (state secrets and over-permission) and closing the drift gap.
1. Never store secrets in state in the clear.
- Use a remote backend with encryption at rest and TLS. For S3, enable SSE-KMS and block all public access:
terraform {
backend "s3" {
bucket = "yunolay-tfstate"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:111122223333:key/abcd"
dynamodb_table = "tf-locks" # state locking
}
}HCL- Pull secrets from a secrets manager at runtime (Vault, AWS Secrets Manager) via data sources or
ephemeralresources (Terraform 1.10+ keeps ephemeral values out of state) rather than hardcoding. - Lock the state backend down: bucket policy denying non-TLS, and IAM restricting
s3:GetObjecton the state key to the CI role only.
2. Shift-left scanning, enforced in CI.
Run both tools as a required gate so nothing merges around them:
# .github/workflows/iac.yml
name: iac-scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/tfsec-action@v1.0.0
- name: Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: .
soft_fail: falseYAMLAdd a pre-commit hook so developers see findings before they push:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/aquasecurity/tfsec
rev: v1.28.13
hooks: [{ id: tfsec }]
- repo: https://github.com/bridgecrewio/checkov.git
rev: 3.2.0
hooks: [{ id: checkov }]YAML3. Enforce least privilege.
- Replace
Action: "*"with scoped actions. Useiam-policy-validatoror Checkov'sCKV_AWS_109/CKV_AWS_111(no wildcard write/permissions actions) to block wildcards. - Generate minimal policies from observed CloudTrail activity with tools like AWS IAM Access Analyzer's policy generation, then commit the tightened policy back to Terraform.
4. Detect drift and tampering.
- Schedule
terraform plan -refresh-only -detailed-exitcodeand alert on exit code 2. - Turn on CloudTrail + Config rules to detect console changes outside the pipeline. Map these to MITRE ATT&CK T1578 (Modify Cloud Compute Infrastructure) and T1098 (Account Manipulation) for detection engineering.
- Scan repos and CI logs for leaked state and keys with
gitleaksortrufflehog; treat any matched live credential as compromised and rotate immediately.
5. Provenance and integrity.
- Restrict who can run
apply. Use OIDC-federated CI roles, not long-lived keys, so there are fewer static secrets to steal. This dovetails with the controls in our CI/CD pipeline hardening writeup.
Conclusion
IaC security is not exotic. The same three issues recur: secrets sitting in state, IAM policies that are wildcards because wildcards are easy, and drift that quietly invalidates your last review. tfsec and Checkov catch most of the static problems for free; an encrypted remote backend with locking removes the biggest prize; and scheduled drift detection closes the loop between what you reviewed and what is actually running. Wire all of it into CI as a hard gate, and the attacker's easy wins disappear. For container-layer follow-up, see our container image scanning guide.
References
- tfsec / Trivy documentation — https://aquasecurity.github.io/tfsec/ and https://trivy.dev/
- Checkov documentation — https://www.checkov.io/
- Terraform: Sensitive Data in State — https://developer.hashicorp.com/terraform/language/state/sensitive-data
- Terraform Ephemeral Resources — https://developer.hashicorp.com/terraform/language/resources/ephemeral
- MITRE ATT&CK T1578 (Modify Cloud Compute Infrastructure) — https://attack.mitre.org/techniques/T1578/
- MITRE ATT&CK T1098 (Account Manipulation) — https://attack.mitre.org/techniques/T1098/
- HackTricks Cloud — https://cloud.hacktricks.xyz/
- CIS Amazon Web Services Foundations Benchmark — https://www.cisecurity.org/benchmark/amazon_web_services
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments