Sunday, November 23, 2025

Terraform Security Best Practices: Protecting Your Infrastructure as Code

Terraform Security Best Practices: Protecting Your Infrastructure as Code
Terraform Security Best Practices Secrets Management IAM Compliance

Terraform Security Best Practices: Protecting Your Infrastructure as Code

Published on: November 3, 2023 | Author: DevOps Security Team

Secure Infrastructure as Code

Welcome to Part 9 of our Terraform Mastery Series! Security is paramount in infrastructure management. Learn comprehensive security best practices for Terraform covering secrets management, state file protection, IAM policies, and secure deployment workflows to protect your cloud infrastructure.

Terraform Security Fundamentals

Terraform security involves protecting your infrastructure code, state files, secrets, and deployment processes. A comprehensive security approach covers the entire Terraform lifecycle.

Terraform Security Defense in Depth

Code Security
Secure configurations & policies
Secrets Protection
Encrypted secrets management
State Security
Encrypted & access-controlled state
Access Control
Least privilege IAM policies

Security Principles

  • Least Privilege: Minimum permissions required
  • Defense in Depth: Multiple security layers
  • Secure by Default: Safe default configurations
  • Automation: Consistent, repeatable security
  • Auditability: Track all changes and access

Common Security Risks

  • Hardcoded secrets in version control
  • Unencrypted state files
  • Overly permissive IAM roles
  • Unreviewed third-party modules
  • Insecure network configurations

Security First Mindset

Security should be integrated into every stage of your Terraform workflow, from code development to deployment and monitoring. Never treat security as an afterthought.

Secrets Management Strategies

Proper secrets management is critical for Terraform security. Never store secrets in plaintext or commit them to version control.

Environment Variables for Secrets

# NEVER do this - secrets in plaintext
variable "db_password" {
  type    = string
  default = "SuperSecret123!"  # ❌ DANGER!
}

# Instead, use environment variables
variable "db_password" {
  type      = string
  sensitive = true
}

# Set via environment variable
$ export TF_VAR_db_password="SuperSecret123!"
$ terraform apply

# Or use a .env file (add to .gitignore)
# .env
TF_VAR_db_password=SuperSecret123!
TF_VAR_api_key=abc123def456

# Load environment variables
$ source .env
$ terraform apply

Safe Approach: Use environment variables or dedicated secrets managers.

AWS Secrets Manager Integration

# Retrieve secrets from AWS Secrets Manager
data "aws_secretsmanager_secret" "db_credentials" {
  name = "production/database"
}

data "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = data.aws_secretsmanager_secret.db_credentials.id
}

# Parse the secret (assuming JSON format)
locals {
  db_secrets = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)
}

# Use the secrets in resources
resource "aws_db_instance" "postgres" {
  identifier     = "production-db"
  username       = local.db_secrets.username
  password       = local.db_secrets.password
  instance_class = "db.t3.micro"
  
  tags = {
    ManagedBy = "terraform"
  }
}

# Output sensitive values carefully
output "database_endpoint" {
  value       = aws_db_instance.postgres.endpoint
  description = "Database connection endpoint"
  sensitive   = true  # Prevents accidental exposure
}

Enterprise Solution: Use cloud-native secrets managers for enhanced security.

HashiCorp Vault Integration

# Configure Vault provider
provider "vault" {
  address = var.vault_address
  token   = var.vault_token
}

# Read secrets from Vault
data "vault_generic_secret" "api_keys" {
  path = "secret/api"
}

data "vault_aws_access_credentials" "aws_creds" {
  backend = "aws"
  role    = "deployment"
}

# Use dynamic AWS credentials from Vault
provider "aws" {
  region = "us-east-1"
  
  access_key = data.vault_aws_access_credentials.aws_creds.access_key
  secret_key = data.vault_aws_access_credentials.aws_creds.secret_key
  token      = data.vault_aws_access_credentials.aws_creds.security_token
}

# Use API keys from Vault
resource "aws_lambda_function" "api" {
  function_name = "api-handler"
  role          = aws_iam_role.lambda.arn
  
  environment {
    variables = {
      API_KEY = data.vault_generic_secret.api_keys.data["key"]
    }
  }
}

