AWS Config Custom Compliance Rules for Enterprise Governance
Building custom AWS Config rules with Lambda and Guard policy language for organization-specific compliance requirements

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
| Feature | Managed Rules | Custom Lambda Rules | Custom Guard Rules |
|---|---|---|---|
| Maintenance | AWS-managed | Self-managed | Self-managed |
| Language | N/A | Python, Node.js, Java | Guard DSL |
| Evaluation | Proactive + Detective | Detective only | Proactive + Detective |
| Execution | AWS Lambda (internal) | Your Lambda | AWS-managed |
| Cost | $0.001/evaluation | $0.001 + Lambda cost | $0.001/evaluation |
| Complexity | Configuration only | Full programming | Policy-as-code |
| Cross-resource | Limited | Full API access | Single resource |
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 Category | Total Resources | Compliant | Non-Compliant | % Compliant |
|---|---|---|---|---|
| Tagging | 2,450 | 2,180 | 270 | 89% |
| Encryption | 1,830 | 1,745 | 85 | 95.4% |
| Network | 890 | 842 | 48 | 94.6% |
| IAM | 560 | 498 | 62 | 88.9% |
| Logging | 340 | 322 | 18 | 94.7% |
| Total | 6,070 | 5,587 | 483 | 92% |
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.
Recommended reading

Per-Team Cost Allocation in Shared Kubernetes Clusters: From Chaos to Clarity
Implementing accurate per-namespace cost allocation in multi-tenant Kubernetes clusters, covering request vs. usage attribution, shared resource amortization, and building showback dashboards that drive accountability.

Measuring and Eliminating Toil: From 40% to 12% of Engineering Time
A systematic approach to identifying, measuring, and automating toil—the repetitive operational work that scales linearly with service growth and prevents engineers from doing creative work.

Serverless Postgres in Production: Branching, Scale-to-Zero, and the End of Database Provisioning
Running Neon serverless Postgres in production for 8 months — covering database branching workflows, scale-to-zero economics, connection pooling, and migration from RDS.

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