Infrastructure drift is the gap between what your Terraform says exists and what actually exists in your account. Someone opens a security group rule manually during an incident and forgets to close it. An S3 bucket loses its public access block because a developer needed to test something quickly. An IAM policy gets broadened to fix a permission issue and never gets tightened again.
AWS Config records every configuration change across your resources continuously and evaluates them against compliance rules you define. When a resource drifts out of compliance, Config flags it and optionally triggers automated remediation. In this article I will walk through setting up Config with managed rules, custom Lambda-based rules, and automated remediation with Terraform.
Enabling AWS Config
resource "aws_s3_bucket" "config" {
bucket = "aws-config-${data.aws_caller_identity.current.account_id}-${var.region}"
}
resource "aws_s3_bucket_versioning" "config" {
bucket = aws_s3_bucket.config.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "config" {
bucket = aws_s3_bucket.config.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.config.arn
}
}
}
resource "aws_s3_bucket_public_access_block" "config" {
bucket = aws_s3_bucket.config.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_config_configuration_recorder" "main" {
name = "main"
role_arn = aws_iam_role.config.arn
recording_group {
all_supported = true
include_global_resource_types = true
}
}
resource "aws_config_delivery_channel" "main" {
name = "main"
s3_bucket_name = aws_s3_bucket.config.bucket
sns_topic_arn = aws_sns_topic.config_notifications.arn
snapshot_delivery_properties {
delivery_frequency = "Six_Hours"
}
depends_on = [aws_config_configuration_recorder.main]
}
resource "aws_config_configuration_recorder_status" "main" {
name = aws_config_configuration_recorder.main.name
is_enabled = true
depends_on = [aws_config_delivery_channel.main]
}
include_global_resource_types = true includes IAM resources in Config recording. IAM users, roles, and policies are global resources and some of the most important things to track for compliance. Without this setting, IAM changes are not captured.
Managed Config Rules
AWS maintains over 200 managed Config rules covering common compliance requirements. Deploy a baseline set that covers the most critical security controls.
locals {
managed_rules = {
"s3-bucket-public-read-prohibited" = {}
"s3-bucket-public-write-prohibited" = {}
"s3-bucket-server-side-encryption-enabled" = {}
"s3-bucket-ssl-requests-only" = {}
"rds-instance-public-access-check" = {}
"rds-storage-encrypted" = {}
"rds-multi-az-support" = {}
"encrypted-volumes" = {}
"root-account-mfa-enabled" = {}
"iam-user-mfa-enabled" = {}
"iam-password-policy" = {
RequireUppercaseCharacters = "true"
RequireLowercaseCharacters = "true"
RequireSymbols = "true"
RequireNumbers = "true"
MinimumPasswordLength = "14"
PasswordReusePrevention = "12"
MaxPasswordAge = "90"
}
"guardduty-enabled-centralized" = {}
"cloudtrail-enabled" = {}
"cloud-trail-encryption-enabled" = {}
"vpc-flow-logs-enabled" = {}
"vpc-default-security-group-closed" = {}
"restricted-ssh" = {}
"restricted-common-ports" = {}
"lambda-function-public-access-prohibited" = {}
"secretsmanager-rotation-enabled-check" = {}
}
}
resource "aws_config_config_rule" "managed" {
for_each = local.managed_rules
name = each.key
source {
owner = "AWS"
source_identifier = upper(replace(each.key, "-", "_"))
}
input_parameters = length(each.value) > 0 ? jsonencode(each.value) : null
depends_on = [aws_config_configuration_recorder.main]
}
Custom Config Rules with Lambda
When managed rules do not cover your specific requirement, write a custom rule as a Lambda function.
import boto3
import json
config = boto3.client("config")
def lambda_handler(event, context):
invoking_event = json.loads(event["invokingEvent"])
configuration_item = invoking_event.get("configurationItem", {})
if configuration_item.get("resourceType") != "AWS::RDS::DBInstance":
return
result_token = event["resultToken"]
resource_id = configuration_item["resourceId"]
config_data = configuration_item.get("configuration", {})
is_compliant = True
annotation = "RDS instance meets all requirements"
if not config_data.get("deletionProtection", False):
is_compliant = False
annotation = "RDS instance does not have deletion protection enabled"
elif not config_data.get("backupRetentionPeriod", 0) >= 7:
is_compliant = False
annotation = "RDS instance backup retention is less than 7 days"
elif not config_data.get("multiAZ", False) and config_data.get("dbInstanceClass", "") not in ["db.t3.micro", "db.t3.small"]:
is_compliant = False
annotation = "Production-class RDS instance is not Multi-AZ"
config.put_evaluations(
Evaluations=[
{
"ComplianceResourceType": configuration_item["resourceType"],
"ComplianceResourceId": resource_id,
"ComplianceType": "COMPLIANT" if is_compliant else "NON_COMPLIANT",
"Annotation": annotation,
"OrderingTimestamp": configuration_item["configurationItemCaptureTime"]
}
],
ResultToken=result_token
)
resource "aws_config_config_rule" "rds_production_standards" {
name = "rds-production-standards"
description = "RDS instances must have deletion protection, 7-day backup, and Multi-AZ"
source {
owner = "CUSTOM_LAMBDA"
source_identifier = aws_lambda_function.rds_compliance_check.arn
source_detail {
event_source = "aws.config"
message_type = "ConfigurationItemChangeNotification"
}
}
scope {
compliance_resource_types = ["AWS::RDS::DBInstance"]
}
depends_on = [aws_config_configuration_recorder.main]
}
Automated Remediation
resource "aws_config_remediation_configuration" "s3_public_access" {
config_rule_name = "s3-bucket-public-read-prohibited"
target_type = "SSM_DOCUMENT"
target_id = "AWS-DisableS3BucketPublicReadWrite"
automatic = true
execution_controls {
ssm_controls {
concurrent_execution_rate_percentage = 25
error_percentage = 20
}
}
maximum_automatic_attempts = 3
retry_attempt_seconds = 60
parameter {
name = "AutomationAssumeRole"
static_value = aws_iam_role.config_remediation.arn
}
parameter {
name = "BucketName"
resource_value = "RESOURCE_ID"
}
}
concurrent_execution_rate_percentage = 25 and error_percentage = 20 are safety controls on bulk remediation. If you have 100 non-compliant S3 buckets, Config will remediate 25 at a time. If more than 20 percent of remediation attempts fail, it stops and alerts you rather than blindly continuing. These defaults prevent a misconfigured remediation from cascading across your entire account.
Querying Config with Advanced Queries
Config Advanced Queries lets you run SQL against your current resource inventory. This is faster than clicking through the console for ad hoc compliance checks.
SELECT
resourceId,
resourceName,
configuration.dbInstanceClass,
configuration.multiAZ,
configuration.storageEncrypted,
configuration.deletionProtection
FROM
aws_rds_dbinstance
WHERE
configuration.multiAZ = false
AND configuration.dbInstanceClass NOT IN ('db.t3.micro', 'db.t3.small')
This query returns all production-class RDS instances that are not Multi-AZ. Save these queries and run them before quarterly audits or when onboarding a new environment. The results are immediate because Config maintains a live inventory of all resource configurations.
Closing Thoughts
AWS Config is the foundation of continuous compliance in AWS. Without it, you can only verify compliance at a point in time, usually right before an audit. With it, you have a continuous record of every configuration change and an immediate view of which resources are out of compliance at any moment.
Enable Config in every region you use, not just your primary region. Apply the managed rules that cover your most critical security controls. Write custom rules for the requirements that managed rules do not cover. Enable automatic remediation for violations where the correct action is unambiguous, like public S3 buckets or missing encryption. Review the compliance dashboard weekly and treat non-compliant resources the same way you treat failing tests in CI: block until fixed.
Enjoy the cloud.
Osama
#AWS #AWSConfig #Compliance #CloudGovernance #CloudSecurity #Terraform #InfrastructureAsCode #AmazonWebServices #SolutionsArchitect #CloudComputing #DriftDetection #SecurityEngineering #DevSecOps #TechBlog #CloudInfrastructure #Automation #IAM #Remediation #CloudAudit #Engineering
Leave a comment