# Auto-renew credentials with short TTL
resource "vault_aws_secret_backend_role" "deployment" {
  backend = vault_aws_secret_backend.aws.path
  name    = "deployment"
  credential_type = "assumed_role"
  
  role_arns = [aws_iam_role.vault_assume.arn]
  default_sts_ttl = 3600  # 1 hour TTL
}

Advanced Security: Use Vault for dynamic secrets with automatic rotation.

Secrets Management Comparison

Method Security Level Complexity Best For Key Features
Environment Variables Medium Low Development, Small teams Simple, No additional tools
AWS Secrets Manager High Medium AWS environments Native integration, Automatic rotation
HashiCorp Vault Very High High Enterprise, Multi-cloud Dynamic secrets, Fine-grained access
Azure Key Vault High Medium Azure environments Azure integration, Hardware security
Google Secret Manager High Medium GCP environments GCP integration, Versioning

Critical: Never Commit Secrets

Always add files containing secrets to .gitignore. Common patterns to ignore: *.tfvars, *.env, terraform.tfstate*, .terraform/. Use git-secrets or similar tools to prevent accidental commits.

State File Security

Terraform state files contain sensitive information and must be protected with encryption, access controls, and proper backup strategies.

Secure Remote State Configuration

# Secure S3 backend configuration
terraform {
  backend "s3" {
    bucket = "my-company-terraform-state"
    key    = "production/network/terraform.tfstate"
    region = "us-east-1"
    
    # Security configurations
    encrypt        = true              # Enable encryption
    kms_key_id     = "alias/terraform-state-key"
    dynamodb_table = "terraform-state-locks"
    
    # Access controls
    role_arn = "arn:aws:iam::123456789012:role/TerraformStateAccess"
  }
}

# S3 bucket policy for state bucket
resource "aws_s3_bucket_policy" "state_bucket" {
  bucket = aws_s3_bucket.terraform_state.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "RequireEncryption"
        Effect = "Deny"
        Principal = "*"
        Action = "s3:PutObject"
        Resource = "${aws_s3_bucket.terraform_state.arn}/*"
        Condition = {
          StringNotEquals = {
            "s3:x-amz-server-side-encryption" = "aws:kms"
          }
        }
      },
      {
        Sid    = "EnforceTLS"
        Effect = "Deny"
        Principal = "*"
        Action = "s3:*"
        Resource = [
          aws_s3_bucket.terraform_state.arn,
          "${aws_s3_bucket.terraform_state.arn}/*"
        ]
        Condition = {
          Bool = {
            "aws:SecureTransport" = "false"
          }
        }
      }
    ]
  })
}

Granular State Access Controls

# IAM policy for Terraform state access
resource "aws_iam_policy" "terraform_state_access" {
  name        = "TerraformStateAccess"
  description = "Permissions for Terraform state management"
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = "S3StateBucketAccess"
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:DeleteObject"
        ]
        Resource = "arn:aws:s3:::my-company-terraform-state/env:/${var.environment}/*"
      },
      {
        Sid = "S3StateBucketList"
        Effect = "Allow"
        Action = "s3:ListBucket"
        Resource = "arn:aws:s3:::my-company-terraform-state"
        Condition = {
          StringLike = {
            "s3:prefix" = "env:/${var.environment}/*"
          }
        }
      },
      {
        Sid = "DynamoDBLockTableAccess"
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:DeleteItem"
        ]
        Resource = "arn:aws:dynamodb:us-east-1:123456789012:table/terraform-state-locks"
      },
      {
        Sid = "KMSDecryptState"
        Effect = "Allow"
        Action = [
          "kms:Decrypt",
          "kms:GenerateDataKey"
        ]
        Resource = "arn:aws:kms:us-east-1:123456789012:key/abcd1234-1234-1234-1234-123456789012"
      }
    ]
  })
}

# Environment-specific state isolation
locals {
  state_key = "${var.environment}/${var.component}/terraform.tfstate"
}

# Separate state per environment and component
terraform {
  backend "s3" {
    bucket = "my-company-terraform-state"
    key    = local.state_key
    region = "us-east-1"
    encrypt = true
  }
}

IAM and Access Control

Implement least privilege principles with carefully crafted IAM policies and roles for Terraform deployments.

