The AWS CIS Foundation Benchmark provides 100+ security controls that form the foundation of cloud security best practices. While AWS provides some built-in security services, implementing these controls consistently across multiple accounts and environments requires Infrastructure as Code (IaC) approaches.

This guide shows you how to implement the most critical CIS controls using Terraform, with production-ready modules that can be deployed across your entire AWS organization.

Understanding CIS Controls Categories

The AWS CIS Benchmark is organized into 5 main categories, each addressing different aspects of cloud security:

Identity and Access Management

Controls 1.1-1.22

IAM policies, MFA enforcement, access key rotation, and privileged access management

Storage

Controls 2.1-2.8

S3 bucket security, encryption, versioning, and public access controls

Logging and Monitoring

Controls 3.1-3.14

CloudTrail, CloudWatch, Config, and security event monitoring

Networking

Controls 4.1-4.8

VPC configuration, security groups, NACLs, and network segmentation

Compute

Controls 5.1-5.4

EC2 security, AMI management, and instance hardening

Setting Up Your Terraform Environment

Before implementing CIS controls, you need a solid Terraform foundation. Here's how to structure your project for enterprise-scale compliance:

# terraform/
├── modules/
│   ├── cis-iam/           # IAM-related controls
│   ├── cis-storage/       # S3 and storage controls
│   ├── cis-monitoring/    # Logging and monitoring
│   ├── cis-networking/    # VPC and network security
│   └── cis-compute/       # EC2 and compute security
├── environments/
│   ├── dev/
│   ├── staging/
│   └── prod/
├── shared/
│   ├── backend.tf
│   ├── providers.tf
│   └── variables.tf
└── main.tf

Backend Configuration

# shared/backend.tf
terraform {
  backend "s3" {
    bucket         = "your-terraform-state-bucket"
    key            = "cis-controls/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    
    # Enable versioning for state file
    versioning = true
    
    # Enable server-side encryption
    server_side_encryption_configuration {
      rule {
        apply_server_side_encryption_by_default {
          sse_algorithm = "AES256"
        }
      }
    }
  }
  
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Critical CIS Controls Implementation

Control 1.1: Maintain current contact details

This control ensures AWS has current contact information for security notifications. While this is typically done through the AWS Console, you can automate the process using AWS CLI and Terraform.

# modules/cis-iam/contact-details.tf
resource "aws_account_alternate_contact" "security" {
  alternate_contact_type = "SECURITY"
  email_address         = var.security_contact_email
  name                  = var.security_contact_name
  phone_number          = var.security_contact_phone
  title                 = var.security_contact_title
}

resource "aws_account_alternate_contact" "billing" {
  alternate_contact_type = "BILLING"
  email_address         = var.billing_contact_email
  name                  = var.billing_contact_name
  phone_number          = var.billing_contact_phone
  title                 = var.billing_contact_title
}

# Variables
variable "security_contact_email" {
  description = "Email address for security contact"
  type        = string
  validation {
    condition     = can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", var.security_contact_email))
    error_message = "Security contact email must be a valid email address."
  }
}

Control 1.3: Ensure security questions are registered

This control requires security questions to be set up for account recovery. This is typically a manual process, but we can create a Lambda function to remind administrators to complete this setup.

# modules/cis-iam/security-questions.tf
resource "aws_lambda_function" "security_questions_check" {
  filename         = "security_questions_check.zip"
  function_name    = "cis-security-questions-check"
  role            = aws_iam_role.lambda_role.arn
  handler         = "index.handler"
  runtime         = "python3.9"
  timeout         = 30

  environment {
    variables = {
      SNS_TOPIC_ARN = aws_sns_topic.security_alerts.arn
    }
  }
}

resource "aws_cloudwatch_event_rule" "security_questions_schedule" {
  name                = "cis-security-questions-check"
  description         = "Check if security questions are configured"
  schedule_expression = "rate(7 days)"
}

resource "aws_cloudwatch_event_target" "lambda" {
  rule      = aws_cloudwatch_event_rule.security_questions_schedule.name
  target_id = "SecurityQuestionsCheckTarget"
  arn       = aws_lambda_function.security_questions_check.arn
}

Control 1.4: Ensure no root user access keys exist

Root access keys are a major security risk. This control ensures they don't exist and creates monitoring to detect if they're created.

# modules/cis-iam/root-access-keys.tf
resource "aws_iam_access_key" "root_key_monitor" {
  count = 0  # Never create root access keys
}

# CloudWatch alarm for root access key creation
resource "aws_cloudwatch_metric_alarm" "root_access_key_created" {
  alarm_name          = "cis-root-access-key-created"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "AccessKeyCreated"
  namespace           = "AWS/IAM"
  period              = "300"
  statistic           = "Sum"
  threshold           = "0"
  alarm_description   = "This metric monitors root access key creation"
  alarm_actions       = [aws_sns_topic.security_alerts.arn]

  dimensions = {
    UserName = "root"
  }
}

# Lambda function to check for root access keys
resource "aws_lambda_function" "root_key_checker" {
  filename         = "root_key_checker.zip"
  function_name    = "cis-root-key-checker"
  role            = aws_iam_role.lambda_role.arn
  handler         = "index.handler"
  runtime         = "python3.9"

  environment {
    variables = {
      SNS_TOPIC_ARN = aws_sns_topic.security_alerts.arn
    }
  }
}

# Lambda function code
data "archive_file" "root_key_checker" {
  type        = "zip"
  output_path = "root_key_checker.zip"
  source {
    content = <<EOF
import boto3
import json
import os

def handler(event, context):
    iam = boto3.client('iam')
    sns = boto3.client('sns')
    
    try:
        # Check for root access keys
        response = iam.list_access_keys(UserName='root')
        
        if response['AccessKeyMetadata']:
            message = {
                "alert": "Root access keys detected",
                "keys": response['AccessKeyMetadata'],
                "recommendation": "Delete all root access keys immediately"
            }
            
            sns.publish(
                TopicArn=os.environ['SNS_TOPIC_ARN'],
                Message=json.dumps(message),
                Subject="CIS Control 1.4 Violation: Root Access Keys Found"
            )
            
            return {
                'statusCode': 200,
                'body': json.dumps('Root access keys detected and alert sent')
            }
        else:
            return {
                'statusCode': 200,
                'body': json.dumps('No root access keys found')
            }
            
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error: {str(e)}')
        }
EOF
    filename = "index.py"
  }
}

