AWS Config Custom Compliance Rules for Enterprise Governance

Building custom AWS Config rules with Lambda and Guard policy language for organization-specific compliance requirements

#aws#config#compliance#governance
Cover image for the article: AWS Config Custom Compliance Rules for Enterprise Governance

Introduction

AWS Config provides continuous compliance monitoring by evaluating resource configurations against defined rules. While AWS offers 300+ managed rules, enterprise environments invariably require custom rules for organization-specific policies that managed rules cannot cover. Custom rules enable enforcement of internal security standards, naming conventions, cost controls, and architectural patterns.

In a multi-account environment with 2,000+ resources, custom Config rules detect an average of 150-200 non-compliant resources per week that would otherwise go unnoticed until an audit or incident.

Rule Types Comparison

FeatureManaged RulesCustom Lambda RulesCustom Guard Rules
MaintenanceAWS-managedSelf-managedSelf-managed
LanguageN/APython, Node.js, JavaGuard DSL
EvaluationProactive + DetectiveDetective onlyProactive + Detective
ExecutionAWS Lambda (internal)Your LambdaAWS-managed
Cost$0.001/evaluation$0.001 + Lambda cost$0.001/evaluation
ComplexityConfiguration onlyFull programmingPolicy-as-code
Cross-resourceLimitedFull API accessSingle resource

Chart

Custom Lambda Rules

Rule 1: Enforce Tagging Standards

import json
import boto3
from datetime import datetime

# Required tags for all resources
REQUIRED_TAGS = {
    'Environment': ['production', 'staging', 'development', 'sandbox'],
    'Team': None,  # Any value accepted
    'CostCenter': None,
    'DataClassification': ['public', 'internal', 'confidential', 'restricted']
}

def lambda_handler(event, context):
    """Evaluate resource tagging compliance."""
    config = boto3.client('config')

    invoking_event = json.loads(event['invokingEvent'])
    configuration_item = invoking_event.get('configurationItem', {})
    resource_type = configuration_item.get('resourceType', '')
    resource_id = configuration_item.get('resourceId', '')

    # Skip resources that don't support tags
    non_taggable = ['AWS::Config::ConfigRule', 'AWS::Config::ConfigurationRecorder']
    if resource_type in non_taggable:
        compliance = 'NOT_APPLICABLE'
        annotation = 'Resource type does not support tagging'
    else:
        tags = configuration_item.get('tags', {})
        compliance, annotation = evaluate_tags(tags, resource_type)

    config.put_evaluations(
        Evaluations=[{
            'ComplianceResourceType': resource_type,
            'ComplianceResourceId': resource_id,
            'ComplianceType': compliance,
            'Annotation': annotation[:256],  # Max 256 chars
            'OrderingTimestamp': datetime.utcnow()
        }],
        ResultToken=event['resultToken']
    )

def evaluate_tags(tags, resource_type):
    """Check tags against required standards."""
    missing_tags = []
    invalid_values = []

    for tag_key, allowed_values in REQUIRED_TAGS.items():
        if tag_key not in tags:
            missing_tags.append(tag_key)
        elif allowed_values and tags[tag_key] not in allowed_values:
            invalid_values.append(f"{tag_key}={tags[tag_key]}")

    if missing_tags or invalid_values:
        issues = []
        if missing_tags:
            issues.append(f"Missing: {', '.join(missing_tags)}")
        if invalid_values:
            issues.append(f"Invalid: {', '.join(invalid_values)}")
        return 'NON_COMPLIANT', '; '.join(issues)

    return 'COMPLIANT', 'All required tags present with valid values'

Rule 2: Enforce Encryption Standards

import json
import boto3
from datetime import datetime

def lambda_handler(event, context):
    """Evaluate encryption compliance for storage resources."""
    config = boto3.client('config')

    invoking_event = json.loads(event['invokingEvent'])
    ci = invoking_event.get('configurationItem', {})
    resource_type = ci.get('resourceType', '')
    resource_id = ci.get('resourceId', '')

    evaluators = {
        'AWS::S3::Bucket': evaluate_s3_encryption,
        'AWS::RDS::DBInstance': evaluate_rds_encryption,
        'AWS::EBS::Volume': evaluate_ebs_encryption,
        'AWS::EFS::FileSystem': evaluate_efs_encryption,
    }

    evaluator = evaluators.get(resource_type)
    if evaluator:
        compliance, annotation = evaluator(ci)
    else:
        compliance = 'NOT_APPLICABLE'
        annotation = 'Resource type not evaluated for encryption'

    config.put_evaluations(
        Evaluations=[{
            'ComplianceResourceType': resource_type,
            'ComplianceResourceId': resource_id,
            'ComplianceType': compliance,
            'Annotation': annotation[:256],
            'OrderingTimestamp': datetime.utcnow()
        }],
        ResultToken=event['resultToken']
    )

def evaluate_s3_encryption(ci):
    """S3 must use SSE-KMS with CMK (not SSE-S3)."""
    config = ci.get('supplementaryConfiguration', {})
    encryption = config.get('ServerSideEncryptionConfiguration', {})

    if not encryption:
        return 'NON_COMPLIANT', 'No server-side encryption configured'

    rules = encryption.get('rules', [])
    for rule in rules:
        sse = rule.get('applyServerSideEncryptionByDefault', {})
        if sse.get('sseAlgorithm') == 'aws:kms':
            if sse.get('kmsMasterKeyID', '').startswith('arn:aws:kms:'):
                return 'COMPLIANT', 'SSE-KMS with CMK enabled'
            return 'NON_COMPLIANT', 'SSE-KMS must use customer-managed CMK'
        return 'NON_COMPLIANT', 'Must use SSE-KMS (not SSE-S3 or AES256)'

    return 'NON_COMPLIANT', 'Encryption configuration incomplete'

