Guide · AWS Compliance

AWS Config Aggregator for Multi-Account MCP Compliance

A Config aggregator collects configuration and compliance data from multiple AWS accounts and regions into a single view — so you can query "which accounts have DynamoDB tables without CMK encryption?" without logging into each account individually. Three things teams get wrong: an aggregator does not deploy Config rules to member accounts (the aggregator only reads data; you still need to deploy rules in each account using organizational conformance packs or your own IaC pipeline — the aggregator is a read plane, not a control plane), the organization aggregator requires Config to already be enabled in every member account (accounts with Config disabled show no data in the aggregator, not an error — you can easily miss non-compliant accounts that have Config entirely disabled), and Config advanced queries operate on the aggregated configuration snapshot, not real-time rule evaluation results (compliance status in the aggregator can lag behind the actual compliance state by up to the next snapshot delivery cycle, typically 3-6 hours).

TL;DR

Create an organization aggregator in your management or security tooling account. It automatically authorizes all member accounts and collects their Config data. Use Config advanced queries (SQL) against the aggregator to run cross-account compliance reports. Aggregated rules show compliance by account and region — useful for identifying which teams own non-compliant resources. Config data in the aggregator is a snapshot — for real-time compliance status, query individual accounts directly.

Organization aggregator vs individual account aggregation

Config supports two aggregator source types. An organization aggregator automatically includes all current and future member accounts in your AWS Organization — you specify the organization as a source, and Config handles authorization for each member account automatically. An individual account aggregator requires explicitly listing each source account ID and region, and each source account must grant the aggregator account permission via put-aggregation-authorization. For MCP server fleets running across many accounts, use an organization aggregator — the automatic authorization saves significant setup effort and ensures new accounts are automatically included.

AttributeOrganization aggregatorIndividual account aggregator
Authorization Automatic via AWS Organizations trust Manual put-aggregation-authorization in each source account
New account coverage Automatic — new member accounts included immediately Manual — must add new accounts to the aggregator source list
Where to create Management account or delegated Config admin account Any account — source accounts grant permission explicitly
Excluded accounts Cannot exclude specific accounts (all-or-nothing per organization) Include only the accounts you specify
Best for Full organization compliance reporting Selective aggregation (e.g., prod accounts only)

Creating an organization aggregator

The aggregator must be created in the management account or in an account designated as the delegated Config administrator. Delegated admin is preferred for security tooling accounts — it keeps compliance data out of the root management account and follows the principle of separation of concerns. The AllRegions: true setting aggregates Config data from every AWS region where member accounts have Config enabled.

import * as cdk from "aws-cdk-lib";
import * as config from "aws-cdk-lib/aws-config";

// Organization aggregator — collects Config data from all member accounts in all regions
// Must be deployed in the management account or delegated Config admin account
const aggregator = new config.CfnConfigurationAggregator(this, "McpOrgAggregator", {
  configurationAggregatorName: "mcp-org-aggregator",
  organizationAggregationSource: {
    // true = aggregate from all regions where member accounts have Config enabled
    allAwsRegions: true,
    // allAwsRegions: false + awsRegions: ["us-east-1", "us-west-2"] for selective regions
    roleArn: `arn:aws:iam::${this.account}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig`,
    // The service-linked role is created automatically when Config is enabled
    // For org aggregator it needs Organizations:List* and Organizations:Describe* permissions
    // which the SLR already has — do not create a custom role
  },
});

// CDK output for the aggregator name (used in advanced queries)
new cdk.CfnOutput(this, "AggregatorName", {
  value: aggregator.configurationAggregatorName!,
  description: "Use this in SelectAggregateResourceConfig and GetAggregateComplianceDetailsByConfigRule",
});
# For individual account aggregation (when not using Organizations):
# Step 1: In each source account, grant aggregation permission to the aggregator account
aws configservice put-aggregation-authorization \
  --authorized-account-id 123456789012 \  # aggregator account ID
  --authorized-aws-region us-east-1 \      # region where aggregator is deployed
  --region us-west-2                        # source account's region

# Step 2: In the aggregator account, create the aggregator referencing source accounts
aws configservice put-configuration-aggregator \
  --configuration-aggregator-name mcp-selective-aggregator \
  --account-aggregation-sources \
    '[{"AccountIds":["234567890123","345678901234"],"AllAwsRegions":true}]'