Control 1.8: Ensure IAM password policy requires uppercase letters

Strong password policies are essential for account security. This control ensures password complexity requirements are enforced.

# modules/cis-iam/password-policy.tf
resource "aws_iam_account_password_policy" "cis_password_policy" {
  minimum_password_length        = 14
  require_lowercase_characters   = true
  require_uppercase_characters   = true
  require_numbers               = true
  require_symbols               = true
  allow_users_to_change_password = true
  max_password_age              = 90
  password_reuse_prevention     = 24
  hard_expiry                   = false
}

# CloudWatch alarm for password policy violations
resource "aws_cloudwatch_metric_alarm" "password_policy_violation" {
  alarm_name          = "cis-password-policy-violation"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "PasswordPolicyViolation"
  namespace           = "AWS/IAM"
  period              = "300"
  statistic           = "Sum"
  threshold           = "0"
  alarm_description   = "This metric monitors password policy violations"
  alarm_actions       = [aws_sns_topic.security_alerts.arn]
}

Storage Security Controls

Control 2.1: Ensure S3 bucket versioning is enabled

Versioning helps protect against accidental deletion and provides an audit trail of object changes.

# modules/cis-storage/s3-versioning.tf
resource "aws_s3_bucket_versioning" "cis_versioning" {
  for_each = var.s3_buckets
  
  bucket = each.value.bucket_name
  versioning_configuration {
    status = "Enabled"
  }
}

# MFA delete protection
resource "aws_s3_bucket_versioning" "mfa_delete" {
  for_each = var.s3_buckets
  
  bucket = each.value.bucket_name
  versioning_configuration {
    status     = "Enabled"
    mfa_delete = "Enabled"
  }
  
  mfa = var.mfa_device_serial_number
}

# Lifecycle policy for old versions
resource "aws_s3_bucket_lifecycle_configuration" "version_lifecycle" {
  for_each = var.s3_buckets
  
  bucket = each.value.bucket_name

  rule {
    id     = "version_lifecycle"
    status = "Enabled"

    noncurrent_version_expiration {
      noncurrent_days = 90
    }

    noncurrent_version_transition {
      noncurrent_days = 30
      storage_class   = "STANDARD_IA"
    }
  }
}

Control 2.2: Ensure S3 bucket server-side encryption is enabled

Server-side encryption protects data at rest using AWS-managed or customer-managed keys.

# modules/cis-storage/s3-encryption.tf
# KMS key for S3 encryption
resource "aws_kms_key" "s3_encryption_key" {
  description             = "KMS key for S3 bucket encryption"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  tags = {
    Name        = "s3-encryption-key"
    Environment = var.environment
    Purpose     = "S3 encryption"
  }
}

resource "aws_kms_alias" "s3_encryption_key" {
  name          = "alias/s3-encryption-key"
  target_key_id = aws_kms_key.s3_encryption_key.key_id
}

# S3 bucket encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "cis_encryption" {
  for_each = var.s3_buckets
  
  bucket = each.value.bucket_name

  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.s3_encryption_key.arn
      sse_algorithm     = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# Block public access