def evaluate_rds_encryption(ci):
    """RDS must have storage encryption enabled."""
    configuration = ci.get('configuration', {})
    if configuration.get('storageEncrypted', False):
        return 'COMPLIANT', 'Storage encryption enabled'
    return 'NON_COMPLIANT', 'Storage encryption not enabled'

def evaluate_ebs_encryption(ci):
    """EBS volumes must be encrypted."""
    configuration = ci.get('configuration', {})
    if configuration.get('encrypted', False):
        return 'COMPLIANT', 'Volume encryption enabled'
    return 'NON_COMPLIANT', 'Volume is not encrypted'

def evaluate_efs_encryption(ci):
    """EFS must have encryption at rest."""
    configuration = ci.get('configuration', {})
    if configuration.get('encrypted', False):
        return 'COMPLIANT', 'Encryption at rest enabled'
    return 'NON_COMPLIANT', 'Encryption at rest not enabled'

Custom Guard Rules (Proactive)

Guard rules evaluate resource configurations before creation using CloudFormation Guard DSL:

Guard Rule: Network Security

# network-security.guard
# Ensure security groups don't allow unrestricted ingress

rule security_group_no_unrestricted_ingress when
    resourceType == "AWS::EC2::SecurityGroup" {

    configuration.ipPermissions[*] {
        # No 0.0.0.0/0 on non-HTTP/HTTPS ports
        when ipRanges[*].cidrIp == "0.0.0.0/0" {
            fromPort in [80, 443]
            toPort in [80, 443]
        }

        # No ::/0 on any port
        ipv6Ranges[*].cidrIpv6 != "::/0"
    }
}

rule security_group_no_all_traffic when
    resourceType == "AWS::EC2::SecurityGroup" {

    configuration.ipPermissions[*] {
        # No rules allowing all protocols (-1)
        ipProtocol != "-1"
    }
}

Guard Rule: Instance Standards

# instance-standards.guard

rule ec2_instance_type_approved when
    resourceType == "AWS::EC2::Instance" {

    # Only allow approved instance families
    configuration.instanceType in [
        /^t3\..*/,
        /^t3a\..*/,
        /^m6i\..*/,
        /^c6i\..*/,
        /^r6i\..*/
    ]
}

rule ec2_no_public_ip when
    resourceType == "AWS::EC2::Instance" {

    # Instances should not have public IPs directly
    configuration.publicIpAddress not exists or
    configuration.publicIpAddress == ""
}

rule ec2_imdsv2_required when
    resourceType == "AWS::EC2::Instance" {

    # Require IMDSv2 (no IMDSv1)
    configuration.metadataOptions.httpTokens == "required"
    configuration.metadataOptions.httpEndpoint == "enabled"
}

Deploying Custom Rules at Scale

Organization-Wide Deployment

# Deploy Config rule across all accounts via CloudFormation StackSet
resource "aws_cloudformation_stack_set" "config_rules" {
  name             = "organization-config-rules"
  permission_model = "SERVICE_MANAGED"

  auto_deployment {
    enabled                          = true
    retain_stacks_on_account_removal = false
  }

  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Resources = {
      TaggingRule = {
        Type = "AWS::Config::ConfigRule"
        Properties = {
          ConfigRuleName = "required-tags-custom"
          Source = {
            Owner            = "CUSTOM_LAMBDA"
            SourceIdentifier = var.tagging_lambda_arn
            SourceDetails = [{
              EventSource = "aws.config"
              MessageType = "ConfigurationItemChangeNotification"
            }]
          }
          Scope = {
            ComplianceResourceTypes = [
              "AWS::EC2::Instance",
              "AWS::S3::Bucket",
              "AWS::RDS::DBInstance"
            ]
          }
        }
      }
    }
  })
}

Compliance Dashboard

Aggregated Compliance Metrics

Rule CategoryTotal ResourcesCompliantNon-Compliant% Compliant
Tagging2,4502,18027089%
Encryption1,8301,7458595.4%
Network8908424894.6%
IAM5604986288.9%
Logging3403221894.7%
Total6,0705,58748392%

Automated Compliance Reporting

# Generate compliance report
aws configservice get-compliance-summary-by-config-rule \
  --query 'ComplianceSummary.{
    Compliant: CompliantResourceCount.CappedCount,
    NonCompliant: NonCompliantResourceCount.CappedCount
  }'

# Export non-compliant resources
aws configservice get-compliance-details-by-config-rule \
  --config-rule-name "required-tags-custom" \
  --compliance-types NON_COMPLIANT \
  --query 'EvaluationResults[].{
    ResourceId: EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId,
    ResourceType: EvaluationResultIdentifier.EvaluationResultQualifier.ResourceType,
    Annotation: Annotation
  }' \
  --output table

Key Takeaways

  • Custom Lambda rules provide full AWS API access for complex evaluations that span multiple resource attributes or require cross-resource checks.
  • Guard rules enable proactive compliance by evaluating resources before creation through CloudFormation hooks, preventing non-compliant resources from being deployed.
  • Deploy rules organization-wide via StackSets to ensure consistent compliance evaluation across all accounts without manual per-account configuration.
  • Tag compliance is typically the lowest (85-90%) and should be addressed with both Config rules for detection and Service Control Policies for prevention.
  • Custom rules detect 150-200 non-compliant resources per week in a typical enterprise environment that managed rules alone would miss.
  • Combine detective and preventive controls using Config rules for detection, SCPs for hard prevention, and Guard rules for proactive CloudFormation validation.
  • Keep rule evaluation costs manageable by scoping rules to specific resource types rather than evaluating all resources against all rules.

Comments

    No comments yet. Be the first to share your thoughts.