LAB36_AWS_Compliance Automation: AWS Config Rules + Lambda Auto-Remediation

Compliance Automation: AWS Config Rules + Lambda Auto-Remediation

☁ AWS 🔥 Advanced AWS Config Config Rules Auto-Remediation SNS Systems Manager

Scenario

Your security team's compliance scan revealed that S3 buckets with public access enabled and EC2 instances without required tags slip through undetected until quarterly audits. You need a continuous compliance system: AWS Config watches all resource changes, flags non-compliant resources immediately, automatically remediates the S3 public access issue, and sends a notification to the security team's SNS topic.

Your Objectives
  • Enable an AWS Config Recorder and Delivery Channel with an S3 bucket for configuration history.
  • Create two managed Config rules: s3-bucket-public-read-prohibited and required-tags (checking for Environment and ManagedBy tags on EC2 instances).
  • Create a Lambda auto-remediation function that blocks S3 public access on non-compliant buckets. Wire it up via aws_config_remediation_configuration with automatic execution.
  • Create an EventBridge rule that triggers on AWS Config ConfigRuleComplianceChangeNotification events and routes to an SNS topic to alert the security team.
  • Output the Config recorder name, delivery channel name, and SNS topic ARN.

Additional Context

AWS Config charges per configuration item recorded and per active Config rule evaluation. Use recording_group with all_supported = true to cover all resource types, or use resource_types to scope recording to specific types and reduce costs.

aws_config_remediation_configuration uses AWS Systems Manager Automation documents for remediation. For simple cases (disabling S3 public access), you can use the AWS-managed SSM document AWS-DisableS3BucketPublicReadWrite if available, or call a Lambda directly with a Lambda remediation action.

variables.tf

variable "aws_region"        { type = string; default = "us-east-1" }
variable "security_email"    { type = string; description = "Email address for compliance alerts." }
variable "environment"       { type = string; default = "prod" }

main.tf

provider "aws" { region = var.aws_region }
locals { tags = { Environment = var.environment, ManagedBy = "terraform" } }

data "aws_caller_identity" "current" {}

# S3 bucket for AWS Config delivery.
resource "aws_s3_bucket" "config" {
  bucket = "aws-config-delivery-${data.aws_caller_identity.current.account_id}"
  tags   = local.tags
}

resource "aws_s3_bucket_policy" "config" {
  bucket = aws_s3_bucket.config.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      { Sid = "AWSConfigBucketPermissionsCheck"; Effect = "Allow"
        Principal = { Service = "config.amazonaws.com" }
        Action = "s3:GetBucketAcl"; Resource = aws_s3_bucket.config.arn },
      { Sid = "AWSConfigBucketDelivery"; Effect = "Allow"
        Principal = { Service = "config.amazonaws.com" }
        Action = "s3:PutObject"
        Resource = "${aws_s3_bucket.config.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/Config/*"
        Condition = { StringEquals = { "s3:x-amz-acl" = "bucket-owner-full-control" } } }
    ]
  })
}

# IAM role for AWS Config service.
resource "aws_iam_role" "config" {
  name = "aws-config-role-${var.environment}"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{ Effect = "Allow"; Principal = { Service = "config.amazonaws.com" }; Action = "sts:AssumeRole" }]
  })
}
resource "aws_iam_role_policy_attachment" "config" {
  role       = aws_iam_role.config.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole"
}

# Config Recorder — records all supported resources.
resource "aws_config_configuration_recorder" "main" {
  name     = "default"
  role_arn = aws_iam_role.config.arn
  recording_group {
    all_supported                 = true
    include_global_resource_types = true
  }
}

# Delivery Channel — sends config data to S3.
resource "aws_config_delivery_channel" "main" {
  name           = "default"
  s3_bucket_name = aws_s3_bucket.config.bucket
  depends_on     = [aws_config_configuration_recorder.main]
}

# Enable recording.
resource "aws_config_configuration_recorder_status" "main" {
  name       = aws_config_configuration_recorder.main.name
  is_enabled = true
  depends_on = [aws_config_delivery_channel.main]
}

# Managed Config Rule 1: S3 public read prohibited.
resource "aws_config_config_rule" "s3_public_read" {
  name = "s3-bucket-public-read-prohibited"
  source {
    owner             = "AWS"
    source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED"
  }
  depends_on = [aws_config_configuration_recorder_status.main]
}

# Managed Config Rule 2: Required tags on EC2 instances.
resource "aws_config_config_rule" "required_tags" {
  name = "ec2-required-tags-${var.environment}"
  source {
    owner             = "AWS"
    source_identifier = "REQUIRED_TAGS"
  }
  input_parameters = jsonencode({
    tag1Key = "Environment"
    tag2Key = "ManagedBy"
  })
  scope {
    compliance_resource_types = ["AWS::EC2::Instance"]
  }
  depends_on = [aws_config_configuration_recorder_status.main]
}

