AWS Governance

AWS Config: Resource Inventory, Compliance Rules & Remediation Hands-On

The auditor’s question is never “was this resource created correctly?” — it is “is it still correct, right now, and how would you know if it drifted?” A volume created encrypted can be replaced by an unencrypted one six months later; a bucket locked down on Monday can be opened for a demo on Friday and forgotten; an IAM policy tightened in review can be loosened in an incident and never tightened back. Preventive controls — IAM, SCPs, resource policies — stop the first bad action, but they cannot tell you what your estate looks like today or prove it stayed that way. That is the gap AWS Config fills. Config is the service that continuously records the configuration of every supported resource in your account, remembers how each one changed over time, and re-checks all of them against rules you write — so “is it still right?” has a real, timestamped, queryable answer instead of a hopeful one.

This article is the deep reference for running Config in production, from a single account to a whole organization. We treat it as a five-stage machine — record → deliver → evaluate → remediate → aggregate — and go through each stage option by option: the configuration recorder and its recording group, the delivery channel to S3 and SNS, configuration items and the resource relationships that make the inventory a graph rather than a list, AWS-managed rules versus custom rules (Lambda-backed and the newer CloudFormation Guard policy rules), config-change versus periodic triggers, conformance packs as versioned rule bundles, remediation through SSM Automation runbooks (manual and automatic), and aggregators that roll multi-account, multi-region data into one pane via AWS Organizations. Because this is a document you open mid-incident and mid-audit, the settings, triggers, rule states, limits, IAM and KMS gotchas, cost drivers and the failure playbook are all laid out as tables you can scan — read the prose once, keep the tables open when the QSA is in the room.

By the end you will stop trusting a green dashboard and start proving it. You will know why a recorder that never started makes every rule report nothing (which looks exactly like COMPLIANT), why recording global resources in every region quietly triples your bill, why a rule sits forever in EVALUATING, why auto-remediation either never fires or fires in a loop, and how to turn all of this on with aws configservice and Terraform in a way that survives an org-wide rollout. The mechanism is simple; getting every setting right so the evidence holds up is the craft.

What problem this solves

Config solves three problems that prevention alone cannot: inventory (what resources exist, across accounts and regions, with their real configuration), drift detection (has any resource stopped matching policy since it was created), and evidence (a timestamped history you can hand an auditor). Without it, your “current state” lives in people’s heads and half-stale spreadsheets, your compliance posture is a point-in-time screenshot that rots the moment it is taken, and your only answer to “prove nothing was public in Q2” is “we’re pretty sure.”

What breaks without it, concretely: a Terraform module default flips and three hundred new EBS volumes ship unencrypted — invisible until the audit, because nothing re-checked them after creation. An engineer opens an S3 bucket for a one-off data share and it is found by a security researcher instead of by you. A new member account is onboarded in a hurry and its Config recorder is never started, so for three months every compliance rule returns empty — and empty renders green on the dashboard, so nobody notices the blind spot until it is a finding. Each of these is a board-level incident when discovered the wrong way, and each is trivially catchable with a recorder that cannot be opted out of and rules that re-evaluate reality on a schedule.

Who hits this: everyone past a handful of resources, and hardest of all the teams who confuse “we turned Config on once” with “every account is recording, every rule is scoped correctly, and someone acts on NON_COMPLIANT.” It is the substrate every framework — PCI-DSS, SOC 2, HIPAA, ISO 27001, FedRAMP, CIS — assumes you already have, and the engine underneath Security Hub controls, Control Tower detective guardrails, and conformance packs. To frame the whole field before the deep dive, here is what Config does, what it does not do, and where each responsibility actually lives:

Capability Does Config do it? How Where the answer lives Common wrong assumption
Inventory of resources Yes Configuration items per resource Config console / advanced query “The tag editor is my inventory”
Configuration history / timeline Yes CI per change, kept in S3 Resource timeline; S3 history files “CloudTrail already has this” (it has the API call, not the state)
Who made the change Partially CI links to the CloudTrail event CloudTrail (Config references it) “Config tells me who” (it points to CloudTrail)
Continuous compliance checks Yes Config rules (managed + custom) Rule compliance; aggregator “IAM/SCP already enforces this” (those are preventive)
Auto-fix a violation Yes Remediation via SSM Automation Remediation execution status “Config fixes it by itself” (only if you wire remediation)
Multi-account roll-up Yes Aggregator (Organizations) Aggregator dashboard / queries “I’ll just log into each account”
Real-time blocking No Config is detective, not preventive “Config will stop the bad action” (it detects after the fact)
Alerting / ticketing Indirectly SNS + EventBridge → your target EventBridge rule targets “Config emails me” (you wire the notification)

Learning objectives

By the end of this article you can:

This maps directly to SOA-C02 (SysOps — Config, remediation, conformance packs), SCS-C02 (Security Specialty — detective controls, evidence), and SAP-C02 (Professional — org-wide governance and aggregators).

Prerequisites & where this fits

You should be comfortable with the AWS account model (accounts, regions, IAM roles), reading and writing JSON, running the AWS CLI, and the basics of S3 (bucket policies), SNS, IAM roles and trust policies, and AWS Organizations. Familiarity with CloudTrail helps because Config and CloudTrail are the two halves of one evidence machine — CloudTrail records the API call (who/what/when), Config records the resulting resource state and whether it still complies. If you have read AWS CloudTrail and Config: Audit and Compliance at Scale, this article is the Config half in far greater operational depth.

Config sits in the Governance layer, downstream of your account structure and upstream of your compliance reporting. It assumes an org foundation (AWS Organizations, SCPs & Multi-Account Guardrails and, if you use it, AWS Control Tower guardrails), and it consumes a tagging strategy — the required-tags rule is only as useful as the tag standard behind it. Config’s detective guardrails and the preventive SCPs are complementary: SCPs stop the action, Config proves it stayed stopped and catches what slipped through. Here is who owns which layer during a real compliance program, so you call the right person fast:

Layer What lives here Who usually owns it What Config contributes
Account structure OUs, accounts, SCPs Platform / landing-zone team Aggregator scoped to the org
Recording plane Recorder, delivery channel Platform (per account, templated) The raw CIs everything else needs
Rule authoring Managed + custom rules, packs Security / compliance engineering Compliance verdicts
Remediation SSM runbooks, assume-roles Platform + service teams Automatic fixes on NON_COMPLIANT
Reporting / audit Dashboards, evidence exports GRC / security leadership Timelines, select-aggregate queries
Cost Per-CI + per-eval spend FinOps + platform Recording scope and frequency tuning

Core concepts

Six mental models make every later decision obvious.

Config is detective, not preventive. It does not block anything. It observes the result of a change, records it, and judges it after the fact. The value is the record and the judgement, not enforcement — enforcement is IAM/SCP/resource-policy territory. Internalise this and you stop expecting Config to “stop” a bad action and start using it to catch and fix one.

The atom is the configuration item (CI). A configuration item is a point-in-time JSON snapshot of one resource: its attributes, its relationships to other resources, the CloudTrail events that touched it, tags, and metadata. Config emits a new CI every time a resource is created, changed, or deleted. The stream of CIs for one resource is its configuration timeline. You pay per CI recorded — which is why what and how often you record is the central cost lever.

One recorder per region, per account. Config runs a single configuration recorder in each region of each account. Its recording group decides which resource types it captures (all supported, an inclusion list, or all-except-an-exclusion list) and whether it includes global resource types (IAM users, groups, roles, policies). The recorder writes to a delivery channel — a mandatory S3 bucket and an optional SNS topic.

A rule is a function that returns a compliance verdict. A Config rule evaluates CIs (or the whole account periodically) and stamps each in-scope resource COMPLIANT, NON_COMPLIANT, NOT_APPLICABLE, or INSUFFICIENT_DATA. Managed rules are AWS-authored; custom rules are yours, backed either by a Lambda function or by a CloudFormation Guard policy (no Lambda to run). A rule fires on configuration changes, on a periodic schedule, or both.

Remediation is a separate, opt-in wiring. A NON_COMPLIANT verdict does nothing on its own. You attach a remediation configuration that runs an SSM Automation document (runbook) against the offending resource — manually (you click/trigger) or automatically (Automatic=true). The runbook executes under an AutomationAssumeRole you provide.

