Skip to main content

Terraform for Security

Introduction

Infrastructure as Code (IaC) has revolutionized how we build and manage cloud resources. Terraform, as a leading IaC tool, offers powerful capabilities for securing your infrastructure from the ground up. This approach, sometimes called "Security as Code," enables teams to automate, standardize, and version control security configurations across cloud environments.

In this guide, we'll explore how Terraform can be used specifically for security purposes, helping you build robust security practices into your infrastructure from the beginning. Whether you're working with AWS, Azure, GCP, or other providers, these principles will help you create more secure environments.

Why Use Terraform for Security?

Terraform provides several advantages for implementing security:

  • Consistency: Security configurations are applied uniformly across environments
  • Version Control: Track changes to security settings over time
  • Automation: Reduce human error in security implementation
  • Compliance: Easily implement and verify compliance requirements
  • Testing: Test security configurations before deploying to production

Security Foundations with Terraform

Secure Authentication Configuration

Before writing any security-specific code, ensure your Terraform setup itself is secure:

hcl
# DON'T do this - hardcoded credentials
provider "aws" {
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
region = "us-west-2"
}

# DO this instead - use environment variables or credential files
provider "aws" {
region = "us-west-2"
# AWS credentials come from environment variables or shared credentials file
}

State File Security

Terraform state files often contain sensitive information. Here's how to configure remote state with encryption:

hcl
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "security/terraform.tfstate"
region = "us-east-1"

# Security best practices
encrypt = true
dynamodb_table = "terraform-locks"
}
}

Network Security with Terraform

Creating Secure VPCs

hcl
resource "aws_vpc" "secure_vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true

tags = {
Name = "secure-vpc"
}
}

# Create public and private subnets
resource "aws_subnet" "private" {
vpc_id = aws_vpc.secure_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"

tags = {
Name = "private-subnet"
}
}

resource "aws_subnet" "public" {
vpc_id = aws_vpc.secure_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b"

tags = {
Name = "public-subnet"
}
}

Securing Network Traffic with Security Groups

hcl
resource "aws_security_group" "web_sg" {
name = "web-server-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.secure_vpc.id

# Allow HTTPS traffic from anywhere
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

# Allow HTTP traffic from anywhere (consider 443 only for production)
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

# Restrict SSH access to specific IP range
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"] # Corporate network only
}

# Allow all outbound traffic
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

Identity and Access Management

Creating IAM Policies with Least Privilege

The principle of least privilege is crucial for security. Here's how to implement restrictive IAM policies:

hcl
resource "aws_iam_policy" "s3_read_only" {
name = "s3-read-only"
description = "Policy that grants read-only access to specific S3 bucket"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"s3:GetObject",
"s3:ListBucket",
]
Effect = "Allow"
Resource = [
"arn:aws:s3:::example-bucket",
"arn:aws:s3:::example-bucket/*"
]
}
]
})
}

resource "aws_iam_role" "app_role" {
name = "application-role"

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

resource "aws_iam_role_policy_attachment" "app_s3_access" {
role = aws_iam_role.app_role.name
policy_arn = aws_iam_policy.s3_read_only.arn
}

Managing Service Accounts

hcl
resource "aws_iam_user" "service_account" {
name = "ci-service-account"
path = "/system/"
}

resource "aws_iam_access_key" "service_account_key" {
user = aws_iam_user.service_account.name
}

resource "aws_iam_user_policy_attachment" "service_account_permissions" {
user = aws_iam_user.service_account.name
policy_arn = aws_iam_policy.s3_read_only.arn
}

# Output the access key ID (but never the secret in logs/output)
output "service_account_access_key_id" {
value = aws_iam_access_key.service_account_key.id
}

# Store the secret key securely (e.g., in AWS Secrets Manager)
resource "aws_secretsmanager_secret" "service_account_secret" {
name = "service-account-secret-key"
}

resource "aws_secretsmanager_secret_version" "service_account_secret_value" {
secret_id = aws_secretsmanager_secret.service_account_secret.id
secret_string = aws_iam_access_key.service_account_key.secret
}

Encryption and Secrets Management

Enforcing Encryption at Rest

hcl
resource "aws_s3_bucket" "data_bucket" {
bucket = "my-secure-data-bucket"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "bucket_encryption" {
bucket = aws_s3_bucket.data_bucket.id

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

# Block public access
resource "aws_s3_bucket_public_access_block" "block_public_access" {
bucket = aws_s3_bucket.data_bucket.id

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

Managing Secrets with Terraform

Rather than hardcoding secrets, use a secrets manager:

hcl
resource "aws_secretsmanager_secret" "database_password" {
name = "db-password"
}

resource "aws_secretsmanager_secret_version" "database_password_value" {
secret_id = aws_secretsmanager_secret.database_password.id
secret_string = jsonencode({
username = "admin"
password = "REFERENCE_TO_EXTERNALLY_MANAGED_SECRET"
})
}

# Grant application access to the secret
resource "aws_iam_policy" "secret_access" {
name = "secret-access-policy"
description = "Policy that grants access to specific secret"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"secretsmanager:GetSecretValue",
]
Effect = "Allow"
Resource = aws_secretsmanager_secret.database_password.arn
}
]
})
}

