This issue has been assessed as high severity. Review affected configurations immediately.
Infrastructure as Code promised repeatable, auditable deployments. It delivered — but it also codified every misconfiguration at scale. A single vulnerable Terraform module applied across 200 environments becomes 200 exposed resources simultaneously. 63% of cloud security incidents trace to misconfiguration, and the majority of those originate in IaC. The good news: problems caught in a pull request cost almost nothing to fix. Problems caught in a production incident cost millions.
What Goes Wrong in Terraform
The highest-risk patterns observed across cloud environments share common roots:
Over-permissive IAM is the most common path to privilege escalation. Terraform makes it easy to grant wildcard permissions and easy to forget to remove them.
# BAD: wildcard action and resource — attacker with any foothold escalates to admin
resource "aws_iam_policy" "bad_example" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = "*"
Resource = "*"
}]
})
}
# GOOD: least-privilege, scoped to specific actions and ARNs
resource "aws_iam_policy" "least_privilege" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "arn:aws:s3:::my-app-bucket/*"
}]
})
}
Public-facing storage — S3, Azure Blob, GCS — defaults have improved but misconfiguration remains common when block_public_acls is explicitly disabled.
# BAD: explicitly disabling the account-level public access block
resource "aws_s3_bucket_public_access_block" "bad" {
bucket = aws_s3_bucket.data.id
block_public_acls = false # ← never do this
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
# GOOD: all four settings enabled (this is the AWS-recommended default)
resource "aws_s3_bucket_public_access_block" "good" {
bucket = aws_s3_bucket.data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Encryption at rest disabled, logging not configured, and security groups with 0.0.0.0/0 ingress on sensitive ports round out the top issues.
Scanning with Checkov
Checkov is the dominant open-source IaC scanner with 2,500+ built-in policies for AWS, Azure, GCP, and Kubernetes. Install and run against a Terraform directory:
pip install checkov
# Scan a Terraform directory
checkov -d ./infrastructure/
# Output only failures, as JSON (useful for CI parsing)
checkov -d ./infrastructure/ --output json --compact | jq '.results.failed_checks[] | {id, check_id, resource, file_path}'
# Suppress a specific check with justification (use sparingly)
checkov -d ./infrastructure/ --skip-check CKV_AWS_18
# Check a specific file
checkov -f ./modules/s3/main.tf
Checkov check IDs map to specific risks. Key checks to prioritise:
| Check ID | Risk |
|---|---|
CKV_AWS_18 | S3 access logging disabled |
CKV_AWS_19 | S3 encryption at rest disabled |
CKV_AWS_20 | S3 bucket publicly accessible |
CKV_AWS_40 | IAM policy with admin permissions |
CKV_AWS_57 | S3 bucket versioning disabled |
CKV_AWS_131 | CloudFront origin without HTTPS only |
CKV2_AWS_62 | S3 event notifications not configured |
Scanning with Trivy
Trivy (which replaced tfsec as of 2024) provides comprehensive IaC scanning alongside container and dependency scanning:
# Install
brew install trivy # macOS
# or
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
# Scan Terraform directory
trivy config ./infrastructure/
# Scan with severity filter
trivy config --severity HIGH,CRITICAL ./infrastructure/
# Output as SARIF for GitHub Advanced Security
trivy config --format sarif --output trivy-results.sarif ./infrastructure/
State File Hardening
Terraform state files contain sensitive data — resource IDs, IP addresses, and sometimes secrets passed as variables. State files stored insecurely are a frequent source of credential exposure.
# AWS S3 backend with encryption and access logging
terraform {
backend "s3" {
bucket = "my-org-terraform-state"
key = "prod/terraform.tfstate"
region = "eu-west-2"
encrypt = true # AES-256 server-side encryption
kms_key_id = "arn:aws:kms:eu-west-2:123456789012:key/mrk-abc123" # CMK
dynamodb_table = "terraform-state-lock" # DynamoDB lock table
# Access logging for the state bucket (configure separately)
# Restrict IAM access — only CI/CD role and break-glass admin should have access
}
}
# The state bucket itself should deny public access and require MFA delete
resource "aws_s3_bucket_versioning" "state_versioning" {
bucket = "my-org-terraform-state"
versioning_configuration {
status = "Enabled"
mfa_delete = "Enabled" # requires MFA to delete versions
}
}
Never commit .tfstate or .tfstate.backup files to source control. Add to .gitignore:
*.tfstate
*.tfstate.*
.terraform/
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
CI/CD Integration
The shift-left value of IaC scanning only materialises if it blocks merges. Here’s a GitHub Actions workflow that fails the PR on critical findings:
name: Terraform Security Scan
on:
pull_request:
paths:
- '**.tf'
- '**.tfvars'
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: infrastructure/
framework: terraform
output_format: cli,sarif
output_file_path: console,results.sarif
soft_fail: false # fail the workflow on policy violations
check: CKV_AWS_18,CKV_AWS_19,CKV_AWS_20,CKV_AWS_40 # critical checks
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy IaC scan
uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: infrastructure/
severity: HIGH,CRITICAL
exit-code: 1 # non-zero exit blocks the merge
format: sarif
output: trivy-iac.sarif
Drift Detection
Drift occurs when someone makes a manual change in the console that diverges from the IaC state. Undetected drift creates shadow configurations that scanners never see. Integrate terraform plan outputs into your CI pipeline and alert on non-empty plans in production:
# Detect drift in production — run on a schedule (not on PR)
terraform plan -detailed-exitcode -out=tfplan
# Exit codes: 0 = no changes, 1 = error, 2 = changes present
if [ $? -eq 2 ]; then
echo "DRIFT DETECTED: production state diverges from IaC"
# Alert your security team
fi
Practical Starting Point
If you’re starting from a messy baseline: run Checkov in --soft-fail mode to inventory your existing violations, suppress the lower-severity findings with documented justifications, and set the pipeline to hard-fail only on CRITICAL severity. Expand the critical list over successive sprints. Getting the pipeline in place with imperfect coverage is better than waiting for perfect coverage before enabling it.