Aggregators give one pane over many accounts. A configuration aggregator collects CIs and compliance from multiple accounts and regions into one place — either an explicit list of accounts (each must authorize the aggregator) or, far better, the whole Organization (no per-account authorization). The aggregator is read-only: you query and dashboard, you do not record or remediate from it.

The vocabulary in one table

Before the deep sections, pin down every moving part side by side; the glossary at the end repeats these for lookup:

Concept One-line definition Where it lives Why it matters
Configuration recorder The thing that captures resource state One per region per account Off → nothing is recorded; green-but-empty
Recording group Which resource types the recorder captures Recorder config Wrong scope → rules see nothing / bill balloons
Recording mode CONTINUOUS or DAILY per type Recorder config DAILY cuts CI cost on low-churn types
Delivery channel S3 (required) + SNS (optional) sink One per region per account Bad bucket policy → delivery FAILURE
Configuration item (CI) Point-in-time snapshot of one resource Delivered to S3; queryable The unit you pay for; the audit atom
Relationship Link between two resources Inside a CI Turns inventory into a graph
Config rule Function returning a compliance verdict Per region (managed/custom) The compliance judgement
Trigger Configuration-change or periodic Rule config Decides when a rule evaluates
Conformance pack Versioned bundle of rules + remediation Per account or org-wide Deploy a framework in one shot
Remediation configuration SSM runbook wired to a rule Per rule Turns NON_COMPLIANT into a fix
Aggregator Multi-account/region read-only roll-up Aggregator account One pane over the whole org
Compliance state COMPLIANT / NON_COMPLIANT / NOT_APPLICABLE / INSUFFICIENT_DATA Per resource per rule The verdict you act on

The configuration recorder and delivery channel

Everything starts with the recorder. If it is off, mis-scoped, or its role cannot read resources or write to S3, every downstream stage silently produces nothing — and nothing is indistinguishable from compliant on a dashboard. This is the single highest-leverage part of the whole service to get right.

The configuration recorder

You get exactly one configuration recorder per region per account (it is conventionally named default). It needs an IAM role it assumes to read your resources and to write to the delivery bucket — either the service-linked role AWSServiceRoleForConfig (the modern default) or a custom role carrying the AWS-managed AWS_ConfigRole policy. Here is every recorder setting, its default, and the trade-off:

Setting Values Default When to change Trade-off / gotcha
name string default Rarely Must be unique per region; one recorder only
roleARN IAM role ARN none (required) Always set SLR AWSServiceRoleForConfig preferred; custom role needs AWS_ConfigRole
recordingGroup.allSupported true/false true Narrow to specific types to cut cost With true you record every supported type — highest cost, highest coverage
recordingGroup.includeGlobalResourceTypes true/false false Enable in one region only true in many regions = duplicate IAM CIs = duplicate cost
recordingGroup.resourceTypes list of types empty Inclusion mode Only valid when allSupported=false
recordingGroup.exclusionByResourceTypes list of types empty Exclusion mode Record all-except a noisy type (e.g. AWS::Config::ResourceCompliance)
recordingGroup.recordingStrategy ALL_SUPPORTED / INCLUSION_BY_RESOURCE_TYPES / EXCLUSION_BY_RESOURCE_TYPES ALL_SUPPORTED Follows the above choice Set automatically by which field you use
recordingMode.recordingFrequency CONTINUOUS / DAILY CONTINUOUS DAILY for low-churn types DAILY = one CI/day max per resource; cheaper, less granular
recordingMode.recordingModeOverrides per-type frequency none Mixed strategy Record high-value types CONTINUOUS, the rest DAILY

Turn it on with the CLI (assuming the service-linked role exists — create it once with aws iam create-service-linked-role --aws-service-name config.amazonaws.com):

# 1) Create/replace the recorder — record everything, global types in this region only
aws configservice put-configuration-recorder --configuration-recorder '{
  "name": "default",
  "roleARN": "arn:aws:iam::111122223333:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig",
  "recordingGroup": { "allSupported": true, "includeGlobalResourceTypes": true },
  "recordingMode": { "recordingFrequency": "CONTINUOUS" }
}'

The Terraform equivalent is three coupled resources — recorder, delivery channel, and a status resource that starts recording (order matters; the status must depend on the channel):

resource "aws_config_configuration_recorder" "this" {
  name     = "default"
  role_arn = aws_iam_service_linked_role.config.arn

  recording_group {
    all_supported                 = true
    include_global_resource_types = true # ONLY in your home region
  }

  recording_mode {
    recording_frequency = "CONTINUOUS"
    recording_mode_override {
      description         = "Low-churn types daily"
      resource_types      = ["AWS::EC2::SecurityGroup"]
      recording_frequency = "DAILY"
    }
  }
}

Recording group: scope is a cost and coverage decision

allSupported=true is the safe default for a security account (you cannot be compliant about a resource you never recorded), but it is also the biggest cost lever. The three strategies:

Strategy Set it with Records Use when Risk
All supported allSupported=true Every supported type in the region Security/audit accounts; you want full coverage Highest CI volume and cost
Inclusion list resourceTypes=[...] Only the listed types You care about a known set (e.g. S3, IAM, EC2, RDS) You miss drift on unlisted types
Exclusion list exclusionByResourceTypes=[...] Everything except the listed types Record all but a noisy/low-value type New types appear automatically (good)

The global resource types flag is the classic footgun. Global resources — AWS::IAM::User, ::Group, ::Role, ::Policy — are not region-scoped, so if includeGlobalResourceTypes=true in five regions, one IAM change produces five CIs and you pay five times. Enable it in exactly one region (your home region), and leave it false everywhere else. Since 2022, AWS records supported global types only in the region where you opt in, but the duplication trap still bites anyone who set the flag broadly out of caution.

Recording mode: CONTINUOUS vs DAILY

CONTINUOUS recording emits a CI on every change (near real-time) — the default and what change-triggered rules need to react quickly. DAILY recording emits at most one CI per resource per day, capturing the latest state — dramatically cheaper for high-volume, low-value churn (think autoscaling security-group associations). You can mix: CONTINUOUS globally with per-type DAILY overrides, or vice-versa.

Aspect CONTINUOUS DAILY
CI frequency One per change At most one per day
Latency to a rule Seconds–minutes Up to ~24 h
Cost Higher (per change) Lower (per day)
Change-triggered rules Fully supported Fire on the daily CI only
Best for Security-critical, low-churn-but-important types High-churn, low-risk types
Gotcha Chatty resources get expensive Slow drift detection; not for tight SLAs

The delivery channel: S3 and SNS

The recorder does not store anything itself — it delivers CIs (and periodic configuration snapshots) to an S3 bucket and, optionally, notifications to an SNS topic. The bucket policy is the number-two failure after “recorder off.” Config writes as the service principal config.amazonaws.com, so the bucket must allow it.

Delivery setting Values Default Notes
s3BucketName bucket name required Can be in another account (central log archive)
s3KeyPrefix string none Optional path prefix inside the bucket
s3KmsKeyArn KMS CMK ARN none (SSE-S3) If set, grant the service kms:GenerateDataKey
snsTopicARN SNS topic ARN none Optional; drives notifications/EventBridge
configSnapshotDeliveryProperties.deliveryFrequency One_Hour / Three_Hours / Six_Hours / Twelve_Hours / TwentyFour_Hours none (no scheduled snapshot) Snapshot cadence; history is delivered on change regardless
# 2) Point the recorder at a bucket + SNS topic, snapshot every 6 hours
aws configservice put-delivery-channel --delivery-channel '{
  "name": "default",
  "s3BucketName": "acme-config-ap-south-1-111122223333",
  "snsTopicARN": "arn:aws:sns:ap-south-1:111122223333:config-notifications",
  "configSnapshotDeliveryProperties": { "deliveryFrequency": "Six_Hours" }
}'

# 3) START recording — nothing happens until you do this
aws configservice start-configuration-recorder --configuration-recorder-name default

The minimum S3 bucket policy Config needs (the exact statements that fix badge 2 delivery failures):