Config advanced queries — SQL against aggregated configuration data

Config advanced queries let you run SQL-like queries against the aggregated configuration snapshot. The query targets a virtual table of resource configuration items and their compliance status. This is the most powerful way to answer cross-account compliance questions — "show me every DynamoDB table without CMK encryption across all accounts" — without writing custom tooling.

import {
  ConfigServiceClient,
  SelectAggregateResourceConfigCommand,
} from "@aws-sdk/client-config-service";

const client = new ConfigServiceClient({ region: "us-east-1" });
const AGGREGATOR = "mcp-org-aggregator";

// Query: all non-compliant DynamoDB tables across all accounts
async function getNonCompliantDynamoDbTables(): Promise<void> {
  let nextToken: string | undefined;
  do {
    const result = await client.send(
      new SelectAggregateResourceConfigCommand({
        ConfigurationAggregatorName: AGGREGATOR,
        Expression: `
          SELECT
            accountId,
            awsRegion,
            resourceId,
            resourceName,
            configuration.tableStatus,
            configuration.sseDescription.sseType,
            configuration.sseDescription.kmsMasterKeyArn,
            tags
          WHERE
            resourceType = 'AWS::DynamoDB::Table'
            AND (
              configuration.sseDescription.sseType <> 'KMS'
              OR configuration.sseDescription IS NULL
            )
          ORDER BY accountId, awsRegion
        `,
        MaxResults: 100,
        NextToken: nextToken,
      })
    );

    for (const item of result.Results ?? []) {
      const resource = JSON.parse(item);
      console.log(
        `Account: ${resource.accountId} | Region: ${resource.awsRegion}`,
        `| Table: ${resource.resourceName}`,
        `| SSE type: ${resource.configuration?.sseDescription?.sseType ?? "none"}`
      );
    }
    nextToken = result.NextToken;
  } while (nextToken);
}

// Query: compliance summary per Config rule per account
async function getComplianceSummaryByAccount(): Promise<void> {
  const result = await client.send(
    new SelectAggregateResourceConfigCommand({
      ConfigurationAggregatorName: AGGREGATOR,
      Expression: `
        SELECT
          accountId,
          awsRegion,
          COUNT(*) AS totalResources
        WHERE
          resourceType = 'AWS::Config::ConfigRule'
        GROUP BY
          accountId, awsRegion
        ORDER BY
          accountId
      `,
      MaxResults: 500,
    })
  );
  console.log("Config rule counts by account/region:", result.Results?.map(r => JSON.parse(r)));
}

// Query: resources missing required tags across all accounts
async function getUntaggedResources(requiredTagKey: string): Promise<void> {
  // Config advanced queries support IS NULL for tag absence checking
  // Note: tag queries use dot notation on the tags object
  const result = await client.send(
    new SelectAggregateResourceConfigCommand({
      ConfigurationAggregatorName: AGGREGATOR,
      Expression: `
        SELECT
          accountId,
          awsRegion,
          resourceType,
          resourceId,
          resourceName,
          tags
        WHERE
          (
            resourceType = 'AWS::DynamoDB::Table'
            OR resourceType = 'AWS::Lambda::Function'
            OR resourceType = 'AWS::ECS::TaskDefinition'
          )
          AND tags IS NULL
        ORDER BY resourceType, accountId
      `,
      MaxResults: 200,
    })
  );
  console.log(`Resources with no tags at all:`, result.Results?.length ?? 0);
}

Querying aggregated rule compliance

Use GetAggregateComplianceDetailsByConfigRule to get per-resource compliance results for a specific rule across all accounts in the aggregator. Use GetAggregateConfigRuleComplianceSummary to get rule-level compliance counts grouped by account ID or AWS region — useful for a compliance dashboard showing which accounts have the most rule violations.

import {
  ConfigServiceClient,
  GetAggregateComplianceDetailsByConfigRuleCommand,
  GetAggregateConfigRuleComplianceSummaryCommand,
} from "@aws-sdk/client-config-service";

const client = new ConfigServiceClient({ region: "us-east-1" });

