Home/Blog/Terraform Security Best Practices: Secure Your Infrastructure as Code
Cloud Security

Terraform Security Best Practices: Secure Your Infrastructure as Code

Learn essential Terraform security practices to prevent misconfigurations, protect secrets, and enforce compliance across your cloud infrastructure.

By InventiveHQ Team
Terraform Security Best Practices: Secure Your Infrastructure as Code

Infrastructure as Code (IaC) revolutionized cloud provisioning—but it also introduced new security risks. A single misconfigured Terraform resource can expose databases to the internet or grant excessive permissions across your entire cloud environment.

This guide covers essential Terraform security practices to protect your infrastructure from misconfigurations, credential exposure, and compliance violations.


Why Terraform Security Matters

Terraform configurations define your production infrastructure. Security issues in code become security issues in production:

  • Misconfigurations deploy automatically - A public S3 bucket in code means a public bucket in AWS
  • Secrets in state files - Terraform state contains sensitive data in plain text
  • Blast radius is large - One terraform apply can affect hundreds of resources
  • Configuration drift - Manual changes create discrepancies between code and reality

The good news: Because infrastructure is code, you can apply software security practices—code review, testing, scanning, and version control.


1. Secure Your State Files

Terraform state files contain sensitive information including resource IDs, IP addresses, and sometimes passwords or connection strings.

Use Remote State with Encryption

Never store state locally in production. Use encrypted remote backends:

AWS S3 with encryption:

terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/infrastructure.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

Azure Storage with encryption:

terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "tfstateaccount"
    container_name       = "tfstate"
    key                  = "prod.terraform.tfstate"
  }
}

GCP Cloud Storage:

terraform {
  backend "gcs" {
    bucket = "mycompany-terraform-state"
    prefix = "terraform/state"
  }
}

Enable State Locking

Prevent concurrent modifications that can corrupt state:

# AWS: DynamoDB for locking
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Restrict State Access

Limit who can read state files—they contain your infrastructure blueprint:

# S3 bucket policy restricting access
resource "aws_s3_bucket_policy" "state_policy" {
  bucket = aws_s3_bucket.terraform_state.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "DenyUnencryptedUploads"
        Effect    = "Deny"
        Principal = "*"
        Action    = "s3:PutObject"
        Resource  = "${aws_s3_bucket.terraform_state.arn}/*"
        Condition = {
          StringNotEquals = {
            "s3:x-amz-server-side-encryption" = "AES256"
          }
        }
      }
    ]
  })
}

2. Never Hardcode Secrets

Hardcoded credentials in Terraform files end up in version control, state files, and logs.

Use Variables for Secrets

# variables.tf
variable "database_password" {
  description = "RDS master password"
  type        = string
  sensitive   = true  # Prevents logging
}

# main.tf
resource "aws_db_instance" "main" {
  # ...
  password = var.database_password
}

Integrate with Secrets Managers

Pull secrets at apply time:

AWS Secrets Manager:

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/database/password"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

Azure Key Vault:

data "azurerm_key_vault_secret" "db_password" {
  name         = "database-password"
  key_vault_id = azurerm_key_vault.main.id
}

resource "azurerm_mssql_server" "main" {
  administrator_login_password = data.azurerm_key_vault_secret.db_password.value
}

HashiCorp Vault:

data "vault_generic_secret" "db" {
  path = "secret/data/database"
}

resource "aws_db_instance" "main" {
  password = data.vault_generic_secret.db.data["password"]
}

Use Environment Variables

For CI/CD pipelines:

export TF_VAR_database_password="secure-password-from-vault"
terraform apply

3. Scan Configurations Before Deployment

Catch misconfigurations before they reach production using IaC scanning tools.

ToolStrengths
Checkov1000+ policies, multi-framework (Terraform, CloudFormation, K8s)
tfsecTerraform-specific, fast, integrates with pre-commit
TerrascanPolicy as code, custom rules, CI/CD integration
Snyk IaCDeveloper-friendly, fix suggestions, IDE plugins
KICSOpen source, 7000+ queries, multiple formats

Integrate Scanning in CI/CD

GitHub Actions example:

name: Terraform Security Scan

on: [pull_request]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ./terraform
          framework: terraform
          output_format: sarif

      - name: Run tfsec
        uses: aquasecurity/[email protected]
        with:
          soft_fail: true

Common Issues Scanners Catch

  • Public S3 buckets or storage accounts
  • Unencrypted databases and storage
  • Overly permissive security groups
  • Missing logging and monitoring
  • Hardcoded secrets
  • Missing tags for cost allocation

4. Enforce Policies with Sentinel or OPA

For enterprise environments, enforce security policies that block non-compliant deployments.

HashiCorp Sentinel (Terraform Cloud/Enterprise)

# Require encryption on all S3 buckets
import "tfplan/v2" as tfplan