IAM Policy Security Analyzer

Select a policy pattern to see security recommendations:

Select a policy pattern to see security recommendations...

Least Privilege IAM Roles

# Terraform execution role with minimal permissions
resource "aws_iam_role" "terraform_execution" {
  name = "TerraformExecutionRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::123456789012:root"
        }
        Action = "sts:AssumeRole"
        Condition = {
          Bool = {
            "aws:MultiFactorAuthPresent" = "true"
          }
        }
      }
    ]
  })
}

# Scoped permissions for specific resource types
resource "aws_iam_role_policy" "ec2_management" {
  name = "EC2Management"
  role = aws_iam_role.terraform_execution.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = "EC2InstanceManagement"
        Effect = "Allow"
        Action = [
          "ec2:RunInstances",
          "ec2:TerminateInstances",
          "ec2:StartInstances",
          "ec2:StopInstances",
          "ec2:DescribeInstances"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:RequestedRegion" = "us-east-1"
          }
          StringLike = {
            "aws:ResourceTag/ManagedBy" = "terraform"
          }
        }
      },
      {
        Sid = "EC2Tagging"
        Effect = "Allow"
        Action = [
          "ec2:CreateTags",
          "ec2:DeleteTags"
        ]
        Resource = "arn:aws:ec2:us-east-1:123456789012:instance/*"
        Condition = {
          StringEquals = {
            "ec2:CreateAction" = "RunInstances"
          }
        }
      }
    ]
  })
}

# Deny dangerous actions
resource "aws_iam_role_policy" "security_denies" {
  name = "SecurityDenies"
  role = aws_iam_role.terraform_execution.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = "DenyResourceDeletionWithoutMFA"
        Effect = "Deny"
        Action = [
          "ec2:DeleteSecurityGroup",
          "rds:DeleteDBInstance",
          "s3:DeleteBucket"
        ]
        Resource = "*"
        Condition = {
          BoolIfExists = {
            "aws:MultiFactorAuthPresent" = "false"
          }
        }
      }
    ]
  })
}

Service-Specific IAM Roles

# EC2 instance role with minimal permissions
resource "aws_iam_role" "web_server" {
  name = "WebServerRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "web_server_s3" {
  name = "S3ReadOnlyAccess"
  role = aws_iam_role.web_server.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = "S3ReadAccess"
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:GetObjectVersion",
          "s3:ListBucket"
        ]
        Resource = [
          "arn:aws:s3:::my-app-assets",
          "arn:aws:s3:::my-app-assets/*"
        ]
      }
    ]
  })
}

# Lambda execution role
resource "aws_iam_role" "lambda_execution" {
  name = "LambdaExecutionRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

# Use managed policies when appropriate
resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# Custom policy for specific Lambda needs
resource "aws_iam_role_policy" "lambda_dynamodb" {
  name = "DynamoDBAccess"
  role = aws_iam_role.lambda_execution.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = "DynamoDBActions"
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem",
          "dynamodb:DeleteItem"
        ]
        Resource = "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable"
      }
    ]
  })
}

Network Security Configuration

Secure network configurations are essential for protecting your infrastructure. Implement proper security groups, NACLs, and network segmentation.

Network Security Checklist

Default Security Groups

Remove all rules from default security groups and create custom ones

Least Privilege Rules

Only allow necessary ports and protocols with specific CIDR ranges

Network Segmentation

Use separate subnets for public, private, and data layers

Encryption in Transit

Enable TLS/SSL for all data transmission

Key Security Takeaways

  • Never store secrets in version control - use secrets managers
  • Encrypt state files and implement proper access controls
  • Apply least privilege principles to all IAM roles and policies
  • Use network segmentation and secure default configurations
  • Implement security scanning in your CI/CD pipelines
  • Enable audit logging and monitor for suspicious activities
  • Regularly update and patch your Terraform versions and providers

In our next tutorial, we'll explore Terraform Testing Strategies, where you'll learn how to implement comprehensive testing for your infrastructure code to ensure reliability and security.


No comments:

Post a Comment

Terraform Security Best Practices: Protecting Your Infrastructure as Code

Terraform Security Best Practices: Protecting Your Infrastructure as Code ...