resource "aws_s3_bucket_public_access_block" "cis_public_access_block" {
  for_each = var.s3_buckets
  
  bucket = each.value.bucket_name

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

Monitoring and Logging Controls

Control 3.1: Ensure CloudTrail is enabled in all regions

CloudTrail provides comprehensive logging of API calls and user activity across all regions.

# modules/cis-monitoring/cloudtrail.tf
# CloudTrail in primary region
resource "aws_cloudtrail" "cis_cloudtrail" {
  name                          = "cis-cloudtrail"
  s3_bucket_name               = aws_s3_bucket.cloudtrail_bucket.id
  include_global_service_events = true
  is_multi_region_trail        = true
  enable_logging               = true

  event_selector {
    read_write_type                 = "All"
    include_management_events      = true
    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3:::"]
    }
  }

  event_selector {
    read_write_type                 = "All"
    include_management_events      = true
    data_resource {
      type   = "AWS::Lambda::Function"
      values = ["arn:aws:lambda"]
    }
  }

  tags = {
    Name        = "cis-cloudtrail"
    Environment = var.environment
  }
}

# S3 bucket for CloudTrail logs
resource "aws_s3_bucket" "cloudtrail_bucket" {
  bucket        = "${var.account_id}-cis-cloudtrail-logs"
  force_destroy = false

  tags = {
    Name        = "cis-cloudtrail-logs"
    Environment = var.environment
  }
}

# CloudTrail log file validation
resource "aws_cloudtrail" "cis_cloudtrail_validation" {
  name                          = "cis-cloudtrail-validation"
  s3_bucket_name               = aws_s3_bucket.cloudtrail_bucket.id
  include_global_service_events = true
  is_multi_region_trail        = true
  enable_logging               = true
  enable_log_file_validation   = true

  tags = {
    Name        = "cis-cloudtrail-validation"
    Environment = var.environment
  }
}

# CloudWatch log group for CloudTrail
resource "aws_cloudwatch_log_group" "cloudtrail_logs" {
  name              = "/aws/cloudtrail/cis"
  retention_in_days = 90

  tags = {
    Name        = "cis-cloudtrail-logs"
    Environment = var.environment
  }
}

Production Deployment Strategy

Environment-Specific Configuration

Different environments require different security postures. Here's how to structure your Terraform configuration for multiple environments:

# environments/prod/main.tf
module "cis_controls" {
  source = "../../modules/cis-iam"
  
  environment = "production"
  account_id  = var.account_id
  
  # Production-specific settings
  security_contact_email = "security@company.com"
  billing_contact_email  = "billing@company.com"
  
  # Stricter policies for production
  password_policy = {
    minimum_password_length = 16
    max_password_age        = 60
    password_reuse_prevention = 12
  }
  
  # Enable all monitoring
  enable_detailed_monitoring = true
  enable_cloudtrail_insights = true
  
  # Production S3 buckets
  s3_buckets = {
    "prod-app-data" = {
      bucket_name = "prod-company-app-data"
      versioning  = true
      encryption  = true
    }
    "prod-logs" = {
      bucket_name = "prod-company-logs"
      versioning  = true
      encryption  = true
    }
  }
  
  tags = {
    Environment = "production"
    Owner       = "security-team"
    Compliance  = "cis-benchmark"
  }
}

# Production-specific overrides
module "cis_storage" {
  source = "../../modules/cis-storage"
  
  environment = "production"
  
  # More aggressive lifecycle policies
  lifecycle_policies = {
    standard_ia_transition_days = 30
    glacier_transition_days     = 90
    expiration_days            = 2555  # 7 years
  }
  
  # Enable MFA delete for critical buckets
  enable_mfa_delete = true
  mfa_device_serial_number = var.mfa_device_serial_number
}

Automated Compliance Testing

Implement automated testing to ensure your CIS controls are working correctly:

# tests/cis_controls_test.py
import boto3
import pytest
import json