s3_buckets = filter tfplan.resource_changes as _, rc {
  rc.type is "aws_s3_bucket" and
  rc.mode is "managed" and
  (rc.change.actions contains "create" or rc.change.actions contains "update")
}

encryption_enabled = rule {
  all s3_buckets as _, bucket {
    bucket.change.after.server_side_encryption_configuration is not null
  }
}

main = rule {
  encryption_enabled
}

Open Policy Agent (OPA)

# deny_public_buckets.rego
package terraform

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  resource.change.after.acl == "public-read"
  msg := sprintf("S3 bucket %s cannot be public", [resource.address])
}

5. Use Modules for Consistency

Modules encapsulate security best practices so teams don't reinvent security for every resource.

Create Secure Baseline Modules

# modules/secure-s3-bucket/main.tf
resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket = aws_s3_bucket.this.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Enabled"
  }
}

Use Verified Modules

Leverage community modules with security built in:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"

  name = "production-vpc"
  cidr = "10.0.0.0/16"

  enable_nat_gateway     = true
  single_nat_gateway     = false
  enable_vpn_gateway     = false
  enable_dns_hostnames   = true
  enable_flow_log        = true
}

6. Implement Least Privilege for Terraform

The credentials Terraform uses should have only the permissions needed.

Create Dedicated IAM Roles

# Role for Terraform with limited permissions
resource "aws_iam_role" "terraform" {
  name = "terraform-deployment-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

# Attach only necessary policies
resource "aws_iam_role_policy_attachment" "terraform_vpc" {
  role       = aws_iam_role.terraform.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonVPCFullAccess"
}

Use Short-Lived Credentials

In CI/CD, use OIDC federation instead of long-lived access keys:

# GitHub Actions with OIDC
- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/terraform-github-actions
    aws-region: us-east-1

7. Version Control Best Practices

Use Branch Protection

Require reviews before merging infrastructure changes:

  • Require pull request reviews
  • Require status checks (security scans) to pass
  • Require signed commits
  • Restrict who can push to main

Tag Sensitive Files in .gitignore

# Never commit these
*.tfvars
*.tfstate
*.tfstate.backup
.terraform/
*.pem
*.key

Review Plans Before Apply

Always review terraform plan output:

# Generate plan file
terraform plan -out=tfplan

# Review the plan
terraform show tfplan

# Apply only after review
terraform apply tfplan

8. Enable Drift Detection

Configuration drift—manual changes outside Terraform—creates security blind spots.

Detect Drift Regularly

# Refresh state and detect drift
terraform plan -refresh-only

# In CI/CD, fail if drift detected
terraform plan -detailed-exitcode
# Exit code 2 means changes detected

Use Terraform Cloud Drift Detection

Terraform Cloud can automatically detect and alert on drift:

terraform {
  cloud {
    organization = "mycompany"
    workspaces {
      name = "production"
    }
  }
}

Security Checklist

PracticePriority
Remote state with encryptionCritical
State file access controlsCritical
No hardcoded secretsCritical
Pre-commit security scanningHigh
CI/CD security gatesHigh
Secure modules for common patternsHigh
Least privilege for Terraform credentialsHigh
Policy enforcement (Sentinel/OPA)Medium
Drift detectionMedium
Signed commitsMedium

Frequently Asked Questions

What's the biggest Terraform security risk?

Exposed state files and hardcoded secrets are the most common issues. State files contain your entire infrastructure blueprint and often include sensitive values. Always use encrypted remote backends with strict access controls.

Should I use Terraform Cloud or self-managed backends?

Terraform Cloud provides built-in security features: encrypted state, access controls, audit logs, policy enforcement, and drift detection. Self-managed backends (S3, Azure Storage) work well but require you to implement these controls yourself.

How do I handle secrets in Terraform?

Never hardcode secrets. Use the sensitive = true flag for variables, integrate with secrets managers (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault), and use environment variables in CI/CD. Mark sensitive outputs as sensitive too.

What IaC scanning tool should I use?

Start with Checkov or tfsec—both are free, well-maintained, and catch common issues. For enterprise environments, consider commercial tools like Snyk IaC or Prisma Cloud that provide remediation guidance and policy management.

How do I prevent configuration drift?

Run terraform plan -refresh-only regularly to detect drift. Use Terraform Cloud's drift detection feature for automated monitoring. Most importantly, enforce a culture where all changes go through Terraform—not manual console changes.


Take Action

  1. Audit your current setup - Check for hardcoded secrets, local state files, and missing encryption
  2. Implement remote state - Move to encrypted backends with state locking
  3. Add security scanning - Integrate Checkov or tfsec into your CI/CD pipeline
  4. Create secure modules - Build reusable modules with security built in
  5. Enable drift detection - Monitor for manual changes outside Terraform

For more cloud security guidance, see our comprehensive guide: 30 Cloud Security Tips for 2026.

Let's turn this knowledge into action

Get a free 30-minute consultation with our experts. We'll help you apply these insights to your specific situation.