# Lambda remediation function — blocks S3 public access on non-compliant buckets.
data "aws_iam_policy_document" "remediation_assume" {
  statement { effect = "Allow"; actions = ["sts:AssumeRole"]
    principals { type = "Service"; identifiers = ["lambda.amazonaws.com","ssm.amazonaws.com"] } }
}
resource "aws_iam_role" "remediation" {
  name               = "config-remediation-role-${var.environment}"
  assume_role_policy = data.aws_iam_policy_document.remediation_assume.json
}
resource "aws_iam_role_policy" "remediation" {
  name = "remediation-s3"
  role = aws_iam_role.remediation.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      { Effect = "Allow"; Action = ["s3:PutBucketPublicAccessBlock","s3:GetBucketPublicAccessBlock"]; Resource = "*" },
      { Effect = "Allow"; Action = ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"]; Resource = "*" }
    ]
  })
}

resource "aws_lambda_function" "remediate_s3" {
  function_name = "config-remediate-s3-public-${var.environment}"
  runtime       = "python3.12"
  handler       = "handler.lambda_handler"
  role          = aws_iam_role.remediation.arn
  filename      = "${path.module}/remediation.zip"
  description   = "Automatically blocks S3 public access on non-compliant buckets"
  tags          = local.tags
}

# Config Remediation — auto-executes Lambda when S3 public read rule fires.
resource "aws_config_remediation_configuration" "s3_public" {
  config_rule_name = aws_config_config_rule.s3_public_read.name
  resource_type    = "AWS::S3::Bucket"
  target_type      = "SSM_DOCUMENT"
  target_id        = "AWS-InvokeFunction"

  parameter {
    name         = "FunctionName"
    static_value = aws_lambda_function.remediate_s3.function_name
  }
  parameter {
    name           = "Payload"
    resource_value = "RESOURCE_ID"
  }

  automatic                  = true
  maximum_automatic_attempts = 3
  retry_attempt_seconds      = 60
  execution_controls {
    ssm_controls { concurrent_execution_rate_percentage = 25; error_percentage = 20 }
  }
}

# SNS topic for compliance alerts.
resource "aws_sns_topic" "compliance" {
  name = "config-compliance-alerts-${var.environment}"
  tags = local.tags
}
resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.compliance.arn
  protocol  = "email"
  endpoint  = var.security_email
}

# EventBridge rule — routes Config compliance change events to SNS.
resource "aws_cloudwatch_event_rule" "compliance_change" {
  name        = "config-compliance-change-${var.environment}"
  description = "Fires when any Config rule compliance status changes"
  event_pattern = jsonencode({
    source      = ["aws.config"]
    detail-type = ["Config Rules Compliance Change"]
  })
}
resource "aws_sns_topic_policy" "compliance" {
  arn = aws_sns_topic.compliance.arn
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{ Effect = "Allow"; Principal = { Service = "events.amazonaws.com" }
      Action = "sns:Publish"; Resource = aws_sns_topic.compliance.arn }]
  })
}
resource "aws_cloudwatch_event_target" "sns" {
  rule      = aws_cloudwatch_event_rule.compliance_change.name
  target_id = "compliance-sns"
  arn       = aws_sns_topic.compliance.arn
}

outputs.tf

output "config_recorder_name"  { value = aws_config_configuration_recorder.main.name }
output "delivery_channel_name" { value = aws_config_delivery_channel.main.name }
output "sns_topic_arn"         { value = aws_sns_topic.compliance.arn }
output "config_s3_bucket"      { value = aws_s3_bucket.config.bucket }

Workflow Commands

terraform init
terraform apply -var="[email protected]"

# Check compliance status for all rules
aws configservice describe-compliance-by-config-rule \
  --config-rule-names s3-bucket-public-read-prohibited ec2-required-tags-prod

# Trigger a compliance evaluation
aws configservice start-config-rules-evaluation \
  --config-rule-names s3-bucket-public-read-prohibited

# List non-compliant resources
aws configservice get-compliance-details-by-config-rule \
  --config-rule-name s3-bucket-public-read-prohibited \
  --compliance-types NON_COMPLIANT

terraform destroy -var="[email protected]"
✓ Expected Output After Apply
Apply complete! Resources: 16 added, 0 changed, 0 destroyed.

Outputs:

config_recorder_name  = "default"
config_s3_bucket      = "aws-config-delivery-123456789012"
delivery_channel_name = "default"
sns_topic_arn         = "arn:aws:sns:us-east-1:123456789012:config-compliance-alerts-prod"