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.