Implementing Security Controls

Creating Security Groups as Code

Let's create a more complex security group setup to illustrate tiered security:

hcl
# Database security group - only accessible from application tier
resource "aws_security_group" "database_sg" {
name = "database-sg"
description = "Security group for database instances"
vpc_id = aws_vpc.secure_vpc.id

# Allow MySQL/PostgreSQL access only from app servers
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.app_sg.id]
}

# Block all outbound by default
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

# Application tier security group
resource "aws_security_group" "app_sg" {
name = "application-sg"
description = "Security group for application servers"
vpc_id = aws_vpc.secure_vpc.id

# Allow traffic from web tier
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web_sg.id]
}

# Allow outbound to database only
egress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.database_sg.id]
}

# Allow outbound HTTP/HTTPS for updates
egress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}

Security Monitoring and Logging

Setting Up CloudTrail with Terraform

hcl
resource "aws_cloudtrail" "security_trail" {
name = "security-audit-trail"
s3_bucket_name = aws_s3_bucket.trail_logs.id
include_global_service_events = true
is_multi_region_trail = true
enable_log_file_validation = true

event_selector {
read_write_type = "All"
include_management_events = true

data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::"]
}
}
}

resource "aws_s3_bucket" "trail_logs" {
bucket = "security-trail-logs"
}

resource "aws_s3_bucket_policy" "trail_logs_policy" {
bucket = aws_s3_bucket.trail_logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AWSCloudTrailAclCheck"
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = "s3:GetBucketAcl"
Resource = aws_s3_bucket.trail_logs.arn
},
{
Sid = "AWSCloudTrailWrite"
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.trail_logs.arn}/AWSLogs/*"
Condition = {
StringEquals = {
"s3:x-amz-acl" = "bucket-owner-full-control"
}
}
}
]
})
}

Implementing Security Policies with Terraform

Network Access Control Lists (NACLs)

hcl
resource "aws_network_acl" "secure_nacl" {
vpc_id = aws_vpc.secure_vpc.id
subnet_ids = [aws_subnet.private.id]

# Allow HTTP/HTTPS inbound
ingress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}

ingress {
protocol = "tcp"
rule_no = 110
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}

# Allow SSH from specific IPs only
ingress {
protocol = "tcp"
rule_no = 120
action = "allow"
cidr_block = "10.0.0.0/8"
from_port = 22
to_port = 22
}

# Allow all outbound traffic
egress {
protocol = "-1"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
}

Implementing Compliance as Code

AWS Config Rules with Terraform

hcl
resource "aws_config_config_rule" "s3_bucket_public_write_prohibited" {
name = "s3-bucket-public-write-prohibited"

source {
owner = "AWS"
source_identifier = "S3_BUCKET_PUBLIC_WRITE_PROHIBITED"
}
}

resource "aws_config_config_rule" "encrypted_volumes" {
name = "encrypted-volumes"

source {
owner = "AWS"
source_identifier = "ENCRYPTED_VOLUMES"
}
}

resource "aws_config_config_rule" "root_account_mfa_enabled" {
name = "root-account-mfa-enabled"

source {
owner = "AWS"
source_identifier = "ROOT_ACCOUNT_MFA_ENABLED"
}
}


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)