Cloud Security Wire
AWS Azure GCP RSS
AWSAzureGCPMulti-Cloud Hardening Guide high

IaC Security: Catching Cloud Misconfigurations in Terraform Before They Reach Production

63% of cloud incidents trace back to misconfiguration — and most are catchable in code. This guide covers Terraform security scanning with Checkov and Trivy, state file hardening, and CI/CD integration patterns.

By Cloud Security Wire · ·
#Terraform#IaC#Checkov#Trivy#infrastructure-as-code#shift-left#CI/CD#AWS#misconfiguration#state file
High Severity

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 IDRisk
CKV_AWS_18S3 access logging disabled
CKV_AWS_19S3 encryption at rest disabled
CKV_AWS_20S3 bucket publicly accessible
CKV_AWS_40IAM policy with admin permissions
CKV_AWS_57S3 bucket versioning disabled
CKV_AWS_131CloudFront origin without HTTPS only
CKV2_AWS_62S3 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.

← All Analysis Subscribe via RSS