// Get all non-compliant resources for a specific rule across all accounts
async function getNonCompliantResourcesForRule(ruleName: string): Promise<void> {
  let nextToken: string | undefined;
  do {
    const result = await client.send(
      new GetAggregateComplianceDetailsByConfigRuleCommand({
        ConfigurationAggregatorName: "mcp-org-aggregator",
        ConfigRuleName: ruleName,
        ComplianceType: "NON_COMPLIANT",
        // AccountId + AwsRegion are optional — omit to get all accounts/regions
        MaxResults: 100,
        NextToken: nextToken,
      })
    );

    for (const eval_ of result.AggregateEvaluationResults ?? []) {
      const id = eval_.EvaluationResultIdentifier?.EvaluationResultQualifier;
      console.log(
        `Account: ${eval_.AccountId} | Region: ${eval_.AwsRegion}`,
        `| ${id?.ResourceType}/${id?.ResourceId}`,
        `| Last evaluated: ${eval_.ResultRecordedTime?.toISOString()}`
      );
      if (eval_.Annotation) {
        console.log(`  Annotation: ${eval_.Annotation}`);
      }
    }
    nextToken = result.NextToken;
  } while (nextToken);
}

// Compliance summary grouped by account — shows which accounts have the most violations
async function getComplianceSummaryGroupedByAccount(): Promise<void> {
  const result = await client.send(
    new GetAggregateConfigRuleComplianceSummaryCommand({
      ConfigurationAggregatorName: "mcp-org-aggregator",
      Filters: {
        ComplianceType: "NON_COMPLIANT",
      },
      GroupByKey: "ACCOUNT_ID",
      MaxResults: 100,
    })
  );

  for (const group of result.GroupedResourceCounts ?? []) {
    console.log(
      `Account: ${group.GroupName}`,
      `| Non-compliant rules: ${group.ResourceCount}`
    );
  }
}

Delegated Config admin — keeping aggregation out of the root account

AWS Organizations allows you to delegate Config administration to a member account. The delegated admin account can create organization conformance packs, organization Config rules, and aggregators on behalf of the organization — without the security risk of performing compliance operations from the root management account. Delegating to a dedicated security tooling account is best practice for larger organizations.

# Register a member account as delegated Config admin
# Must run from the management account
aws organizations register-delegated-administrator \
  --account-id 123456789012 \              # your security/compliance tooling account
  --service-principal config.amazonaws.com

# Verify delegation
aws organizations list-delegated-administrators \
  --service-principal config.amazonaws.com

# From the delegated admin account, create an org aggregator
aws configservice put-configuration-aggregator \
  --configuration-aggregator-name mcp-org-aggregator \
  --organization-aggregation-source \
    '{"RoleArn":"arn:aws:iam::123456789012:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig","AllAwsRegions":true}' \
  --region us-east-1

# Deploy an org conformance pack from the delegated admin account
aws configservice put-organization-conformance-pack \
  --organization-conformance-pack-name mcp-compliance-baseline \
  --template-s3-uri s3://mcp-config-templates-123456789012/mcp-compliance-pack.yaml \
  --delivery-s3-bucket mcp-config-delivery-123456789012

Failure modes and common mistakes

SymptomRoot causeFix
Aggregator shows data for some accounts but not others Config not enabled in the missing accounts — aggregator can only read what's there Enable Config recorder in every member account; use describe-aggregation-authorizations to see which accounts have authorized the aggregator
Advanced query returns stale compliance status Aggregator reflects the last snapshot delivery — not real-time evaluation results For real-time status, query individual accounts with get-compliance-details-by-config-rule; use the aggregator for trending and cross-account reporting only
Cannot create organization aggregator — access denied Creating from a member account without delegated admin permissions, or delegated admin not registered Register delegated admin from management account first; or create from the management account itself
Advanced query returns no results even though non-compliant resources exist Query uses compliance-status fields that aren't part of the configuration snapshot schema; compliance results are in a different data source Use GetAggregateComplianceDetailsByConfigRule for compliance results; use advanced queries for configuration attribute queries (resource properties, tags)
Aggregator data missing for resources in specific regions AllAwsRegions: false set, or the specified region list doesn't include the missing region Use AllAwsRegions: true for complete coverage; or add the missing region to the explicit region list
Organization aggregator stopped collecting data after organizational change AWS Organizations service control policy change, or management account's Config service-linked role permissions were modified Verify the SLR for Config has OrganizationRead permissions; re-create the aggregator if the SLR was deleted