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:
# 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:
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
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
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:
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
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
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:
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:
# 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
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)
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
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! :)