AWS Config Rules with Auto Remediation - Enforce Compliance Automatically
Someone creates an S3 bucket without encryption. Someone launches an EC2 instance with a public IP. Someone disables versioning on a critical bucket. By the time you notice, it’s been running non-compliant for weeks.
AWS Config changes this from reactive firefighting to proactive enforcement. It continuously evaluates your resources against rules, detects violations, and - with auto remediation - fixes them automatically.
This post covers how to set up Config Rules with automatic remediation, common compliance use cases, and complete Terraform examples.
TL;DR
- AWS Config continuously evaluates resource configurations against rules
- Managed rules cover common compliance scenarios (encryption, public access, tagging)
- Custom rules use Lambda or Guard policy-as-code for specific requirements
- Auto remediation uses SSM Automation documents to fix violations
- IAM permissions are the tricky part - remediation role needs specific actions
Code Repository: All code from this post is available at github.com/moabukar/blog-code/aws-config-auto-remediation
How AWS Config Works
AWS Config records configuration changes to your AWS resources and evaluates them against rules:
┌─────────────────────────────────────────────────────────────────┐
│ AWS Config Flow │
└─────────────────────────────────────────────────────────────────┘
Resource Change Config Rule Remediation
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ S3 Bucket │ ──────► │ Evaluate │ ──────►│ SSM │
│ Created │ │ Against │ │ Automation │
│ │ │ Rules │ │ Document │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ COMPLIANT │ │ Fixed! │
│ or │ │ Encryption │
│NON_COMPLIANT│ │ Enabled │
└─────────────┘ └─────────────┘
The flow is:
- Recording - Config records resource configurations and changes
- Evaluation - Rules evaluate resources (on change or periodic)
- Compliance - Resources are marked COMPLIANT or NON_COMPLIANT
- Remediation - Optional: auto-fix non-compliant resources
Enabling AWS Config
Before using Config Rules, you need a Config Recorder and Delivery Channel:
# S3 bucket for Config recordings
resource "aws_s3_bucket" "config" {
bucket = "my-config-recordings-${data.aws_caller_identity.current.account_id}"
}
resource "aws_s3_bucket_versioning" "config" {
bucket = aws_s3_bucket.config.id
versioning_configuration {
status = "Enabled"
}
}
# IAM role for Config
resource "aws_iam_role" "config" {
name = "aws-config-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "config.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "config" {
role = aws_iam_role.config.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole"
}
resource "aws_iam_role_policy" "config_s3" {
name = "config-s3-policy"
role = aws_iam_role.config.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:PutObjectAcl"
]
Resource = "${aws_s3_bucket.config.arn}/*"
Condition = {
StringLike = {
"s3:x-amz-acl" = "bucket-owner-full-control"
}
}
},
{
Effect = "Allow"
Action = "s3:GetBucketAcl"
Resource = aws_s3_bucket.config.arn
}
]
})
}
# Config Recorder
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
resource "aws_config_delivery_channel" "main" {
name = "default"
s3_bucket_name = aws_s3_bucket.config.id
depends_on = [aws_config_configuration_recorder.main]
}
# Start the recorder
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 Rules
AWS provides 300+ managed rules covering common compliance requirements. No Lambda required - just reference them:
S3 Bucket Encryption
resource "aws_config_config_rule" "s3_encryption" {
name = "s3-bucket-server-side-encryption-enabled"
source {
owner = "AWS"
source_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
}
depends_on = [aws_config_configuration_recorder.main]
}
S3 Public Access Block
resource "aws_config_config_rule" "s3_public_access" {
name = "s3-bucket-public-read-prohibited"
source {
owner = "AWS"
source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED"
}
depends_on = [aws_config_configuration_recorder.main]
}
EBS Encryption
resource "aws_config_config_rule" "ebs_encryption" {
name = "encrypted-volumes"
source {
owner = "AWS"
source_identifier = "ENCRYPTED_VOLUMES"
}
depends_on = [aws_config_configuration_recorder.main]
}
Required Tags
resource "aws_config_config_rule" "required_tags" {
name = "required-tags"
source {
owner = "AWS"
source_identifier = "REQUIRED_TAGS"
}
input_parameters = jsonencode({
tag1Key = "Environment"
tag2Key = "Owner"
tag3Key = "CostCenter"
})
scope {
compliance_resource_types = [
"AWS::EC2::Instance",
"AWS::S3::Bucket",
"AWS::RDS::DBInstance"
]
}
depends_on = [aws_config_configuration_recorder.main]
}
IAM Password Policy
resource "aws_config_config_rule" "iam_password_policy" {
name = "iam-password-policy"
source {
owner = "AWS"
source_identifier = "IAM_PASSWORD_POLICY"
}
input_parameters = jsonencode({
RequireUppercaseCharacters = "true"
RequireLowercaseCharacters = "true"
RequireSymbols = "true"
RequireNumbers = "true"
MinimumPasswordLength = "14"
PasswordReusePrevention = "24"
MaxPasswordAge = "90"
})
depends_on = [aws_config_configuration_recorder.main]
}
RDS Encryption
resource "aws_config_config_rule" "rds_encryption" {
name = "rds-storage-encrypted"
source {
owner = "AWS"
source_identifier = "RDS_STORAGE_ENCRYPTED"
}
depends_on = [aws_config_configuration_recorder.main]
}
Custom Rules with Guard
For requirements not covered by managed rules, use Guard (policy-as-code) or Lambda.
Guard is simpler for most use cases:
resource "aws_config_config_rule" "ec2_instance_type" {
name = "ec2-approved-instance-types"
source {
owner = "CUSTOM_POLICY"
source_detail {
message_type = "ConfigurationItemChangeNotification"
}
custom_policy_details {
policy_runtime = "guard-2.x.x"
policy_text = <<-POLICY
rule ec2_approved_instance_types when resourceType == "AWS::EC2::Instance" {
configuration.instanceType IN ["t3.micro", "t3.small", "t3.medium", "t3.large"]
}
POLICY
}
}
depends_on = [aws_config_configuration_recorder.main]
}
Guard syntax is declarative and readable:
# Ensure RDS instances use approved engine versions
rule rds_approved_versions when resourceType == "AWS::RDS::DBInstance" {
configuration.engineVersion IN ["14.7", "14.8", "15.2", "15.3"]
}
# Ensure EC2 instances don't have public IPs
rule ec2_no_public_ip when resourceType == "AWS::EC2::Instance" {
configuration.publicIpAddress NOT EXISTS OR
configuration.publicIpAddress == null
}
# Ensure S3 buckets have logging enabled
rule s3_logging_enabled when resourceType == "AWS::S3::Bucket" {
supplementaryConfiguration.BucketLoggingConfiguration.destinationBucketName EXISTS
}
Auto Remediation
The magic happens when you connect Config Rules to SSM Automation documents. Non-compliant resources get fixed automatically.
Remediation Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Auto Remediation Flow │
└──────────────────────────────────────────────────────────────────┘
Config Rule Remediation SSM Document
│ Config │
▼ │ ▼
┌────────────────┐ ┌─────────────┐ ┌─────────────┐
│ NON_COMPLIANT │ ──────── │ Trigger │ ──────► │ AWS- │
│ S3 Bucket │ │ Remediation │ │ EnableS3 │
│ (no encryption)│ │ Action │ │ BucketEnc │
└────────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ S3 Bucket │
│ Now Has │
│ Encryption! │
└─────────────┘
IAM Role for Remediation
This is where most people get stuck. The remediation role needs permissions for both SSM and the actions being performed:
# Remediation role
resource "aws_iam_role" "config_remediation" {
name = "config-remediation-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ssm.amazonaws.com"
}
}]
})
}
# SSM Automation permissions
resource "aws_iam_role_policy" "remediation_ssm" {
name = "remediation-ssm"
role = aws_iam_role.config_remediation.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ssm:StartAutomationExecution",
"ssm:GetAutomationExecution"
]
Resource = "*"
}
]
})
}
# S3 remediation permissions
resource "aws_iam_role_policy" "remediation_s3" {
name = "remediation-s3"
role = aws_iam_role.config_remediation.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:PutBucketEncryption",
"s3:PutBucketPublicAccessBlock",
"s3:PutBucketVersioning",
"s3:PutBucketLogging"
]
Resource = "arn:aws:s3:::*"
}
]
})
}
# EC2 remediation permissions
resource "aws_iam_role_policy" "remediation_ec2" {
name = "remediation-ec2"
role = aws_iam_role.config_remediation.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ec2:ModifyInstanceAttribute",
"ec2:StopInstances",
"ec2:StartInstances",
"ec2:TerminateInstances"
]
Resource = "*"
}
]
})
}
S3 Encryption Remediation
resource "aws_config_remediation_configuration" "s3_encryption" {
config_rule_name = aws_config_config_rule.s3_encryption.name
target_type = "SSM_DOCUMENT"
target_id = "AWS-EnableS3BucketEncryption"
target_version = "1"
parameter {
name = "BucketName"
resource_value = "RESOURCE_ID"
}
parameter {
name = "SSEAlgorithm"
static_value = "AES256"
}
parameter {
name = "AutomationAssumeRole"
static_value = aws_iam_role.config_remediation.arn
}
automatic = true
maximum_automatic_attempts = 5
retry_attempt_seconds = 60
execution_controls {
ssm_controls {
concurrent_execution_rate_percentage = 25
error_percentage = 25
}
}
}
S3 Public Access Block Remediation
resource "aws_config_remediation_configuration" "s3_public_access" {
config_rule_name = aws_config_config_rule.s3_public_access.name
target_type = "SSM_DOCUMENT"
target_id = "AWS-DisableS3BucketPublicReadWrite"
target_version = "1"
parameter {
name = "S3BucketName"
resource_value = "RESOURCE_ID"
}
parameter {
name = "AutomationAssumeRole"
static_value = aws_iam_role.config_remediation.arn
}
automatic = true
maximum_automatic_attempts = 5
retry_attempt_seconds = 60
}
Custom SSM Automation Documents
When managed SSM documents don’t fit your needs, create custom ones:
Enable S3 Versioning
resource "aws_ssm_document" "enable_s3_versioning" {
name = "Custom-EnableS3BucketVersioning"
document_type = "Automation"
document_format = "YAML"
content = <<-DOC
description: Enable versioning on S3 bucket
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
BucketName:
type: String
description: Name of the S3 bucket
AutomationAssumeRole:
type: String
description: IAM role for automation
mainSteps:
- name: EnableVersioning
action: aws:executeAwsApi
inputs:
Service: s3
Api: PutBucketVersioning
Bucket: '{{ BucketName }}'
VersioningConfiguration:
Status: Enabled
isEnd: true
DOC
}
resource "aws_config_remediation_configuration" "s3_versioning" {
config_rule_name = aws_config_config_rule.s3_versioning.name
target_type = "SSM_DOCUMENT"
target_id = aws_ssm_document.enable_s3_versioning.name
parameter {
name = "BucketName"
resource_value = "RESOURCE_ID"
}
parameter {
name = "AutomationAssumeRole"
static_value = aws_iam_role.config_remediation.arn
}
automatic = true
maximum_automatic_attempts = 3
retry_attempt_seconds = 60
}
Stop Non-Compliant EC2 Instances
For serious violations, you might want to stop instances:
resource "aws_ssm_document" "stop_ec2_instance" {
name = "Custom-StopNonCompliantEC2"
document_type = "Automation"
document_format = "YAML"
content = <<-DOC
description: Stop non-compliant EC2 instance
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
InstanceId:
type: String
description: EC2 Instance ID
AutomationAssumeRole:
type: String
description: IAM role for automation
mainSteps:
- name: StopInstance
action: aws:executeAwsApi
inputs:
Service: ec2
Api: StopInstances
InstanceIds:
- '{{ InstanceId }}'
- name: WaitForStop
action: aws:waitForAwsResourceProperty
inputs:
Service: ec2
Api: DescribeInstances
InstanceIds:
- '{{ InstanceId }}'
PropertySelector: '$.Reservations[0].Instances[0].State.Name'
DesiredValues:
- stopped
isEnd: true
DOC
}
Tag Non-Compliant Resources
Instead of fixing, tag resources for review:
resource "aws_ssm_document" "tag_non_compliant" {
name = "Custom-TagNonCompliantResource"
document_type = "Automation"
document_format = "YAML"
content = <<-DOC
description: Tag resource as non-compliant
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
ResourceArn:
type: String
description: ARN of the resource
ViolationType:
type: String
description: Type of compliance violation
AutomationAssumeRole:
type: String
description: IAM role for automation
mainSteps:
- name: TagResource
action: aws:executeAwsApi
inputs:
Service: resourcegroupstaggingapi
Api: TagResources
ResourceARNList:
- '{{ ResourceArn }}'
Tags:
compliance-status: non-compliant
violation-type: '{{ ViolationType }}'
detected-at: '{{ global:DATE_TIME }}'
isEnd: true
DOC
}
Common Remediation Patterns
Pattern 1: Encryption Everywhere
locals {
encryption_rules = {
s3 = {
rule_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
remediation_doc = "AWS-EnableS3BucketEncryption"
parameters = {
BucketName = "RESOURCE_ID"
SSEAlgorithm = "aws:kms"
KMSMasterKey = data.aws_kms_key.default.arn
}
}
ebs = {
rule_identifier = "ENCRYPTED_VOLUMES"
remediation_doc = null # Can't encrypt existing volumes - alert only
parameters = {}
}
rds = {
rule_identifier = "RDS_STORAGE_ENCRYPTED"
remediation_doc = null # Can't encrypt existing RDS - alert only
parameters = {}
}
}
}
resource "aws_config_config_rule" "encryption" {
for_each = local.encryption_rules
name = "${each.key}-encryption-enabled"
source {
owner = "AWS"
source_identifier = each.value.rule_identifier
}
depends_on = [aws_config_configuration_recorder.main]
}
resource "aws_config_remediation_configuration" "encryption" {
for_each = { for k, v in local.encryption_rules : k => v if v.remediation_doc != null }
config_rule_name = aws_config_config_rule.encryption[each.key].name
target_type = "SSM_DOCUMENT"
target_id = each.value.remediation_doc
dynamic "parameter" {
for_each = each.value.parameters
content {
name = parameter.key
static_value = parameter.value != "RESOURCE_ID" ? parameter.value : null
resource_value = parameter.value == "RESOURCE_ID" ? "RESOURCE_ID" : null
}
}
parameter {
name = "AutomationAssumeRole"
static_value = aws_iam_role.config_remediation.arn
}
automatic = true
maximum_automatic_attempts = 5
retry_attempt_seconds = 60
}
Pattern 2: Security Baseline
locals {
security_baseline = {
"s3-public-read" = "S3_BUCKET_PUBLIC_READ_PROHIBITED"
"s3-public-write" = "S3_BUCKET_PUBLIC_WRITE_PROHIBITED"
"ec2-imdsv2" = "EC2_IMDSV2_CHECK"
"iam-root-mfa" = "ROOT_ACCOUNT_MFA_ENABLED"
"iam-user-mfa" = "IAM_USER_MFA_ENABLED"
"rds-public" = "RDS_INSTANCE_PUBLIC_ACCESS_CHECK"
"sg-ssh-restricted" = "INCOMING_SSH_DISABLED"
"cloudtrail-enabled" = "CLOUDTRAIL_ENABLED"
"vpc-flow-logs" = "VPC_FLOW_LOGS_ENABLED"
"guardduty-enabled" = "GUARDDUTY_ENABLED_CENTRALIZED"
}
}
resource "aws_config_config_rule" "security_baseline" {
for_each = local.security_baseline
name = each.key
source {
owner = "AWS"
source_identifier = each.value
}
depends_on = [aws_config_configuration_recorder.main]
}
Pattern 3: Alerting Without Remediation
For sensitive resources where auto-remediation is risky:
resource "aws_config_config_rule" "production_changes" {
name = "production-resource-changes"
source {
owner = "CUSTOM_POLICY"
source_detail {
message_type = "ConfigurationItemChangeNotification"
}
custom_policy_details {
policy_runtime = "guard-2.x.x"
policy_text = <<-POLICY
# Alert on any change to production-tagged resources
rule production_change_alert {
tags.Environment == "production"
}
POLICY
}
}
}
# SNS notification instead of remediation
resource "aws_sns_topic" "config_alerts" {
name = "config-compliance-alerts"
}
resource "aws_config_config_rule" "notify_sns" {
# ... rule config ...
}
# CloudWatch Event to trigger SNS
resource "aws_cloudwatch_event_rule" "config_compliance" {
name = "config-compliance-change"
event_pattern = jsonencode({
source = ["aws.config"]
detail-type = ["Config Rules Compliance Change"]
detail = {
messageType = ["ComplianceChangeNotification"]
newEvaluationResult = {
complianceType = ["NON_COMPLIANT"]
}
}
})
}
resource "aws_cloudwatch_event_target" "sns" {
rule = aws_cloudwatch_event_rule.config_compliance.name
target_id = "SendToSNS"
arn = aws_sns_topic.config_alerts.arn
}
Multi-Account Setup with AWS Organizations
For organisation-wide compliance, use AWS Config Aggregator:
resource "aws_config_configuration_aggregator" "organisation" {
name = "organisation-aggregator"
organization_aggregation_source {
all_regions = true
role_arn = aws_iam_role.config_aggregator.arn
}
}
resource "aws_iam_role" "config_aggregator" {
name = "config-aggregator-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "config.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "config_aggregator" {
role = aws_iam_role.config_aggregator.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSConfigRoleForOrganizations"
}
Deploy rules across all accounts using Organisation Config Rules:
resource "aws_config_organization_managed_rule" "s3_encryption" {
name = "org-s3-encryption"
rule_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
excluded_accounts = [
"123456789012", # Security account - managed separately
]
}
Troubleshooting
Remediation Not Triggering
Check the remediation execution status:
aws configservice describe-remediation-execution-status \
--config-rule-name s3-bucket-server-side-encryption-enabled
Common issues:
- Missing IAM permissions - Remediation role lacks required actions
- SSM document not found - Check document name and region
- Rate limiting - Adjust
concurrent_execution_rate_percentage
Finding Available SSM Documents
List AWS-provided remediation documents:
aws ssm list-documents \
--filters "Key=Owner,Values=Amazon" \
--query "DocumentIdentifiers[?contains(Name, 'AWS-')].Name" \
--output table
Debugging Custom Documents
Test SSM documents manually before attaching to Config:
aws ssm start-automation-execution \
--document-name "Custom-EnableS3BucketVersioning" \
--parameters "BucketName=my-test-bucket,AutomationAssumeRole=arn:aws:iam::123456789012:role/config-remediation-role"
Check execution status:
aws ssm get-automation-execution \
--automation-execution-id <execution-id>
Best Practices
1. Start with Detection, Then Add Remediation
Don’t enable auto-remediation immediately. First:
- Deploy rules in detection-only mode
- Review compliance reports
- Understand the scope of violations
- Test remediation manually
- Then enable automatic remediation
2. Exclude Sensitive Resources
Some resources shouldn’t be auto-remediated:
resource "aws_config_config_rule" "s3_encryption" {
name = "s3-bucket-encryption"
source {
owner = "AWS"
source_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
}
# Exclude specific buckets
scope {
compliance_resource_id = "my-special-bucket" # Exclude this
tag_key = "AutoRemediate"
tag_value = "false" # Or exclude by tag
}
}
3. Rate Limit Remediation
Prevent remediation storms:
execution_controls {
ssm_controls {
concurrent_execution_rate_percentage = 10 # Only 10% at a time
error_percentage = 10 # Stop if >10% fail
}
}
4. Log Everything
Enable CloudTrail logging for Config and SSM:
resource "aws_cloudtrail" "config_audit" {
name = "config-audit-trail"
s3_bucket_name = aws_s3_bucket.audit_logs.id
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::"]
}
}
}
5. Use Conformance Packs
For standards like CIS or PCI-DSS, use conformance packs:
resource "aws_config_conformance_pack" "cis" {
name = "cis-aws-foundations-benchmark"
template_body = file("${path.module}/conformance-packs/cis-benchmark.yaml")
input_parameter {
parameter_name = "AccessKeysRotatedParameterMaxAccessKeyAge"
parameter_value = "90"
}
}
Cost Considerations
AWS Config pricing:
- Configuration items recorded: $0.003 per item
- Config rule evaluations: $0.001 per evaluation
- Conformance pack evaluations: $0.001 per evaluation per rule
For a medium account (1000 resources, 20 rules):
- Monthly cost: ~$50-100
- Cost per remediation: Free (you pay for SSM, which is also free for most use cases)
Conclusion
AWS Config with auto remediation transforms compliance from a manual audit exercise into continuous enforcement. Resources that violate policies get fixed automatically, reducing the window of non-compliance from weeks to minutes.
Start with managed rules for common scenarios, then add custom Guard policies for specific requirements. Always test remediation manually before enabling automatic mode, and be careful with remediation on production resources.
The combination of Config Rules + SSM Automation is powerful - use it wisely.