class TestCISControls:
    def setup_method(self):
        self.iam = boto3.client('iam')
        self.s3 = boto3.client('s3')
        self.cloudtrail = boto3.client('cloudtrail')
    
    def test_control_1_4_no_root_access_keys(self):
        """Test that no root access keys exist"""
        try:
            response = self.iam.list_access_keys(UserName='root')
            assert len(response['AccessKeyMetadata']) == 0, "Root access keys should not exist"
        except self.iam.exceptions.NoSuchEntityException:
            pass  # This is expected
    
    def test_control_1_8_password_policy(self):
        """Test that password policy meets CIS requirements"""
        policy = self.iam.get_account_password_policy()
        
        assert policy['PasswordPolicy']['MinimumPasswordLength'] >= 14
        assert policy['PasswordPolicy']['RequireUppercaseCharacters'] == True
        assert policy['PasswordPolicy']['RequireLowercaseCharacters'] == True
        assert policy['PasswordPolicy']['RequireNumbers'] == True
        assert policy['PasswordPolicy']['RequireSymbols'] == True
        assert policy['PasswordPolicy']['MaxPasswordAge'] <= 90
    
    def test_control_2_1_s3_versioning(self):
        """Test that S3 buckets have versioning enabled"""
        buckets = self.s3.list_buckets()
        
        for bucket in buckets['Buckets']:
            versioning = self.s3.get_bucket_versioning(Bucket=bucket['Name'])
            assert versioning.get('Status') == 'Enabled', f"Bucket {bucket['Name']} should have versioning enabled"
    
    def test_control_3_1_cloudtrail_enabled(self):
        """Test that CloudTrail is enabled"""
        trails = self.cloudtrail.describe_trails()
        
        assert len(trails['trailList']) > 0, "At least one CloudTrail should be enabled"
        
        for trail in trails['trailList']:
            assert trail['IsMultiRegionTrail'] == True, f"Trail {trail['Name']} should be multi-region"
            assert trail['IncludeGlobalServiceEvents'] == True, f"Trail {trail['Name']} should include global service events"

# Run tests
if __name__ == "__main__":
    pytest.main([__file__, "-v"])

Continuous Compliance Monitoring

Automated Remediation

Implement automated remediation for common CIS control violations:

# modules/cis-monitoring/automated-remediation.tf
# Lambda function for automated remediation
resource "aws_lambda_function" "cis_remediation" {
  filename         = "cis_remediation.zip"
  function_name    = "cis-automated-remediation"
  role            = aws_iam_role.remediation_role.arn
  handler         = "index.handler"
  runtime         = "python3.9"
  timeout         = 300

  environment {
    variables = {
      SNS_TOPIC_ARN = aws_sns_topic.security_alerts.arn
      SLACK_WEBHOOK = var.slack_webhook_url
    }
  }
}

# EventBridge rule for CIS violations
resource "aws_cloudwatch_event_rule" "cis_violations" {
  name        = "cis-control-violations"
  description = "Capture CIS control violations"

  event_pattern = jsonencode({
    source      = ["aws.iam", "aws.s3", "aws.cloudtrail"]
    detail-type = ["AWS API Call via CloudTrail"]
    detail = {
      eventName = [
        "CreateAccessKey",
        "DeleteBucketVersioning",
        "PutBucketEncryption"
      ]
    }
  })
}

resource "aws_cloudwatch_event_target" "lambda" {
  rule      = aws_cloudwatch_event_rule.cis_violations.name
  target_id = "CISRemediationTarget"
  arn       = aws_lambda_function.cis_remediation.arn
}

# IAM role for remediation Lambda
resource "aws_iam_role" "remediation_role" {
  name = "cis-remediation-role"

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

resource "aws_iam_role_policy" "remediation_policy" {
  name = "cis-remediation-policy"
  role = aws_iam_role.remediation_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "iam:DeleteAccessKey",
          "iam:PutBucketVersioning",
          "iam:PutBucketEncryption",
          "s3:PutBucketVersioning",
          "s3:PutEncryptionConfiguration"
        ]
        Resource = "*"
      }
    ]
  })
}

Best Practices and Recommendations

Production Deployment Checklist

  • ✅ Test all modules in a development environment first
  • ✅ Use Terraform workspaces for environment isolation
  • ✅ Implement proper state file locking and encryption
  • ✅ Set up automated testing and validation
  • ✅ Configure monitoring and alerting for all controls
  • ✅ Document all customizations and exceptions
  • ✅ Implement change management processes
  • ✅ Regular compliance audits and reporting

Common Pitfalls to Avoid

❌ Hardcoded Values

Always use variables and data sources instead of hardcoded values. This makes your configuration reusable and maintainable.

❌ Missing Dependencies

Ensure proper resource dependencies are defined to avoid deployment failures and ensure correct order of operations.

❌ Inadequate Testing

Implement comprehensive testing for all controls. Use tools like Terratest for automated testing of your Terraform modules.

❌ Ignoring Regional Requirements

Some controls need to be implemented in all regions. Use Terraform's for_each or count to ensure global coverage.

Conclusion

Implementing AWS CIS controls with Terraform provides a scalable, maintainable approach to cloud security compliance. By following this guide and using the provided modules, you can achieve comprehensive security coverage across your AWS environment.

Remember that compliance is an ongoing process, not a one-time implementation. Regular monitoring, testing, and updates are essential to maintain your security posture and meet evolving compliance requirements.

Ready to Implement CIS Controls?

Get started with our comprehensive Terraform modules and accelerate your compliance journey.