Sid Principal Action Resource Condition
AWSConfigBucketPermissionsCheck config.amazonaws.com s3:GetBucketAcl bucket aws:SourceAccount = your account
AWSConfigBucketExistenceCheck config.amazonaws.com s3:ListBucket bucket aws:SourceAccount
AWSConfigBucketDelivery config.amazonaws.com s3:PutObject .../AWSLogs/<acct>/Config/* s3:x-amz-acl=bucket-owner-full-control + aws:SourceAccount

Verify the whole plane is live before you trust a single rule:

# recording must be true and lastStatus SUCCESS
aws configservice describe-configuration-recorder-status \
  --query 'ConfigurationRecordersStatus[0].{recording:recording,last:lastStatus,err:lastErrorMessage}'
# delivery must not be FAILURE
aws configservice describe-delivery-channel-status \
  --query 'DeliveryChannelsStatus[0].configHistoryDeliveryInfo.lastStatus'

Resource inventory, relationships and the configuration timeline

Once recording, Config becomes a live inventory and a time machine. Every resource has a stream of CIs; every CI carries the resource’s attributes, its relationships, and pointers to the CloudTrail events that changed it.

Anatomy of a configuration item

CI field What it holds Why you care
configurationItemCaptureTime When Config recorded this state The timeline axis
configurationItemStatus OK / ResourceDiscovered / ResourceModified / ResourceDeleted Lifecycle of the resource
resourceType / resourceId / arn Identity of the resource Join key across services
configuration The resource’s actual attributes (JSON) What Guard rules evaluate
supplementaryConfiguration Extra state (e.g. S3 encryption, bucket policy) Where many compliance signals live
relationships Linked resources + relationship name Turns inventory into a graph
tags Resource tags at capture time required-tags, cost allocation
relatedEvents CloudTrail event IDs “Who changed it” (via CloudTrail)
configurationStateId Monotonic state marker Ordering CIs deterministically

Relationships: inventory as a graph

A CI does not just describe a resource in isolation — it records how it connects to others. This is what lets you answer “which security groups is this instance in?” or “what subnets does this ENI touch?” without walking the API yourself.

Example resource Relationship Related resource
AWS::EC2::Instance is contained in AWS::EC2::Subnet
AWS::EC2::Instance is associated with AWS::EC2::SecurityGroup
AWS::EC2::Instance is attached to AWS::EC2::Volume
AWS::EC2::Subnet is contained in AWS::EC2::VPC
AWS::EC2::NetworkInterface is associated with AWS::EC2::SecurityGroup
AWS::S3::Bucket (standalone, policy in supplementary)
AWS::IAM::Role is attached to AWS::IAM::Policy

Advanced queries: SQL over your estate

Config exposes a SQL-like advanced query language over the current configuration state, in one account (select-resource-config) or across an aggregator (select-aggregate-resource-config). It is the fastest way to answer inventory questions.

# Every S3 bucket that is NOT using default encryption, this account
aws configservice select-resource-config --expression \
  "SELECT resourceId, resourceName WHERE resourceType = 'AWS::S3::Bucket' \
   AND supplementaryConfiguration.ServerSideEncryptionConfiguration = 'null'"

# Across the whole org aggregator: instance count by type and account
aws configservice select-aggregate-resource-config \
  --configuration-aggregator-name org-agg --expression \
  "SELECT accountId, configuration.instanceType, COUNT(*) \
   WHERE resourceType = 'AWS::EC2::Instance' \
   GROUP BY accountId, configuration.instanceType"
Query capability Supported? Notes
SELECT specific fields Yes Dot-path into configuration / tags
WHERE filters Yes =, IN, LIKE, BETWEEN, AND/OR
GROUP BY / COUNT Yes Aggregations for inventory reports
ORDER BY / LIMIT Yes Paginate large result sets
JOIN across resources No Use relationships in the CI instead
Historical point-in-time No (current state only) Use the timeline / S3 history for history
Across accounts+regions Yes (aggregate query) Requires an aggregator

The configuration timeline in the console shows every CI for a resource with a diff between states and the CloudTrail events that caused each change — the single best “what happened to this resource and when” view during an incident. For programmatic history, get-resource-config-history returns the CI stream for one resource.

Config rules: managed, custom Lambda, and custom Guard

A rule turns recorded state into a compliance verdict. There are three kinds, and choosing correctly is most of the skill.

Managed vs custom Lambda vs custom Guard

Rule type Who authors the logic Backing compute Best for Effort
AWS-managed AWS (400+ prebuilt) None (AWS runs it) Common checks (encryption, public access, tags) Lowest — set parameters and go
Custom Lambda You (any language) Your Lambda function Complex logic, external lookups, cross-resource checks Highest — you run and patch code
Custom Guard (policy) You (Guard DSL) None (AWS runs the policy) Declarative property checks on a CI Low — a policy file, no Lambda

Custom Guard (policy) rules are the sweet spot for most custom needs added since 2022: you write a CloudFormation Guard (guard-2.0.0) policy that evaluates the CI JSON, and AWS runs it — no Lambda to deploy, permission, or patch. Reach for custom Lambda only when you need imperative logic, external API calls, or state a policy language cannot express.

Triggers: configuration-change vs periodic

Trigger type Fires when Latency Use for Note
Configuration changes A CI for an in-scope resource is recorded Seconds–minutes Per-resource property checks (encryption, public access) Needs the type recorded; DAILY mode delays it
Periodic On a fixed schedule (1/3/6/12/24 h) Up to the interval Account-wide checks with no single resource (e.g. iam-password-policy, cloudtrail-enabled) Runs even with no change
Both Either of the above Mixed Rules that want change reactivity and a safety-net sweep Most AWS-managed rules pick this for you

Compliance states — what each verdict means

State Meaning Common cause Action
COMPLIANT In-scope resource passes Correct configuration None
NON_COMPLIANT In-scope resource fails The violation the rule checks for Remediate
NOT_APPLICABLE Rule does not apply to this resource Out of scope / wrong type None (expected)
INSUFFICIENT_DATA Rule could not evaluate Recorder off, rule just created, Lambda error Investigate — often “recorder not recording”

INSUFFICIENT_DATA is the one that trips people: it usually means the recorder is not capturing the type the rule needs, or the rule was just created and has not yet had a CI to chew on. It is not the same as compliant.

The staple managed rules

These are the rules you deploy on day one. Know their source identifier (what you pass to the API), their trigger, and their key parameters:

Rule name Source identifier Trigger Key parameters What it catches
required-tags REQUIRED_TAGS Change tag1Keytag6Key (+ optional values) Resources missing mandated tags
s3-bucket-public-read-prohibited S3_BUCKET_PUBLIC_READ_PROHIBITED Change + periodic none Buckets readable by the public
s3-bucket-public-write-prohibited S3_BUCKET_PUBLIC_WRITE_PROHIBITED Change + periodic none Buckets writable by the public
s3-bucket-server-side-encryption-enabled S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED Change none Buckets without default encryption
restricted-ssh INCOMING_SSH_DISABLED Change none Security groups open to 0.0.0.0/0:22
restricted-common-ports RESTRICTED_INCOMING_TRAFFIC Change blockedPort1blockedPort5 SGs exposing sensitive ports
encrypted-volumes ENCRYPTED_VOLUMES Change optional kmsId Unencrypted attached EBS volumes
rds-storage-encrypted RDS_STORAGE_ENCRYPTED Change optional kmsKeyId RDS instances without storage encryption
iam-password-policy IAM_PASSWORD_POLICY Periodic length/complexity params Weak account password policy
root-account-mfa-enabled ROOT_ACCOUNT_MFA_ENABLED Periodic none Root user without MFA
cloudtrail-enabled CLOUD_TRAIL_ENABLED Periodic optional trail params No CloudTrail in the account
ec2-instance-no-public-ip EC2_INSTANCE_NO_PUBLIC_IP Change none Instances with a public IP
access-keys-rotated ACCESS_KEYS_ROTATED Periodic maxAccessKeyAge Stale IAM access keys
vpc-flow-logs-enabled VPC_FLOW_LOGS_ENABLED Periodic trafficType VPCs without flow logs

Deploy a managed rule with the CLI — required-tags demanding Environment and Owner:

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "required-tags",
  "Source": { "Owner": "AWS", "SourceIdentifier": "REQUIRED_TAGS" },
  "InputParameters": "{\"tag1Key\":\"Environment\",\"tag2Key\":\"Owner\"}",
  "Scope": { "ComplianceResourceTypes": ["AWS::S3::Bucket","AWS::EC2::Instance"] }
}'
resource "aws_config_config_rule" "required_tags" {
  name = "required-tags"
  source {
    owner             = "AWS"
    source_identifier = "REQUIRED_TAGS"
  }
  input_parameters = jsonencode({ tag1Key = "Environment", tag2Key = "Owner" })
  scope { compliance_resource_types = ["AWS::S3::Bucket", "AWS::EC2::Instance"] }
  depends_on = [aws_config_configuration_recorder.this]
}

A custom Guard (policy) rule, end to end

A Guard rule evaluates the CI directly. This one flags any S3 bucket without server-side encryption configured — no Lambda anywhere:

# s3_sse.guard — evaluate against each AWS::S3::Bucket configuration item
rule s3_default_encryption when resourceType == "AWS::S3::Bucket" {
    supplementaryConfiguration.ServerSideEncryptionConfiguration exists
    supplementaryConfiguration.ServerSideEncryptionConfiguration.rules[*].applyServerSideEncryptionByDefault.sseAlgorithm in ["aws:kms","AES256"]
}
resource "aws_config_config_rule" "s3_sse_guard" {
  name = "s3-default-encryption-guard"
  source {
    owner = "CUSTOM_POLICY"
    source_detail {
      message_type = "ConfigurationItemChangeNotification"
    }
    custom_policy_details {
      policy_runtime               = "guard-2.0.0"
      policy_text                  = file("${path.module}/s3_sse.guard")
      enable_debug_log_delivery    = true
    }
  }
  scope { compliance_resource_types = ["AWS::S3::Bucket"] }
  depends_on = [aws_config_configuration_recorder.this]
}

Custom Lambda rule contract

If you do need a Lambda rule, the function receives the invoking event, evaluates it, and reports back with PutEvaluations. Two non-obvious requirements bite everyone: the function needs a resource-based policy allowing config.amazonaws.com to invoke it, and it must call PutEvaluations or the rule sits in EVALUATING forever.

Requirement Why How
lambda:InvokeFunction permission for config.amazonaws.com Config invokes your function aws lambda add-permission --principal config.amazonaws.com --action lambda:InvokeFunction
Function role has config:PutEvaluations Report the verdict back Attach to the function’s execution role
Handle both change and periodic events Rule may trigger either way Branch on messageType in the event
Return within the timeout Long evals time out to INSUFFICIENT_DATA Keep it fast; paginate carefully

Rule limits

Limit Default value Adjustable?
Config rules per region per account 150 Yes (Service Quotas)
Conformance packs per account 50 Some regions/quotas
Rules per conformance pack up to ~130 No
Aggregators per account 50 Yes
Aggregator source accounts large (org-based has no per-account cap)
Tags per Config rule 50 No

Conformance packs

Deploying rules one at a time does not scale to a framework with fifty controls across a hundred accounts. A conformance pack is an immutable, versioned bundle of Config rules and their remediation actions in a single YAML template, deployed as one unit — and deployable org-wide from a delegated-admin account so every account gets the same controls automatically.

Anatomy and sample packs

AWS ships sample conformance pack templates mapped to real frameworks; you rarely start from scratch:

Sample pack (Operational Best Practices for…) Maps to Typical rule count
PCI-DSS Payment card dozens
CIS AWS Foundations Benchmark CIS dozens
HIPAA Security Healthcare dozens
NIST 800-53 US federal 100+
FedRAMP (Moderate/High) US federal 100+
ISO 27001 Infosec management dozens
AWS Well-Architected Security WAF dozens
SOC 2 Trust services dozens
Pack element Purpose Notes
Parameters Reusable inputs (e.g. required tag keys) Passed as ConformancePackInputParameters
Resources: AWS::Config::ConfigRule The rules themselves Managed or custom
Resources: AWS::Config::RemediationConfiguration Optional auto-fix per rule Ships remediation with the rule
Delivery S3 bucket awsconfigconforms-* bucket Auto-created or specified

Deploy a pack from a template and go org-wide:

# Single account
aws configservice put-conformance-pack \
  --conformance-pack-name pci-dss-baseline \
  --template-s3-uri s3://acme-conformance-templates/pci-dss.yaml \
  --conformance-pack-input-parameters ParameterName=TagKey,ParameterValue=Environment

# Whole organization (run in the delegated-admin/management account)
aws configservice put-organization-conformance-pack \
  --organization-conformance-pack-name pci-dss-baseline \
  --template-s3-uri s3://acme-conformance-templates/pci-dss.yaml
resource "aws_config_organization_conformance_pack" "pci" {
  name            = "pci-dss-baseline"
  template_s3_uri = "s3://acme-conformance-templates/pci-dss.yaml"
  input_parameter {
    parameter_name  = "TagKey"
    parameter_value = "Environment"
  }
  depends_on = [aws_config_configuration_recorder.this]
}

Pack vs individual rules

Dimension Individual rules Conformance pack
Unit of deployment One rule A whole framework
Org-wide rollout Per rule (put-organization-config-rule) One pack, all accounts
Remediation bundled Wire separately Included in the template
Versioning / drift Manual Pack is immutable; redeploy to change
Reporting Per rule Pack-level compliance score
Best for A handful of ad-hoc checks Auditable framework baselines

Remediation: manual and automatic via SSM Automation

A NON_COMPLIANT verdict is inert until you attach remediation. Config remediation runs an SSM Automation document (runbook) against the offending resource, either manually (you trigger it) or automatically (Automatic=true, with retries). AWS ships a large family of purpose-built runbooks named AWSConfigRemediation-*, plus general AWS-* automation docs.

Manual vs automatic

Mode When it runs Set with Use for Risk
Manual You call start-remediation-execution Automatic=false High-blast-radius fixes; review-first Nothing fixes itself
Automatic On NON_COMPLIANT, with retries Automatic=true, MaximumAutomaticAttempts, RetryAttemptSeconds High-frequency, low-risk, idempotent fixes Loops / fights a deploy pipeline if not scoped

Common remediation runbooks

SSM document Fixes Pairs with rule
AWS-DisableS3BucketPublicReadWrite Blocks public access on a bucket s3-bucket-public-read-prohibited
AWSConfigRemediation-EnableS3BucketEncryption Turns on default SSE s3-bucket-server-side-encryption-enabled
AWS-EnableS3BucketEncryption Sets bucket encryption encryption rules
AWSConfigRemediation-EnableEbsEncryptionByDefault Default EBS encryption in a region ec2-ebs-encryption-by-default
AWSConfigRemediation-RemoveUnusedIAMUserCredentials Deactivates stale credentials iam-user-unused-credentials-check
AWSConfigRemediation-EnableCloudTrailEncryption KMS-encrypts a trail cloud-trail-encryption-enabled
AWS-ConfigureS3BucketLogging Enables access logging logging rules
AWSConfigRemediation-RevokeUnusedIAMUserPermissions Strips unused permissions least-privilege rules

The remediation configuration

The runbook executes under an AutomationAssumeRole you pass as a parameter — a role SSM assumes to perform the fix. It must trust ssm.amazonaws.com and carry least-privilege permissions for exactly that fix. You also map the resource under evaluation into the runbook, usually via the special RESOURCE_ID value.

Field Purpose Gotcha
ConfigRuleName The rule this remediates Must already exist
TargetType SSM_DOCUMENT Only SSM Automation is supported
TargetId / TargetVersion Runbook name + version Pin the version for stability
Parameters Map runbook inputs One must resolve the resource (ResourceValue: RESOURCE_ID)
Parameters.AutomationAssumeRole Role SSM assumes Wrong trust/permissions → nothing fires (badge 4)
Automatic Auto-run on NON_COMPLIANT Default false
MaximumAutomaticAttempts Retry count (1–25) With RetryAttemptSeconds
RetryAttemptSeconds Backoff between attempts 1–2678000
ExecutionControls.SsmControls.ConcurrentExecutionRatePercentage Throttle blast radius Cap concurrent fixes
aws configservice put-remediation-configurations --remediation-configurations '[{
  "ConfigRuleName": "s3-bucket-public-read-prohibited",
  "TargetType": "SSM_DOCUMENT",
  "TargetId": "AWS-DisableS3BucketPublicReadWrite",
  "Automatic": true,
  "MaximumAutomaticAttempts": 5,
  "RetryAttemptSeconds": 60,
  "Parameters": {
    "AutomationAssumeRole": { "StaticValue": { "Values": ["arn:aws:iam::111122223333:role/config-remediation"] } },
    "S3BucketName": { "ResourceValue": { "Value": "RESOURCE_ID" } }
  }
}]'
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-DisableS3BucketPublicReadWrite"
  automatic        = true
  maximum_automatic_attempts = 5
  retry_attempt_seconds      = 60

  parameter {
    name         = "AutomationAssumeRole"
    static_value = aws_iam_role.remediation.arn
  }
  parameter {
    name           = "S3BucketName"
    resource_value = "RESOURCE_ID"
  }
}

Two hard-won rules: make the runbook idempotent (running it twice must be safe), and give the rule an exception tag the logic honours (e.g. compliance-exception=true) so a legitimately-public static-site bucket does not get fought by an auto-remediation on every deploy — the classic blast-radius loop.

Aggregators: multi-account, multi-region

A configuration aggregator collects CIs and compliance from many accounts and regions into one read-only pane. There are two source types, and one is dramatically less painful than the other.

Aggregator type Source Authorization needed Scales to Use when
Account-based Explicit list of account IDs Each source account must run put-aggregation-authorization for the aggregator account+region Tens of accounts No Organizations, or a curated subset
Organization-based The whole AWS Organization None per account (uses org trust) The entire org, automatically You use Organizations (almost always)

For anything beyond a couple of accounts, use the organization aggregator: register a delegated administrator for Config, and the aggregator sees every account with no per-account authorization dance. Account-based aggregators require the reverse handshake — the source account authorizes the aggregator — which is exactly what fails as badge 5 when someone forgets a region.

# Org aggregator (run in the delegated-admin account) — all regions
aws configservice put-configuration-aggregator \
  --configuration-aggregator-name org-agg \
  --organization-aggregation-source '{
    "RoleArn": "arn:aws:iam::444455556666:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig",
    "AllAwsRegions": true }'

# Account-based: the SOURCE account must authorize the aggregator account+region
aws configservice put-aggregation-authorization \
  --authorized-account-id 444455556666 --authorized-aws-region ap-south-1
resource "aws_config_configuration_aggregator" "org" {
  name = "org-agg"
  organization_aggregation_source {
    all_regions = true
    role_arn    = aws_iam_role.config_org_agg.arn
  }
}
Aggregator source status Meaning Fix
SUCCEEDED Data flowing None
OUTDATED Aggregation lagging Usually transient; recheck
FAILED Cannot read the source Missing authorization (account-based) or SLR (org-based)

Architecture at a glance

The diagram traces the continuous-compliance loop left to right, from a resource changing to the drift being fixed and rolled up org-wide. In the recorded estate, EC2/EBS, S3 and IAM (global) resources are what Config watches. Every change is captured by the single configuration recorder as a configuration item; the recorder ships CIs to a delivery-channel S3 bucket (SSE-KMS) and notifies an SNS topic. In the rules-evaluate zone, AWS-managed rules (required-tags, restricted-ssh) and a custom Guard/Lambda rule evaluate each CI on change or on a schedule and stamp it COMPLIANT or NON_COMPLIANT. A NON_COMPLIANT verdict flows into remediate, where an SSM Automation runbook fixes the resource — manually or Automatic=true — and the fix re-records a new CI, closing the loop. In parallel, the org aggregator zone shows Organizations (delegated admin) running a configuration aggregator across all accounts and regions, with conformance packs attaching packaged rules and remediation.

The six numbered badges mark where the loop silently breaks: the recorder off or its role unable to read (badge 1) so everything downstream sees nothing; the S3 delivery channel blocked by a bucket policy (badge 2) so no history lands; a custom rule stuck EVALUATING because a Lambda lacks the invoke permission or the Guard scope misses the type (badge 3); auto-remediation that never fires because the AutomationAssumeRole cannot be assumed (badge 4); an aggregator with pending authorization (badge 5) leaving an account blank; and global-resource duplication plus per-CI/per-evaluation cost (badge 6). The legend narrates each as symptom, the exact confirm command, and the fix.

AWS Config continuous-compliance loop: a recorded estate of EC2/EBS, S3 and global IAM resources captured by a single configuration recorder that delivers configuration items to an SSE-KMS S3 delivery-channel bucket and an SNS topic; managed rules (required-tags, restricted-ssh) and a custom Guard/Lambda rule evaluate each configuration item on change or on a schedule and mark it COMPLIANT or NON_COMPLIANT; a NON_COMPLIANT verdict drives SSM Automation remediation (manual or Automatic=true) that re-records state to close the loop; and an Organizations delegated-admin account runs a configuration aggregator across all accounts and regions with conformance packs attaching packaged rules and remediation — with six numbered failure badges marking a recorder that is off or lacks read permission, a blocked S3 delivery channel, a rule stuck EVALUATING, auto-remediation that never fires, aggregator authorization pending, and global-resource duplication with per-item and per-evaluation cost

Real-world scenario

Novantic Health, a fictional but realistic health-tech company, runs a 28-account AWS organization in ap-south-1 and us-east-1, regulated under HIPAA and pursuing SOC 2 Type II. The platform team is five engineers. The monthly Config bill — CIs, rule evaluations and conformance-pack evaluations — sits around ₹78,000, and the CFO questions it every quarter until the audit makes the case for him.

The crisis surfaced three weeks before the SOC 2 window closed. The auditor asked Novantic to prove that no S3 bucket holding PHI had been publicly readable at any point in the audit period, and that any exception had been detected and closed within the stated SLA. The security lead opened the Config aggregator, filtered s3-bucket-public-read-prohibited, and the dashboard was reassuringly green — until someone noticed the resource count for account #19 was zero. That account, onboarded four months earlier during a hiring crunch, had a Config recorder that had been created but never started. Every rule for that account had been returning INSUFFICIENT_DATA, which the roll-up rendered as “no non-compliant resources” — green. They had a four-month blind spot in a PHI account. This is badge 1 exactly: describe-configuration-recorder-status returned recording: false.

The fix became the template. First, they repaired the recording plane everywhere: a Terraform module that created the recorder with allSupported=true, includeGlobalResourceTypes=true only in ap-south-1, a delivery channel to a central log-archive bucket, and — critically — the aws_config_configuration_recorder_status resource that actually starts it, deployed to all 28 accounts through their pipeline. Within twenty minutes, account #19 lit up with seven publicly-readable buckets that had been invisible for a third of a year. Second, they replaced their scattered individual rules with a single conformance pack built from the Operational Best Practices for HIPAA Security sample template, deployed org-wide with put-organization-conformance-pack from their delegated-admin account, so a future new account could not be onboarded without the controls.

Third — carefully — they added remediation. For s3-bucket-public-read-prohibited they wired automatic remediation to AWS-DisableS3BucketPublicReadWrite, but only after two safeguards: an idempotent runbook, and a compliance-exception=true tag the pack honoured for the two genuinely-public buckets serving a patient-facing static site. They had learned badge 4’s lesson the hard way a year earlier — an auto-remediation whose AutomationAssumeRole lacked s3:PutBucketPublicAccessBlock had silently failed for weeks, leaving buckets NON_COMPLIANT with no execution ever recorded, until someone finally ran describe-remediation-execution-status and saw the AccessDenied.

Fourth, they attacked cost. The bill had crept up because three engineers had, at various times, set includeGlobalResourceTypes=true in both regions “to be safe,” doubling every IAM CI (badge 6). They pinned global recording to one region and switched a batch of high-churn, low-risk types (security-group associations churned by autoscaling) to DAILY recording, cutting CI volume by roughly a third with no loss of meaningful coverage.

At the audit, the security lead answered the public-bucket question in ninety seconds: an aggregate advanced query listing every AWS::S3::Bucket with its public-access status across all accounts, and a Config compliance timeline showing account #19’s seven buckets going NON_COMPLIANT and then COMPLIANT after remediation, with timestamps inside the SLA. Novantic passed with zero findings on monitoring. The line that went on the wall: “Green isn’t compliant — empty is also green. Prove the recorder is started in every account before you trust one dashboard.” The order is the lesson:

Phase Finding Action Effect
Pre-audit Aggregator green, but account #19 count = 0 describe-configuration-recorder-status = false Found the blind spot
Day 1 Recorder created, never started Deploy recorder status resource org-wide Recording began everywhere
Day 1 7 public PHI buckets appear HIPAA conformance pack org-wide Controls enforced by default
Week 1 Some buckets stay NON_COMPLIANT describe-remediation-execution-status → AccessDenied Fixed AutomationAssumeRole
Week 2 Bill too high Global types → one region; low-risk → DAILY ~⅓ CI reduction

Advantages and disadvantages

Advantages Disadvantages
Continuous drift detection, not point-in-time Detective only — never blocks the bad action
Timestamped configuration history for audits Per-CI + per-evaluation cost adds up fast
Resource relationships turn inventory into a graph Change-triggered rules lag under DAILY recording
400+ managed rules; custom Guard needs no Lambda Custom Lambda rules are code you must run/patch
Conformance packs deploy a framework in one shot Packs are immutable — a change means a redeploy
Org aggregator = one pane over the whole estate Aggregator is read-only; no central remediation
Native SSM remediation closes the loop Auto-remediation blast radius if unscoped
Feeds Security Hub, Control Tower, audit tooling “Empty looks compliant” — silent blind spots

Config is worth its cost the first time an auditor asks a “prove it stayed correct” question — but only if you have paid the discipline tax up front: recorder started in every account, scope tuned so you are not paying to record noise, rules scoped so INSUFFICIENT_DATA is rare, and remediation scoped so it fixes without fighting. Skip that discipline and you get a large bill and a green dashboard that proves nothing.

Hands-on lab

You will turn on Config, add one managed rule and one custom Guard rule, wire automatic remediation, make a resource go NON_COMPLIANT, and watch it flip back — via Console notes, aws configservice, and Terraform, then tear it all down. Region ap-south-1, account 111122223333. ⚠️ Config is not free — you pay per configuration item and per rule evaluation; the teardown at the end stops the meter.

Step 0 — Prerequisites

aws sts get-caller-identity            # confirm account + region
export AWS_DEFAULT_REGION=ap-south-1
export ACCT=$(aws sts get-caller-identity --query Account --output text)

Step 1 — Create the service-linked role and a delivery bucket

# Service-linked role Config uses to read resources + write S3 (idempotent)
aws iam create-service-linked-role --aws-service-name config.amazonaws.com 2>/dev/null || true

# Delivery bucket (block public access, default encryption)
aws s3api create-bucket --bucket acme-config-$ACCT --region ap-south-1 \
  --create-bucket-configuration LocationConstraint=ap-south-1
aws s3api put-public-access-block --bucket acme-config-$ACCT \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Attach the bucket policy Config needs (write it to bucket-policy.json with the three statements from the delivery-channel table, then):

aws s3api put-bucket-policy --bucket acme-config-$ACCT --policy file://bucket-policy.json

Console path: Config → Get started walks you through recorder + delivery bucket + role in one wizard; the CLI below is the explicit form.

Step 2 — Turn on the recorder and delivery channel

SLR="arn:aws:iam::$ACCT:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig"

aws configservice put-configuration-recorder --configuration-recorder \
  "{\"name\":\"default\",\"roleARN\":\"$SLR\",\"recordingGroup\":{\"allSupported\":true,\"includeGlobalResourceTypes\":true}}"

aws configservice put-delivery-channel --delivery-channel \
  "{\"name\":\"default\",\"s3BucketName\":\"acme-config-$ACCT\"}"

aws configservice start-configuration-recorder --configuration-recorder-name default

Verify (expected output):

aws configservice describe-configuration-recorder-status \
  --query 'ConfigurationRecordersStatus[0].{recording:recording,last:lastStatus}'
# {
#     "recording": true,
#     "last": "SUCCESS"
# }

If recording is false or last is FAILURE, jump to the troubleshooting playbook — do not proceed until this is SUCCESS.

Step 3 — Add the managed rule

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "s3-bucket-public-read-prohibited",
  "Source": { "Owner": "AWS", "SourceIdentifier": "S3_BUCKET_PUBLIC_READ_PROHIBITED" },
  "Scope": { "ComplianceResourceTypes": ["AWS::S3::Bucket"] }
}'

Step 4 — Add the custom Guard rule

Save the s3_sse.guard policy from earlier, then create a CUSTOM_POLICY rule (CLI takes the policy inline via the rule JSON, or use the Terraform in Step 7). Confirm both rules exist:

aws configservice describe-config-rules --query 'ConfigRules[].ConfigRuleName'
# [ "s3-bucket-public-read-prohibited", "s3-default-encryption-guard" ]

Step 5 — Create a deliberately non-compliant resource

# A test bucket, then make it public-read (this is what the rule should catch)
aws s3api create-bucket --bucket acme-badbucket-$ACCT --region ap-south-1 \
  --create-bucket-configuration LocationConstraint=ap-south-1
aws s3api delete-public-access-block --bucket acme-badbucket-$ACCT
aws s3api put-bucket-acl --bucket acme-badbucket-$ACCT --acl public-read

Force an evaluation instead of waiting, then check compliance:

aws configservice start-config-rules-evaluation --config-rule-names s3-bucket-public-read-prohibited
sleep 30
aws configservice get-compliance-details-by-config-rule \
  --config-rule-name s3-bucket-public-read-prohibited \
  --query 'EvaluationResults[].{r:EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId,c:ComplianceType}'
# [ { "r": "acme-badbucket-111122223333", "c": "NON_COMPLIANT" } ]

Step 6 — Wire automatic remediation and watch it flip

Create the remediation role (trusts ssm.amazonaws.com, least-privilege for the fix), then the remediation configuration from the CLI block earlier. After it runs:

aws configservice describe-remediation-execution-status \
  --config-rule-name s3-bucket-public-read-prohibited \
  --query 'RemediationExecutionStatuses[].{r:ResourceKey.ResourceId,s:State}'
# [ { "r": "acme-badbucket-111122223333", "s": "SUCCEEDED" } ]

# Re-evaluate — the bucket should now be COMPLIANT
aws configservice start-config-rules-evaluation --config-rule-names s3-bucket-public-read-prohibited
sleep 30
aws configservice get-compliance-details-by-config-rule \
  --config-rule-name s3-bucket-public-read-prohibited \
  --query 'EvaluationResults[0].ComplianceType'
# "COMPLIANT"

You have watched the full loop: record → evaluate → NON_COMPLIANT → auto-remediate → re-record → COMPLIANT.

Step 7 — The whole lab as Terraform

The recorder, channel, status, both rules and the remediation in one file (abbreviated — combine the blocks shown earlier plus the status resource):

resource "aws_config_configuration_recorder_status" "this" {
  name       = aws_config_configuration_recorder.this.name
  is_enabled = true
  depends_on = [aws_config_delivery_channel.this]
}

Step 8 — Teardown (stops the billing)

# Action Command
1 Delete remediation config aws configservice delete-remediation-configuration --config-rule-name s3-bucket-public-read-prohibited --resource-type AWS::S3::Bucket
2 Delete both rules aws configservice delete-config-rule --config-rule-name <name> (each)
3 Stop the recorder aws configservice stop-configuration-recorder --configuration-recorder-name default
4 Delete delivery channel aws configservice delete-delivery-channel --delivery-channel-name default
5 Delete the recorder aws configservice delete-configuration-recorder --configuration-recorder-name default
6 Empty + delete buckets aws s3 rb s3://acme-badbucket-$ACCT --force and aws s3 rb s3://acme-config-$ACCT --force
7 (Optional) remove remediation role aws iam delete-role --role-name config-remediation (detach policies first)

⚠️ Stopping the recorder is what halts per-CI charges; deleting rules halts per-evaluation charges. Leaving Config “on but idle” still bills for any CIs recorded.

Common mistakes & troubleshooting

This is the section you open mid-incident. The playbook first — symptom to fix — then the API-error reference, a decision table, and prose on the nastiest failures.

# Symptom Root cause Confirm (exact command / console path) Fix
1 Dashboard green but a resource count is 0 Recorder created but never started aws configservice describe-configuration-recorder-statusrecording:false start-configuration-recorder; deploy the status/start step in IaC
2 Recorder recording, but no history in S3 Delivery bucket policy denies config.amazonaws.com describe-delivery-channel-statuslastStatus:FAILURE, AccessDenied Add the three bucket-policy statements with aws:SourceAccount
3 Delivery FAILURE with an SSE-KMS bucket CMK key policy blocks the service KMS CloudTrail Decrypt/GenerateDataKey deny Grant config.amazonaws.com kms:GenerateDataKey on the CMK
4 Rule stuck in EVALUATING forever Lambda rule never calls PutEvaluations / no invoke permission Lambda logs; get-function-policy missing config.amazonaws.com Add lambda:add-permission for Config; ensure code calls PutEvaluations
5 Rule shows INSUFFICIENT_DATA Recorder does not capture the rule’s resource type describe-configuration-recorder recordingGroup lacks the type Widen recording group; wait for a CI
6 Custom Guard rule never flags anything Scope resource type not recorded, or Guard path wrong Rule scope vs recordingGroup; Guard debug log in S3 Record the type; fix the supplementaryConfiguration path
7 Auto-remediation never runs AutomationAssumeRole not assumable / missing param describe-remediation-execution-status → AccessDenied/param error Fix role trust (ssm.amazonaws.com) + permissions; map RESOURCE_ID
8 Remediation loops on the same resource A pipeline keeps re-creating the violation CloudTrail shows repeated fix + re-break Add compliance-exception tag the rule honours; fix the pipeline
9 Aggregator shows an account as blank Authorization missing (account-based) Aggregator source status FAILED put-aggregation-authorization in the source account+region
10 Org aggregator empty for new accounts Delegated admin / SLR not set describe-configuration-aggregators; org access Register Config delegated admin; use AllAwsRegions
11 IAM CIs duplicated, bill jumps includeGlobalResourceTypes=true in many regions Count IAM CIs per region; recordingGroup per region Enable global types in one region only
12 Config bill much higher than expected Recording everything, everywhere, CONTINUOUS Cost Explorer by usage type; CI count DAILY for low-risk types; scope recording; scope rules
13 put-config-rule fails NoAvailableConfigurationRecorder Rule created before the recorder exists describe-configuration-recorder empty Create + start recorder first; depends_on in TF
14 Deleting a rule fails Remediation still attached describe-remediation-configurations Delete remediation config first, then the rule

API / operation error reference

Error / status Where it appears Meaning Fix
NoAvailableConfigurationRecorderException put-config-rule, put-remediation-configurations No recorder exists yet Create + start the recorder first
MaxNumberOfConfigRulesExceededException put-config-rule Over the 150-rule limit Request a quota increase; consolidate into packs
InsufficientDeliveryPolicyException put-delivery-channel Bucket policy insufficient Add Config service statements to the bucket policy
ValidationException on remediation put-remediation-configurations Parameter/type mismatch Match runbook params; set ResourceValue: RESOURCE_ID
InsufficientPermissionsException remediation execution Assume-role cannot perform the fix Add least-privilege perms to the runbook role
OrganizationAccessDeniedException org rule/pack/aggregator Org access or delegated admin missing Enable trusted access; register delegated admin
NoSuchConfigurationRecorderException start/stop recorder Wrong recorder name Use the actual name (default)
LastDeliveryChannelDeleteFailedException delete-delivery-channel Recorder still references the channel Stop recorder before deleting the channel

Decision table — read the symptom, name the cause

If you see… It’s probably… Do this
Everything COMPLIANT and counts are 0 Recorder off / never started describe-configuration-recorder-status
Only new accounts are blind Recorder start step not in the account template Add the start/status step to IaC and redeploy
A rule stuck EVALUATING Lambda not reporting or not permitted Check PutEvaluations + Lambda invoke permission
INSUFFICIENT_DATA on a rule Type not recorded / rule brand new Widen recording group; wait one cycle
NON_COMPLIANT that never self-fixes No remediation, or role can’t assume Wire remediation; fix AutomationAssumeRole
A fix that keeps re-breaking Deploy pipeline fights the remediation Exception tag + fix the source
The bill spiking Over-recording + duplicated globals Scope + DAILY + one-region globals

The three nastiest failures, in prose

“Green is empty.” The most dangerous Config failure is not a red dashboard — it is a green one over an account whose recorder never started. Every rule returns INSUFFICIENT_DATA, roll-ups render that as “no non-compliant resources,” and you have a blind spot that looks like health. The only defence is to treat resource count = 0 as an alarm, not a comfort, and to make the recorder start step (start-configuration-recorder / aws_config_configuration_recorder_status) a non-optional part of every account’s baseline. A recorder that exists but is stopped is the single most common real-world Config gap.

Auto-remediation that silently no-ops. You wire Automatic=true, feel safe, and weeks later discover buckets sitting NON_COMPLIANT with zero remediation executions — because the AutomationAssumeRole either cannot be assumed by SSM (wrong trust policy) or lacks the specific permission the runbook needs (e.g. s3:PutBucketPublicAccessBlock). Config does not shout about this; the evidence is in describe-remediation-execution-status showing AccessDenied or nothing at all. Always test a new remediation against a deliberately-broken resource in a sandbox and confirm you see SUCCEEDED before you trust it in production.

Global-resource duplication. Because IAM is global, includeGlobalResourceTypes=true in several regions records the same IAM change once per region — multiplying both your CI count and your bill for no added coverage. It is insidious because each engineer who set it “to be safe” was individually reasonable. Enable it in exactly one home region, verify with a per-region CI count, and encode the rule in your recorder module so nobody re-introduces it.

Best practices

# Practice Why
1 Record in every account and region; treat count = 0 as an alarm Empty renders as green — the classic blind spot
2 Make start-configuration-recorder a non-optional part of the account baseline A stopped recorder is the #1 real-world gap
3 Enable includeGlobalResourceTypes in one region only Avoids duplicate IAM CIs and duplicate cost
4 Deliver to a central, immutable log-archive bucket in a security account Tamper-proof evidence; survives account compromise
5 Prefer conformance packs over scattered rules for frameworks Auditable, versioned, org-wide in one shot
6 Prefer custom Guard over custom Lambda when a policy expresses it No Lambda to run, permission, or patch
7 Scope rules to the resource types that matter Fewer INSUFFICIENT_DATA, lower evaluation cost
8 Make every remediation runbook idempotent and honour an exception tag Prevents the fix-then-refight loop
9 Test new auto-remediation in a sandbox before Automatic=true in prod Catches assume-role and blast-radius failures
10 Use an organization aggregator, not account-based No per-account authorization to forget
11 Switch low-churn, low-risk types to DAILY recording Cuts CI cost with no meaningful loss
12 Route only HIGH/CRITICAL compliance changes to a ticket via EventBridge Keeps the signal actionable

Security notes

Config is a security tool, so its own security matters. Least-privilege, a tamper-proof archive, and careful remediation permissions are the whole game.

Concern Risk Control
Recorder role over-permission Role can read everything it records Use the service-linked role; do not add write perms
Delivery bucket in the same account A compromised account can delete its own evidence Deliver to a separate security/log-archive account bucket
Archive mutability Attacker deletes the proof S3 Object Lock (WORM) + SCP denying StopConfigurationRecorder/DeleteDeliveryChannel
KMS on the delivery bucket Wrong key policy blocks delivery Grant only config.amazonaws.com kms:GenerateDataKey; least-privilege the CMK
Remediation assume-role scope Over-broad role = large blast radius One narrowly-scoped role per remediation; only the actions the runbook needs
Custom Lambda rule code Supply-chain / privilege in the function Least-privilege execution role; pin dependencies; review code
Org delegated admin A delegated account can see everything Delegate to a dedicated security/audit account only
Advanced-query data exposure Inventory reveals the estate Restrict config:SelectAggregateResourceConfig to auditors

Key IAM actions to gate, and to whom:

Action Grants Give to
config:PutConfigurationRecorder / StartConfigurationRecorder Turn recording on/off Platform automation only
config:StopConfigurationRecorder Disable evidence Nobody in prod (SCP-deny; break-glass only)
config:PutConfigRule / PutOrganizationConfigRule Author rules Security engineering
config:PutRemediationConfigurations Wire auto-fixes Platform + service teams (reviewed)
config:StartRemediationExecution Trigger a manual fix On-call responders
config:SelectResourceConfig / SelectAggregateResourceConfig Query the inventory Auditors, read-only

Cost & sizing

Config has no meaningful free tier for real use — you pay for what it does. Two drivers dominate: configuration items recorded and rule evaluations. Conformance packs are billed per pack evaluation. SSM Automation for remediation is billed separately under Systems Manager.

Cost driver Roughly Note
Configuration item recorded ~$0.003 per CI (US/most regions) The main lever; scope + DAILY reduce it
Config rule evaluation ~$0.001 each (first 100k/region/mo), then cheaper tiers Per resource per evaluation
Conformance pack evaluation ~$0.0012 each (first 500k), then cheaper tiers Per rule-in-pack evaluation
Advanced queries No extra charge Query freely
SSM Automation (remediation) Systems Manager pricing Separate line item
S3 storage for history S3 pricing Small unless you keep years
SNS notifications SNS pricing Negligible

A worked example for a single 200-resource account with all-supported CONTINUOUS recording and ~20 rules:

Component Estimate Monthly (USD) Monthly (INR ≈ ₹83/$)
~4,000 CIs/month (churn + snapshots) 4,000 × $0.003 ~$12 ~₹1,000
~30,000 rule evaluations 30,000 × $0.001 ~$30 ~₹2,490
Conformance pack (one, ~40 rules) pack evals ~$10 ~₹830
Total (indicative) ~$52 ~₹4,300

Multiply by account count for an org — which is exactly why scope discipline matters at scale. The levers, ranked by impact:

Lever Saves Trade-off
Global types in one region Removes duplicate IAM CIs None — pure waste removal
DAILY for low-churn types Fewer CIs on chatty resources Slower drift detection on those
Inclusion/exclusion recording scope Skip low-value types Miss drift on excluded types
Fewer, better-scoped rules Fewer evaluations Cover only what matters
Right-size conformance packs Fewer pack evaluations Do not stack overlapping packs

Interview & exam questions

Q1. What is the difference between AWS Config and CloudTrail? CloudTrail records API activity (who called what, when, from where); Config records resource configuration state over time and whether it complies with rules. CloudTrail answers “who did it,” Config answers “is it still right.” You need both; a Config CI even links to the CloudTrail event that caused it. (SCS-C02)

Q2. Why can a Config dashboard show all-green yet be wrong? Because a recorder that is off or never started makes every rule return INSUFFICIENT_DATA, which roll-ups render as “no non-compliant resources” — green over a blind spot. Always verify describe-configuration-recorder-status shows recording: true. (SOA-C02)

Q3. When would you choose a custom Guard (policy) rule over a custom Lambda rule? When the check is a declarative property assertion on the configuration item — Guard needs no Lambda to run, permission, or patch. Use Lambda only for imperative logic, external lookups, or cross-resource state a policy language cannot express. (SOA-C02)

Q4. What is the difference between config-change and periodic triggers? Config-change rules evaluate when a CI is recorded for an in-scope resource (near real-time); periodic rules run on a fixed schedule (1–24 h) even with no change — required for account-wide checks like iam-password-policy that have no single triggering resource. (SOA-C02)

Q5. How does automatic remediation actually execute the fix? Config invokes an SSM Automation document against the NON_COMPLIANT resource, executed under an AutomationAssumeRole you supply. The role must trust ssm.amazonaws.com and carry least-privilege permissions for that specific fix; a resource parameter (usually RESOURCE_ID) tells the runbook which resource to act on. (SOA-C02)

Q6. What is a conformance pack and why use one? An immutable, versioned bundle of Config rules and their remediation in one template, deployable org-wide from a delegated-admin account — the way to roll out a whole framework (PCI, CIS, HIPAA) consistently rather than wiring rules one at a time. (SCS-C02)

Q7. Account-based vs organization aggregator — which and why? Organization aggregators require no per-account authorization (they use org trust), so they scale to the whole org automatically; account-based aggregators require each source account to run put-aggregation-authorization for the aggregator account+region, which is easy to forget per region. Prefer organization aggregators. (SAP-C02)

Q8. Why is recording global resource types in every region a problem? IAM resources are global, so includeGlobalResourceTypes=true in multiple regions records the same change once per region — duplicating CIs and cost with no added coverage. Enable it in exactly one home region. (SOA-C02)

Q9. What does INSUFFICIENT_DATA mean and how do you fix it? The rule could not evaluate — usually the recorder is not capturing the resource type the rule needs, or the rule was just created and has not yet seen a CI. Widen the recording group to include the type and wait a cycle. (SOA-C02)

Q10. How do you make Config evidence tamper-proof? Deliver to an S3 bucket in a separate security/log-archive account with Object Lock (WORM), and use an SCP to deny StopConfigurationRecorder/DeleteDeliveryChannel org-wide (with a break-glass exception) so a compromised workload account cannot erase its own evidence. (SCS-C02)

Q11. What are the two dominant Config cost drivers and how do you control them? Configuration items recorded and rule evaluations. Control CIs with scoped recording, one-region global types, and DAILY mode for low-churn types; control evaluations with fewer, better-scoped rules. (SOA-C02)

Q12. How do you stop an auto-remediation from fighting a deploy pipeline? Make the runbook idempotent and have the rule honour an exception tag (e.g. compliance-exception=true) for legitimately non-standard resources, then fix the pipeline that keeps re-introducing the violation. Otherwise the fix and the pipeline loop forever. (SOA-C02)

Quick check

  1. Your Config aggregator shows an account with zero resources and all rules green. What is the first thing you check, and with which command?
  2. You need a compliance check that asserts a property on an S3 bucket’s configuration item. Which rule type avoids running any Lambda?
  3. Which recorder setting, if enabled in multiple regions, silently duplicates IAM configuration items and cost?
  4. A rule sits in EVALUATING forever. Name the two most likely causes for a custom Lambda rule.
  5. Auto-remediation is wired with Automatic=true but the resource stays NON_COMPLIANT and no execution appears. Where do you look and what is the usual cause?

Answers

  1. That the recorder is actually startedaws configservice describe-configuration-recorder-status should show recording: true. Zero resources + green usually means the recorder never started, so every rule returns INSUFFICIENT_DATA (which renders as green).
  2. A custom Guard (policy) rule (CUSTOM_POLICY, guard-2.0.0) — AWS runs the policy against the CI; there is no Lambda to deploy or permission.
  3. recordingGroup.includeGlobalResourceTypes = true — enable it in exactly one region.
  4. The function never calls PutEvaluations, or config.amazonaws.com lacks lambda:InvokeFunction permission on the function.
  5. aws configservice describe-remediation-execution-status — the usual cause is an AutomationAssumeRole SSM cannot assume (wrong trust) or that lacks the specific permission the runbook needs (e.g. s3:PutBucketPublicAccessBlock).

Glossary

Term Definition
Configuration recorder The per-region, per-account component that captures resource configuration as configuration items.
Recording group The recorder setting deciding which resource types (and whether global types) are captured.
Recording mode CONTINUOUS (per change) or DAILY (at most once a day) capture, settable per resource type.
Delivery channel The S3 bucket (required) and SNS topic (optional) the recorder delivers to.
Configuration item (CI) A point-in-time JSON snapshot of one resource’s state, relationships and related events.
Relationship A recorded link between two resources (e.g. instance is associated with a security group).
Configuration timeline The ordered history of a resource’s CIs, with diffs and the CloudTrail events that caused them.
Config rule A check that evaluates CIs and returns COMPLIANT / NON_COMPLIANT / NOT_APPLICABLE / INSUFFICIENT_DATA.
Managed rule An AWS-authored Config rule you configure by parameters.
Custom Guard rule A CloudFormation Guard (guard-2.0.0) policy rule AWS runs with no Lambda.
Custom Lambda rule A Config rule whose logic runs in your Lambda, which must call PutEvaluations.
Trigger What causes a rule to evaluate: configuration-change, periodic, or both.
Conformance pack An immutable, versioned bundle of rules and remediation deployed as one unit, optionally org-wide.
Remediation configuration The wiring that runs an SSM Automation runbook against a NON_COMPLIANT resource.
AutomationAssumeRole The IAM role SSM assumes to perform a remediation; trusts ssm.amazonaws.com.
Aggregator A read-only, multi-account/region roll-up of CIs and compliance, ideally org-based.

Next steps

AWSAWS ConfigComplianceGovernanceRemediationConformance PacksSSM AutomationCloudFormation Guard
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading